"""Regression coverage for canonical-coordinate PDF export.""" import json import pymupdf import pytest from app.services.export.renderer import _needs_synthetic_italic, export_annotations def _source_pdf(path) -> None: document = pymupdf.open() page = document.new_page(width=720, height=936) page.set_cropbox(pymupdf.Rect(36, 72, 648, 864)) page.set_rotation(90) document.save(path) document.close() def test_export_preserves_rotation_and_flattens_supported_annotations(tmp_path) -> None: source = tmp_path / "source.pdf" _source_pdf(source) annotations = [ { "id": "text", "page": 0, "type": "text", "rect": {"x": 40, "y": 50, "width": 180, "height": 40}, "props": { "text": "Exported text", "fontFamily": "Liberation Sans", "fontSize": 14, "color": "#112233", "styles": {"0": {"0": {"fill": "#ff0000", "fontWeight": "bold"}}}, }, }, { "id": "line", "page": 0, "type": "draw", "rect": {"x": 40, "y": 110, "width": 80, "height": 40}, "props": { "paths": [[0, 0], [80, 40]], "strokeColor": "#ff0000", "strokeWidth": 2, "opacity": 1, }, }, { "id": "highlight", "page": 0, "type": "highlight", "rect": {"x": 40, "y": 170, "width": 100, "height": 20}, "props": {"color": "#ffff00", "opacity": 0.4}, }, { "id": "signature", "page": 0, "type": "signature", "rect": {"x": 40, "y": 220, "width": 160, "height": 50}, "props": { "mode": "type", "text": "Ava", "fontFamily": "Great Vibes", "color": "#000000", "fontSize": 48, }, }, { "id": "shape", "page": 0, "type": "shape", "rect": {"x": 180, "y": 170, "width": 60, "height": 30}, "props": {"kind": "ellipse", "strokeColor": "#0000ff", "strokeWidth": 2}, }, { "id": "future", "page": 0, "type": "future-stamp", "rect": {"x": 0, "y": 0, "width": 10, "height": 10}, "props": {}, }, ] exported = export_annotations(source, annotations, tmp_path / "assets") output = tmp_path / "exported.pdf" output.write_bytes(exported) with pymupdf.open(output) as document: page = document[0] assert page.rotation == 90 assert page.rect.width == 792 assert page.rect.height == 612 assert "Exported text" in page.get_text("text") assert "Ava" in page.get_text("text") signature_spans = [ span for block in page.get_text("dict")["blocks"] if "lines" in block for line in block["lines"] for span in line["spans"] if span["text"] == "Ava" ] assert signature_spans signature_span = signature_spans[0] assert signature_span["bbox"][2] - signature_span["bbox"][0] == pytest.approx(160) assert signature_span["bbox"][3] - signature_span["bbox"][1] == pytest.approx( (signature_span["ascender"] - signature_span["descender"]) * 48 ) # Normalize only for inspection: the exported PDF still retains the # original page rotation above. page.set_rotation(0) words = page.get_text("words") text_word = next(word for word in words if word[4] == "Exported") assert 35 <= text_word[0] <= 45 assert 45 <= text_word[1] <= 60 assert len(page.get_drawings()) >= 3 def test_export_renders_text_when_saved_box_is_short(tmp_path) -> None: source = tmp_path / "source.pdf" document = pymupdf.open() document.new_page(width=240, height=320) document.save(source) document.close() annotations = [ { "id": "short-text", "page": 0, "type": "text", "rect": {"x": 20, "y": 20, "width": 120, "height": 12}, "props": { "text": "Small text", "fontFamily": "Liberation Sans", "fontSize": 14, "color": "#000000", "align": "left", "lineHeight": 1.2, }, }, { "id": "styled-multiline", "page": 0, "type": "text", "rect": {"x": 20, "y": 60, "width": 120, "height": 18}, "props": { "text": "Styled\ntext", "fontFamily": "Liberation Sans", "fontSize": 14, "color": "#000000", "align": "left", "lineHeight": 1.2, "styles": { "0": {"0": {"fill": "#ff0000", "fontWeight": "bold"}}, "1": {"0": {"fill": "#0000ff", "fontStyle": "italic"}}, }, }, }, ] exported = export_annotations(source, annotations, tmp_path / "assets") with pymupdf.open(stream=exported, filetype="pdf") as document: text = document[0].get_text("text") assert "Small text" in text assert "Styled" in text assert "text" in text def test_export_uses_the_selected_text_font(tmp_path) -> None: source = tmp_path / "source.pdf" document = pymupdf.open() document.new_page(width=300, height=140) document.save(source) document.close() annotations = [ { "id": "outfit-text", "page": 0, "type": "text", "rect": {"x": 20, "y": 20, "width": 220, "height": 40}, "props": { "text": "Outfit text", "fontFamily": "Outfit", "fontSize": 24, "color": "#000000", }, } ] exported = export_annotations(source, annotations, tmp_path / "assets") with pymupdf.open(stream=exported, filetype="pdf") as document: spans = [ span for block in document[0].get_text("dict")["blocks"] if "lines" in block for line in block["lines"] for span in line["spans"] if span["text"] == "Outfit text" ] assert spans assert "outfit" in spans[0]["font"].lower() def test_export_uses_an_embeddable_serif_font_for_times_new_roman(tmp_path) -> None: source = tmp_path / "source.pdf" document = pymupdf.open() document.new_page(width=300, height=120) document.save(source) document.close() annotations = [ { "id": "times-text", "page": 0, "type": "text", "rect": {"x": 20, "y": 20, "width": 240, "height": 40}, "props": { "text": "Times New Roman", "fontFamily": "Times New Roman", "fontSize": 20, "color": "#000000", }, } ] exported = export_annotations(source, annotations, tmp_path / "assets") with pymupdf.open(stream=exported, filetype="pdf") as document: spans = [ span for block in document[0].get_text("dict")["blocks"] if "lines" in block for line in block["lines"] for span in line["spans"] if span["text"] == "Times New Roman" ] assert spans assert "liberationserif" in spans[0]["font"].lower() def test_export_highlight_tracks_styled_text_instead_of_the_saved_box(tmp_path) -> None: source = tmp_path / "source.pdf" document = pymupdf.open() document.new_page(width=320, height=140) document.save(source) document.close() annotation_rect = pymupdf.Rect(20, 20, 260, 50) annotations = [ { "id": "styled-text", "page": 0, "type": "text", "rect": { "x": annotation_rect.x0, "y": annotation_rect.y0, "width": annotation_rect.width, "height": annotation_rect.height, }, "props": { "text": "Highlighted text", "fontFamily": "Liberation Sans", "fontSize": 18, "color": "#000000", "highlightColor": "#ffff00", "styles": {"0": {"0": {"fill": "#ff0000"}}}, }, } ] exported = export_annotations(source, annotations, tmp_path / "assets") with pymupdf.open(stream=exported, filetype="pdf") as document: fill_drawings = [drawing for drawing in document[0].get_drawings() if drawing["fill"]] assert fill_drawings assert all(drawing["rect"] != annotation_rect for drawing in fill_drawings) assert all(drawing["rect"].width < annotation_rect.width for drawing in fill_drawings) def test_export_preserves_smooth_freehand_strokes(tmp_path) -> None: source = tmp_path / "source.pdf" document = pymupdf.open() document.new_page(width=300, height=180) document.save(source) document.close() annotations = [ { "id": "smooth-draw", "page": 0, "type": "draw", "rect": {"x": 20, "y": 20, "width": 180, "height": 100}, "props": { "svgPath": "M 0 0 Q 45 100 90 0 Q 135 -100 180 0", "strokeColor": "#000000", "strokeWidth": 2, "strokeWidthUnit": "pdf", "opacity": 1, }, } ] exported = export_annotations(source, annotations, tmp_path / "assets") with pymupdf.open(stream=exported, filetype="pdf") as document: drawings = document[0].get_drawings() items = [item for drawing in drawings for item in drawing["items"]] assert any(item[0] == "c" for item in items) assert all(all(cap == 1 for cap in drawing["lineCap"]) for drawing in drawings) assert all(drawing["lineJoin"] == 1 for drawing in drawings) def test_export_does_not_shrink_saved_typed_signature_size(tmp_path) -> None: source = tmp_path / "source.pdf" document = pymupdf.open() document.new_page(width=300, height=180) document.save(source) document.close() annotations = [ { "id": "typed-signature", "page": 0, "type": "signature", "rect": {"x": 20, "y": 20, "width": 160, "height": 60}, "props": { "mode": "type", "text": "John Doe", "fontFamily": "Great Vibes", "fontSize": 48, "color": "#000000", }, } ] exported = export_annotations(source, annotations, tmp_path / "assets") with pymupdf.open(stream=exported, filetype="pdf") as document: spans = [ span for block in document[0].get_text("dict")["blocks"] if "lines" in block for line in block["lines"] for span in line["spans"] if span["text"] == "John Doe" ] assert spans span = spans[0] assert span["bbox"][2] - span["bbox"][0] == pytest.approx(160, abs=0.01) assert span["bbox"][3] - span["bbox"][1] == pytest.approx( (span["ascender"] - span["descender"]) * 48 ) def test_export_does_not_double_apply_typed_signature_glyph_bearing(tmp_path) -> None: source = tmp_path / "source.pdf" document = pymupdf.open() document.new_page(width=320, height=160) document.save(source) document.close() annotations = [ { "id": "offset-signature", "page": 0, "type": "signature", "rect": {"x": 120, "y": 40, "width": 190, "height": 70}, "props": { "mode": "type", "text": "John Doe", "fontFamily": "Great Vibes", "fontSize": 48, # Stale records from the previous implementation may still # contain this value. The font already carries the bearing. "fontOffsetX": 4, "color": "#000000", }, } ] exported = export_annotations(source, annotations, tmp_path / "assets") with pymupdf.open(stream=exported, filetype="pdf") as document: spans = [ span for block in document[0].get_text("dict")["blocks"] if "lines" in block for line in block["lines"] for span in line["spans"] if span["text"] == "John Doe" ] assert spans assert spans[0]["bbox"][0] == pytest.approx(120) assert spans[0]["bbox"][2] - spans[0]["bbox"][0] == pytest.approx(190) pixmap = document[0].get_pixmap(alpha=False) dark_x = [ index % pixmap.width for index in range(pixmap.width * pixmap.height) if min(pixmap.samples[index * 3 : index * 3 + 3]) < 128 ] assert min(dark_x) == pytest.approx(117, abs=1) def test_outfit_italic_uses_browser_equivalent_synthetic_oblique() -> None: assert _needs_synthetic_italic("Outfit", False, "italic") assert _needs_synthetic_italic("Outfit", True, "italic") assert not _needs_synthetic_italic("Outfit", False, "normal") assert not _needs_synthetic_italic("Plus Jakarta Sans", False, "italic") def test_export_slants_outfit_italic_to_the_right(tmp_path) -> None: source = tmp_path / "source.pdf" document = pymupdf.open() document.new_page(width=360, height=140) document.save(source) document.close() annotations = [ { "page": 0, "type": "text", "rect": {"x": 20, "y": 20, "width": 120, "height": 80}, "props": { "text": "test", "fontFamily": "Outfit", "fontSize": 48, "color": "#0000ff", }, }, { "page": 0, "type": "text", "rect": {"x": 180, "y": 20, "width": 120, "height": 80}, "props": { "text": "test", "fontFamily": "Outfit", "fontSize": 48, "color": "#0000ff", "italic": True, }, }, ] exported = export_annotations(source, annotations, tmp_path / "assets") with pymupdf.open(stream=exported, filetype="pdf") as document: spans = [ span for block in document[0].get_text("dict")["blocks"] if "lines" in block for line in block["lines"] for span in line["spans"] if span["text"] == "test" ] assert len(spans) == 2 pixmap = document[0].get_pixmap(alpha=False) def blue_pixels(row: int, start: int, end: int) -> list[int]: return [ x for x in range(start, end) if pixmap.samples[(row * pixmap.width + x) * 3 + 2] > 120 and pixmap.samples[(row * pixmap.width + x) * 3] < 100 ] normal_top = min(blue_pixels(40, 0, 140)) - 20 italic_top = min(blue_pixels(40, 140, 340)) - 180 normal_bottom = min(blue_pixels(65, 0, 140)) - 20 italic_bottom = min(blue_pixels(65, 140, 340)) - 180 assert italic_top > normal_top + 5 assert italic_bottom == pytest.approx(normal_bottom, abs=1) def test_export_preserves_canonical_stroke_widths(tmp_path) -> None: source = tmp_path / "source.pdf" document = pymupdf.open() document.new_page(width=240, height=320) document.save(source) document.close() annotations = [ { "id": "draw", "page": 0, "type": "draw", "rect": {"x": 20, "y": 20, "width": 100, "height": 40}, "props": { "paths": [[0, 0], [100, 40]], "strokeColor": "#000000", "strokeWidth": 1.25, "strokeWidthUnit": "pdf", "opacity": 1, }, }, { "id": "shape", "page": 0, "type": "shape", "rect": {"x": 20, "y": 100, "width": 100, "height": 40}, "props": { "kind": "rect", "strokeColor": "#000000", "fillColor": "transparent", "strokeWidth": 2.5, "strokeWidthUnit": "pdf", }, }, ] exported = export_annotations(source, annotations, tmp_path / "assets") with pymupdf.open(stream=exported, filetype="pdf") as document: widths = [float(drawing["width"]) for drawing in document[0].get_drawings()] assert any(width == pytest.approx(1.25) for width in widths) assert any(width == pytest.approx(2.5) for width in widths) def test_export_does_not_mutate_annotation_input(tmp_path) -> None: source = tmp_path / "source.pdf" _source_pdf(source) annotations = [ { "id": "unknown", "page": 0, "type": "not-yet-supported", "rect": {"x": 1, "y": 2, "width": 3, "height": 4}, "props": {"future": True}, } ] before = json.loads(json.dumps(annotations)) export_annotations(source, annotations, tmp_path / "assets") assert annotations == before