Browse Source

✨ feat(word-preview): 实现图片尺寸自动限制功能

- 添加parseImageSizeLimits函数,支持从Word模板解析图片尺寸限制说明
- 实现calculateProportionalSize函数,确保图片按比例缩放至符合限制
- 优化图片尺寸获取逻辑,支持PNG和JPEG格式原始尺寸检测
- 更新图片插入逻辑,应用尺寸限制并保持原始比例
- 调整使用指南步骤编号,增加图片尺寸限制说明

📝 docs(word-preview): 更新使用指南

- 添加图片尺寸限制使用说明:{图片大小:高度:10cm,宽度8厘米}格式
- 调整步骤编号,将原步骤3-4顺延为4-5
yourname 4 months ago
parent
commit
da99ed7171
1 changed files with 95 additions and 40 deletions
  1. 95 40
      src/client/home-shadcn/pages/WordPreview.tsx

+ 95 - 40
src/client/home-shadcn/pages/WordPreview.tsx

@@ -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>