|
|
@@ -254,6 +254,63 @@ export default function WordPreview() {
|
|
|
return mimeTypes[extension || ''] || 'image/jpeg';
|
|
|
};
|
|
|
|
|
|
+ // 解析Word模板中的图片尺寸限制
|
|
|
+ const parseImageSizeLimits = async (wordFile: File): Promise<Record<string, { width: number; height: number }>> => {
|
|
|
+ try {
|
|
|
+ const arrayBuffer = await wordFile.arrayBuffer();
|
|
|
+ const zip = new PizZip(arrayBuffer);
|
|
|
+
|
|
|
+ // 读取word/document.xml文件
|
|
|
+ const documentXml = zip.file('word/document.xml');
|
|
|
+ if (!documentXml) return {};
|
|
|
+
|
|
|
+ const xmlContent = documentXml.asText();
|
|
|
+
|
|
|
+ // 解析XML内容,查找图片尺寸说明
|
|
|
+ const sizeLimits: Record<string, { width: number; height: number }> = {};
|
|
|
+
|
|
|
+ // 使用正则表达式匹配图片尺寸说明格式:{图片大小:高度:10cm,宽度8厘米}
|
|
|
+ const sizePattern = /\{图片大小:高度:(\d+(?:\.\d+)?)(?:cm|厘米|毫米|mm),宽度(\d+(?:\.\d+)?)(?:cm|厘米|毫米|mm)\}/g;
|
|
|
+
|
|
|
+ let match;
|
|
|
+ while ((match = sizePattern.exec(xmlContent)) !== null) {
|
|
|
+ const height = parseFloat(match[1]);
|
|
|
+ const width = parseFloat(match[2]);
|
|
|
+
|
|
|
+ // 将厘米转换为像素(假设1cm ≈ 37.795像素)
|
|
|
+ const heightPx = Math.round(height * 37.795);
|
|
|
+ const widthPx = Math.round(width * 37.795);
|
|
|
+
|
|
|
+ // 为所有图片设置相同的尺寸限制
|
|
|
+ sizeLimits['default'] = { width: widthPx, height: heightPx };
|
|
|
+ }
|
|
|
+
|
|
|
+ return sizeLimits;
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Failed to parse image size limits:', error);
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 计算保持比例的图片尺寸
|
|
|
+ const calculateProportionalSize = (
|
|
|
+ originalWidth: number,
|
|
|
+ originalHeight: number,
|
|
|
+ maxWidth: number,
|
|
|
+ maxHeight: number
|
|
|
+ ): [number, number] => {
|
|
|
+ const widthRatio = maxWidth / originalWidth;
|
|
|
+ const heightRatio = maxHeight / originalHeight;
|
|
|
+
|
|
|
+ // 使用较小的比例以保持原始长宽比
|
|
|
+ const ratio = Math.min(widthRatio, heightRatio, 1);
|
|
|
+
|
|
|
+ const newWidth = Math.round(originalWidth * ratio);
|
|
|
+ const newHeight = Math.round(originalHeight * ratio);
|
|
|
+
|
|
|
+ return [newWidth, newHeight];
|
|
|
+ };
|
|
|
+
|
|
|
// 替换Word字段并插入图片
|
|
|
const replaceFieldsInWord = async (wordFile: File, excelRow: ExcelRow, rowIndex: number): Promise<Blob> => {
|
|
|
try {
|
|
|
@@ -275,7 +332,11 @@ export default function WordPreview() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 配置图片模块 - 使用实际图片尺寸
|
|
|
+ // 解析图片尺寸限制
|
|
|
+ const sizeLimits = await parseImageSizeLimits(wordFile);
|
|
|
+ const defaultLimit = sizeLimits['default'] || { width: 200, height: 150 };
|
|
|
+
|
|
|
+ // 配置图片模块 - 使用实际图片尺寸并应用限制
|
|
|
const imageSizeCache = new Map<string, [number, number]>();
|
|
|
|
|
|
const imageOpts = {
|
|
|
@@ -287,48 +348,34 @@ export default function WordPreview() {
|
|
|
return null;
|
|
|
},
|
|
|
getSize: (img: ArrayBuffer, tagValue: string, tagName: string) => {
|
|
|
- console.log('tagName', tagName)
|
|
|
- console.log('tagValue', tagValue)
|
|
|
- // 从图片数据中获取实际尺寸
|
|
|
try {
|
|
|
- // 为每个图片创建唯一的缓存键
|
|
|
const cacheKey = `${tagValue}_${img.byteLength}`;
|
|
|
|
|
|
- // 如果已经缓存了尺寸,直接返回
|
|
|
if (imageSizeCache.has(cacheKey)) {
|
|
|
return imageSizeCache.get(cacheKey)!;
|
|
|
}
|
|
|
|
|
|
- // 简化的图片尺寸检测(基于图片数据特征)
|
|
|
- // 这里我们使用一个合理的方法来从图片数据推断尺寸
|
|
|
+ // 获取图片原始尺寸
|
|
|
+ let originalWidth = 200;
|
|
|
+ let originalHeight = 150;
|
|
|
+
|
|
|
const view = new DataView(img);
|
|
|
|
|
|
// PNG格式检测
|
|
|
if (view.getUint32(0) === 0x89504E47 && view.getUint32(4) === 0x0D0A1A0A) {
|
|
|
- // PNG IHDR chunk: width at offset 16, height at offset 20
|
|
|
- const width = view.getUint32(16);
|
|
|
- const height = view.getUint32(20);
|
|
|
- const size: [number, number] = [width, height];
|
|
|
- console.log('size', size)
|
|
|
- imageSizeCache.set(cacheKey, size);
|
|
|
- return size;
|
|
|
+ originalWidth = view.getUint32(16);
|
|
|
+ originalHeight = view.getUint32(20);
|
|
|
}
|
|
|
-
|
|
|
// JPEG格式检测
|
|
|
- if (view.getUint16(0) === 0xFFD8) {
|
|
|
- // 简化的JPEG尺寸检测
|
|
|
+ else if (view.getUint16(0) === 0xFFD8) {
|
|
|
let offset = 2;
|
|
|
while (offset < img.byteLength - 10) {
|
|
|
if (view.getUint8(offset) === 0xFF) {
|
|
|
const marker = view.getUint8(offset + 1);
|
|
|
if (marker >= 0xC0 && marker <= 0xC3) {
|
|
|
- // SOF marker found
|
|
|
- const height = view.getUint16(offset + 5);
|
|
|
- const width = view.getUint16(offset + 7);
|
|
|
- const size: [number, number] = [width, height];
|
|
|
- console.log('size', size)
|
|
|
- imageSizeCache.set(cacheKey, size);
|
|
|
- return size;
|
|
|
+ originalHeight = view.getUint16(offset + 5);
|
|
|
+ originalWidth = view.getUint16(offset + 7);
|
|
|
+ break;
|
|
|
}
|
|
|
const length = view.getUint16(offset + 2);
|
|
|
offset += length + 2;
|
|
|
@@ -338,27 +385,28 @@ export default function WordPreview() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 如果无法检测尺寸,使用默认尺寸
|
|
|
- const defaultSize: [number, number] = [200, 150];
|
|
|
- imageSizeCache.set(cacheKey, defaultSize);
|
|
|
- console.log('defaultSize', defaultSize)
|
|
|
- return defaultSize;
|
|
|
+ // 计算符合尺寸限制的最终尺寸
|
|
|
+ const [finalWidth, finalHeight] = calculateProportionalSize(
|
|
|
+ originalWidth,
|
|
|
+ originalHeight,
|
|
|
+ defaultLimit.width,
|
|
|
+ defaultLimit.height
|
|
|
+ );
|
|
|
+
|
|
|
+ const finalSize: [number, number] = [finalWidth, finalHeight];
|
|
|
+ imageSizeCache.set(cacheKey, finalSize);
|
|
|
+
|
|
|
+ return finalSize;
|
|
|
|
|
|
} catch (error) {
|
|
|
console.warn('Failed to get image size, using default:', error);
|
|
|
- return [200, 150];
|
|
|
+ return [defaultLimit.width, defaultLimit.height];
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const imageModule = new ImageModule(imageOpts);
|
|
|
|
|
|
- const doc = new Docxtemplater(zip, {
|
|
|
- paragraphLoop: true,
|
|
|
- linebreaks: true,
|
|
|
- modules: [imageModule]
|
|
|
- })
|
|
|
-
|
|
|
// 处理嵌套数据结构
|
|
|
const processedData: Record<string, any> = {};
|
|
|
|
|
|
@@ -936,21 +984,28 @@ export default function WordPreview() {
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
- <h4 className="font-medium mb-1">2. 准备Excel数据</h4>
|
|
|
+ <h4 className="font-medium mb-1">2. 设置图片尺寸限制</h4>
|
|
|
+ <p className="text-muted-foreground">
|
|
|
+ 在模板中添加图片尺寸说明:{'{图片大小:高度:10cm,宽度8厘米}'},系统将自动限制图片尺寸并保留长宽比例
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>
|
|
|
+ <h4 className="font-medium mb-1">3. 准备Excel数据</h4>
|
|
|
<p className="text-muted-foreground">
|
|
|
Excel文件第一行为表头,列名应与Word模板中的字段名对应
|
|
|
</p>
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
- <h4 className="font-medium mb-1">3. 准备图片压缩包</h4>
|
|
|
+ <h4 className="font-medium mb-1">4. 准备图片压缩包</h4>
|
|
|
<p className="text-muted-foreground">
|
|
|
压缩包结构:第一层为序号文件夹(1,2,3...对应Excel行),第二层为图片文件(文件名对应模板中的图片名)
|
|
|
</p>
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
- <h4 className="font-medium mb-1">4. 图片命名规则</h4>
|
|
|
+ <h4 className="font-medium mb-1">5. 图片命名规则</h4>
|
|
|
<p className="text-muted-foreground">
|
|
|
例如:模板中使用 {'{%logo}'},则图片文件应命名为 logo.jpg/png等
|
|
|
</p>
|