|
@@ -28,6 +28,13 @@ use Symfony\Component\Process\Process;
|
|
|
class ExamPdfExportService
|
|
class ExamPdfExportService
|
|
|
{
|
|
{
|
|
|
private ?KatexRenderer $katexRenderer = null;
|
|
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(
|
|
public function __construct(
|
|
|
private readonly LearningAnalyticsService $learningAnalyticsService,
|
|
private readonly LearningAnalyticsService $learningAnalyticsService,
|
|
@@ -1391,6 +1398,7 @@ class ExamPdfExportService
|
|
|
{
|
|
{
|
|
|
$tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_').'.html';
|
|
$tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_').'.html';
|
|
|
$utf8Html = $this->ensureUtf8Html($html);
|
|
$utf8Html = $this->ensureUtf8Html($html);
|
|
|
|
|
+ $utf8Html = $this->applyAdaptiveWideImageSizing($utf8Html);
|
|
|
$written = file_put_contents($tmpHtml, $utf8Html);
|
|
$written = file_put_contents($tmpHtml, $utf8Html);
|
|
|
|
|
|
|
|
Log::debug('ExamPdfExportService: HTML文件已创建', [
|
|
Log::debug('ExamPdfExportService: HTML文件已创建', [
|
|
@@ -1413,6 +1421,117 @@ class ExamPdfExportService
|
|
|
return $chromePdf;
|
|
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
|
|
* 使用Chrome渲染PDF
|
|
|
*/
|
|
*/
|