Prechádzať zdrojové kódy

fix(pdf): refine adaptive image sizing and option image constraints

Limit adaptive widening to qualified flat question images and tighten option-image bounds for more consistent PDF rendering.

Made-with: Cursor
yemeishu 4 týždňov pred
rodič
commit
32b7441f17

+ 119 - 0
app/Services/ExamPdfExportService.php

@@ -28,6 +28,13 @@ use Symfony\Component\Process\Process;
 class ExamPdfExportService
 {
     private ?KatexRenderer $katexRenderer = null;
+    private const PDF_IMAGE_WIDTH_WIDE_PX = 250;
+    private const PDF_IMAGE_WIDTH_VERY_WIDE_PX = 330;
+
+    /**
+     * @var array<string, array{w:int,h:int}|null>
+     */
+    private array $pdfImageDimensionCache = [];
 
     public function __construct(
         private readonly LearningAnalyticsService $learningAnalyticsService,
@@ -1391,6 +1398,7 @@ class ExamPdfExportService
     {
         $tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_').'.html';
         $utf8Html = $this->ensureUtf8Html($html);
+        $utf8Html = $this->applyAdaptiveWideImageSizing($utf8Html);
         $written = file_put_contents($tmpHtml, $utf8Html);
 
         Log::debug('ExamPdfExportService: HTML文件已创建', [
@@ -1413,6 +1421,117 @@ class ExamPdfExportService
         return $chromePdf;
     }
 
+    /**
+     * 对扁长/超扁长图片做全局自适应放大,普通图片不处理。
+     */
+    private function applyAdaptiveWideImageSizing(string $html): string
+    {
+        return (string) preg_replace_callback(
+            '/<img\b[^>]*\bsrc=(["\'])([^"\']+)\1[^>]*>/i',
+            function (array $m): string {
+                $fullTag = $m[0] ?? '';
+                $src = $m[2] ?? '';
+                if ($fullTag === '' || $src === '' || str_starts_with($src, 'data:')) {
+                    return $fullTag;
+                }
+                if (! $this->shouldApplyAdaptiveSizingToSrc($src)) {
+                    return $fullTag;
+                }
+
+                $dim = $this->getPdfImageDimensions($src);
+                if (! $dim || ($dim['w'] ?? 0) <= 0 || ($dim['h'] ?? 0) <= 0) {
+                    return $fullTag;
+                }
+
+                $ratio = $dim['w'] / max(1, $dim['h']);
+                if ($ratio < 2.8) {
+                    return $fullTag;
+                }
+
+                $targetWidth = $ratio >= 3.5 ? self::PDF_IMAGE_WIDTH_VERY_WIDE_PX : self::PDF_IMAGE_WIDTH_WIDE_PX;
+                $targetWidth = min($targetWidth, $dim['w']);
+                $targetStyle = sprintf(
+                    'width:%dpx!important;max-width:%dpx!important;max-height:60mm!important;height:auto!important;object-fit:contain!important;',
+                    $targetWidth,
+                    $targetWidth
+                );
+
+                if (preg_match('/\sstyle=(["\'])(.*?)\1/i', $fullTag, $sm)) {
+                    $originStyle = $sm[2] ?? '';
+                    $originStyle = preg_replace('/\bmax-width\s*:[^;]+;?/i', '', $originStyle);
+                    $originStyle = preg_replace('/\bmax-height\s*:[^;]+;?/i', '', $originStyle);
+                    $originStyle = preg_replace('/\bwidth\s*:[^;]+;?/i', '', $originStyle);
+                    $originStyle = preg_replace('/\bheight\s*:[^;]+;?/i', '', $originStyle);
+                    $originStyle = preg_replace('/\bobject-fit\s*:[^;]+;?/i', '', $originStyle);
+                    $newStyle = $targetStyle.trim((string) $originStyle);
+
+                    return preg_replace(
+                        '/\sstyle=(["\'])(.*?)\1/i',
+                        ' style="'.$newStyle.'"',
+                        $fullTag,
+                        1
+                    ) ?? $fullTag;
+                }
+
+                return preg_replace('/<img\b/i', '<img style="'.$targetStyle.'"', $fullTag, 1) ?? $fullTag;
+            },
+            $html
+        );
+    }
+
+    /**
+     * @return array{w:int,h:int}|null
+     */
+    private function getPdfImageDimensions(string $src): ?array
+    {
+        if (array_key_exists($src, $this->pdfImageDimensionCache)) {
+            return $this->pdfImageDimensionCache[$src];
+        }
+
+        try {
+            if (! str_starts_with($src, 'http://') && ! str_starts_with($src, 'https://')) {
+                $this->pdfImageDimensionCache[$src] = null;
+
+                return null;
+            }
+
+            $size = @getimagesize($src);
+            if (is_array($size) && count($size) >= 2) {
+                $data = ['w' => (int) $size[0], 'h' => (int) $size[1]];
+                $this->pdfImageDimensionCache[$src] = $data;
+
+                return $data;
+            }
+
+            $this->pdfImageDimensionCache[$src] = null;
+
+            return null;
+        } catch (\Throwable $e) {
+            Log::debug('ExamPdfExportService: 图片尺寸探测失败', ['src' => $src, 'error' => $e->getMessage()]);
+            $this->pdfImageDimensionCache[$src] = null;
+
+            return null;
+        }
+    }
+
+    private function shouldApplyAdaptiveSizingToSrc(string $src): bool
+    {
+        $parts = parse_url($src);
+        if (! is_array($parts)) {
+            return false;
+        }
+        $host = strtolower((string) ($parts['host'] ?? ''));
+        $path = (string) ($parts['path'] ?? '');
+        if ($host !== 'file.chunsunqiuzhu.com') {
+            return false;
+        }
+        if (! str_contains($path, '/data/')) {
+            return false;
+        }
+
+        return (bool) preg_match('/\.(png|jpe?g|webp)$/i', $path);
+    }
+
     /**
      * 使用Chrome渲染PDF
      */

+ 3 - 1
resources/views/pdf/exam-grading.blade.php

@@ -337,8 +337,10 @@
         }
         /* 选项中的图片样式 - 防止超出容器 */
         .option img {
-            max-width: 100%;
+            max-width: 92%;
+            max-height: 42mm;
             height: auto;
+            object-fit: contain;
             vertical-align: middle;
         }
         .question-stem .katex,

+ 3 - 1
resources/views/pdf/exam-paper.blade.php

@@ -384,8 +384,10 @@
         }
         /* 选项中的图片样式 - 防止超出容器 */
         .option img {
-            max-width: 100%;
+            max-width: 92%;
+            max-height: 42mm;
             height: auto;
+            object-fit: contain;
             vertical-align: middle;
         }
         @media print {

+ 3 - 1
resources/views/pdf/partials/common-styles.blade.php

@@ -342,8 +342,10 @@
     
     /* 选项中的图片样式 */
     .option img {
-        max-width: 100%;
+        max-width: 92%;
+        max-height: 42mm;
         height: auto;
+        object-fit: contain;
         vertical-align: middle;
     }