Files
maziggy 86bd9e1bd4 fix(file-manager): harden the merged previews - PDF.js legacy build, server PDF thumbnails, STEP progress (issue #2976)
Follow-ups after merging PR #3128 (with the #2990 preview work):

- PDF preview failed on every browser without
  Map.prototype.getOrInsertComputed (Chrome < 145, Firefox < 144,
  older Safari): "This file cannot be previewed" for any PDF. Load
  pdf.js's legacy build, which bundles the polyfills for page and
  worker, and bundle the worker through Vite (?worker&url) so
  build.target lowers its class static block for Safari 16.0-16.3.
  The browser-baseline check only scanned .js output and never saw
  the copied .mjs worker; it scans .mjs too.
- PDF grid thumbnails only existed after someone opened the preview.
  Page one is now rendered server-side with PDFium (pypdfium2, new
  dependency, prebuilt wheels for every shipped platform) on upload,
  ZIP extraction and external-folder scans; Generate Thumbnails
  backfills PDFs as well. Renders are serialised behind a lock
  (PDFium is not thread-safe), run off the event loop, and scale
  from the page size so a huge MediaBox cannot allocate a huge
  bitmap. Unreadable PDFs land without a thumbnail and keep the
  browser fallback.
- A large STEP file takes over a minute to mesh in the browser and
  showed only a spinner. STEP loads now show "Converting STEP
  model... N s" and a note that large files can take a minute or
  more, in all 15 locales.
- Drop the two occt-import-js "externalized for browser
  compatibility" build warnings (path/crypto are only required in
  its Node branch); any other externalization still shows.
2026-09-26 09:51:52 +02:00

73 lines
2.9 KiB
Python

"""PDF Thumbnail Generation Service.
Renders the first page of a PDF with PDFium (pypdfium2) so a library PDF gets
its grid thumbnail at upload/scan time. Before this, PDF thumbnails only
existed once somebody had opened the in-browser preview, which posted its
first render back (#2976); that route stays as the fallback for a PDF this
renderer cannot read.
"""
import logging
import threading
import uuid
from pathlib import Path
logger = logging.getLogger(__name__)
# PDFium is not thread-safe, and callers run this through asyncio.to_thread,
# so two uploads landing together would otherwise enter it concurrently.
_PDFIUM_LOCK = threading.Lock()
# Rendered at twice the output size and downsampled, so small print on the
# page stays legible instead of breaking up into aliased specks.
_SUPERSAMPLE = 2
def generate_pdf_thumbnail(pdf_path: Path, thumbnails_dir: Path, size: int = 256) -> str | None:
"""Render page one of a PDF into a square, white-backed PNG thumbnail.
Same shape as the thumbnail the browser preview posts back (a ``size``
square with the page centred on white), so a grid mixing both kinds
looks uniform. Returns the thumbnail path, or None if the file cannot be
rendered - an encrypted, empty or damaged PDF simply gets no thumbnail.
"""
try:
import pypdfium2 as pdfium
from PIL import Image
except ImportError:
logger.warning("pypdfium2 not installed, skipping PDF thumbnail for %s", pdf_path.name)
return None
try:
with _PDFIUM_LOCK:
pdf = pdfium.PdfDocument(str(pdf_path))
try:
if len(pdf) == 0:
return None
page = pdf[0]
try:
width, height = page.get_size()
if width <= 0 or height <= 0:
return None
# Scale from the page's own size, so the bitmap is bounded
# by the output size whatever MediaBox the file declares.
scale = (size * _SUPERSAMPLE) / max(width, height)
rendered = page.render(scale=scale).to_pil()
finally:
page.close()
finally:
pdf.close()
rendered = rendered.convert("RGB")
rendered.thumbnail((size, size), Image.Resampling.LANCZOS)
thumbnail = Image.new("RGB", (size, size), (255, 255, 255))
thumbnail.paste(rendered, ((size - rendered.width) // 2, (size - rendered.height) // 2))
thumb_filename = f"{uuid.uuid4().hex}.png"
thumb_path = thumbnails_dir / thumb_filename # SEC-PATH-OK: thumb_filename = uuid.uuid4().hex + ".png"
thumbnail.save(thumb_path, "PNG", optimize=True)
return str(thumb_path)
except Exception as e:
logger.warning("Failed to create PDF thumbnail for %s: %s", pdf_path.name, e)
return None