Files
bambuddy/backend/tests/unit/test_pdf_thumbnail.py
T
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

85 lines
2.9 KiB
Python

"""Unit tests for server-side PDF thumbnails (#2976).
generate_pdf_thumbnail renders page one with PDFium into the same square,
white-backed PNG the browser preview posts back, and returns None rather than
raising for anything it cannot render.
"""
from pathlib import Path
import pytest
from PIL import Image
from reportlab.lib.pagesizes import A4, landscape
from reportlab.pdfgen import canvas
from backend.app.services.pdf_thumbnail import generate_pdf_thumbnail
def _make_pdf(path: Path, pagesize=A4, pages: int = 1) -> Path:
c = canvas.Canvas(str(path), pagesize=pagesize)
for n in range(pages):
# A solid black block in the top-left corner of each page, so the
# render can be told apart from a blank white square.
c.setFillColorRGB(0, 0, 0)
c.rect(0, pagesize[1] - 200, 200, 200, fill=1, stroke=0)
c.drawString(72, 72, f"page {n + 1}")
c.showPage()
c.save()
return path
def test_renders_page_one_as_square_png(tmp_path):
pdf = _make_pdf(tmp_path / "doc.pdf", pages=3)
thumb = generate_pdf_thumbnail(pdf, tmp_path)
assert thumb is not None
with Image.open(thumb) as img:
assert img.format == "PNG"
assert img.size == (256, 256)
assert img.mode == "RGB"
# Something was drawn: not a blank white square.
assert img.getextrema() != ((255, 255), (255, 255), (255, 255))
def test_portrait_page_is_centred_on_white(tmp_path):
pdf = _make_pdf(tmp_path / "portrait.pdf")
thumb = generate_pdf_thumbnail(pdf, tmp_path)
with Image.open(thumb) as img:
# A4 portrait is narrower than tall: the side margins are padding.
assert img.getpixel((2, 128)) == (255, 255, 255)
assert img.getpixel((253, 128)) == (255, 255, 255)
def test_landscape_page_is_centred_on_white(tmp_path):
pdf = _make_pdf(tmp_path / "landscape.pdf", pagesize=landscape(A4))
thumb = generate_pdf_thumbnail(pdf, tmp_path)
with Image.open(thumb) as img:
assert img.size == (256, 256)
assert img.getpixel((128, 2)) == (255, 255, 255)
assert img.getpixel((128, 253)) == (255, 255, 255)
def test_huge_mediabox_still_yields_bounded_thumbnail(tmp_path):
# 5 m x 5 m: the render scale comes from the page size, so this must not
# allocate a page-sized bitmap.
pdf = _make_pdf(tmp_path / "huge.pdf", pagesize=(14400, 14400))
thumb = generate_pdf_thumbnail(pdf, tmp_path)
with Image.open(thumb) as img:
assert img.size == (256, 256)
@pytest.mark.parametrize("content", [b"", b"not a pdf at all", b"%PDF-1.4\n%truncated"])
def test_unreadable_pdf_returns_none(tmp_path, content):
bad = tmp_path / "bad.pdf"
bad.write_bytes(content)
assert generate_pdf_thumbnail(bad, tmp_path) is None
assert list(tmp_path.glob("*.png")) == []
def test_missing_file_returns_none(tmp_path):
assert generate_pdf_thumbnail(tmp_path / "missing.pdf", tmp_path) is None