mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
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.
73 lines
2.9 KiB
Python
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
|