#!/usr/bin/env python3 """Create deterministic, original PDF fixtures with explicit drawing geometry. Dependencies: ReportLab, pypdf, Pillow. The Japanese font is subset-embedded; its adjacent SIL OFL license is copied alongside the generated documents. """ from pathlib import Path from io import BytesIO import hashlib import json import os import shutil from PIL import Image, ImageDraw from pypdf import PdfReader, PdfWriter from pypdf.generic import NameObject, NumberObject, RectangleObject from reportlab.lib import colors from reportlab.lib.utils import ImageReader from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas HERE = Path(__file__).resolve().parent OUT = HERE / "extended" FONT = Path(os.environ.get("DOCVIEW_JAPANESE_FONT", "/usr/local/share/fonts/BIZ_UDPGothic/BIZUDPGothic-Regular.ttf")) LICENSE = FONT.parent / "OFL.txt" OUT.mkdir(parents=True, exist_ok=True) if not FONT.is_file() or not LICENSE.is_file(): raise SystemExit("Set DOCVIEW_JAPANESE_FONT to BIZUDPGothic-Regular.ttf with its adjacent OFL.txt.") license_text = LICENSE.read_text(encoding="utf-8") if "SIL OPEN FONT LICENSE Version 1.1" not in license_text: raise SystemExit("The expected SIL Open Font License was not found.") pdfmetrics.registerFont(TTFont("FixtureJapanese", str(FONT))) shutil.copyfile(LICENSE, OUT / "FONT-OFL.txt") manifest = { "origin": "Original deterministic vector, text and raster drawings authored for DocView tests", "reference_status": "Synthetic source specifies content and coordinates; no human-approved golden image exists", "font": { "family": "BIZ UDPGothic Regular", "source_file": FONT.name, "sha256": hashlib.sha256(FONT.read_bytes()).hexdigest(), "license": "SIL Open Font License 1.1", "license_copy": "FONT-OFL.txt", "upstream": "https://github.com/googlefonts/morisawa-biz-ud-gothic", "upstream_license": "https://raw.githubusercontent.com/googlefonts/morisawa-biz-ud-gothic/main/OFL.txt", "embedding": "Subset embedded by ReportLab; OFL permits embedding and redistribution. No reserved font name is changed.", }, "documents": [], } def new_canvas(name, size): result = canvas.Canvas(str(OUT / name), pagesize=size, invariant=1, pageCompression=1) result.setTitle("DocView synthetic rendering fixture - " + name) result.setAuthor("DocView tests") return result def label(c, x, y, text, size=11): c.setFillColor(colors.HexColor("#263746")) c.setFont("Helvetica", size) c.drawString(x, y, text) def japanese(c, x, y, text, size=21): c.setFillColor(colors.HexColor("#132c35")) c.setFont("FixtureJapanese", size) c.drawString(x, y, text) def record(name, pages, expected, limits): manifest["documents"].append({"file": name, "sha256": hashlib.sha256((OUT / name).read_bytes()).hexdigest(), "page_count": pages, "expected": expected, "limits": limits}) c = new_canvas("japanese-layout.pdf", (540, 720)) label(c, 38, 675, "EMBEDDED JAPANESE / HORIZONTAL", 17) japanese(c, 38, 614, "日本語の文字配置と埋込みフォント", 23) japanese(c, 38, 557, "漢字・ひらがな・カタカナ・1234", 21) japanese(c, 38, 500, "読み取り専用の文書表示を検証します。", 20) label(c, 38, 454, "ASCII text uses the same embedded Japanese font:") japanese(c, 38, 420, "DocView 0123456789 ABC xyz", 20) japanese(c, 55, 330, "漢字", 29) japanese(c, 55, 364, "かんじ", 11) label(c, 155, 341, "Ruby has explicit, independent PDF coordinates.") c.setStrokeColor(colors.HexColor("#147d79")); c.setLineWidth(1) c.rect(38, 220, 464, 76, fill=0) japanese(c, 53, 262, "天地左右", 23) label(c, 53, 236, "Four ideographs above must remain distinct and correctly placed.") label(c, 38, 55, "540 x 720 pt / exact source coordinates / embedded TrueType subset") c.showPage() label(c, 38, 675, "EMBEDDED JAPANESE / EXPLICIT VERTICAL POSITIONS", 15) columns = [{"x": 420, "top": 590, "step": 32, "text": "日本語縦書き検証"}, {"x": 374, "top": 590, "step": 32, "text": "天地左右回転配置"}] for col in columns: for index, glyph in enumerate(col["text"]): japanese(c, col["x"], col["top"] - index * col["step"], glyph, 24) c.setStrokeColor(colors.HexColor("#d7e8e6")); c.line(col["x"] - 5, 315, col["x"] - 5, 620) for x, y, angle in [(80, 525, 0), (230, 485, 90), (200, 310, 180), (65, 350, 270)]: c.saveState(); c.translate(x, y); c.rotate(angle); japanese(c, 0, 0, "天地左右", 20); c.restoreState() label(c, 38, 228, "Right column is read top to bottom; the next column is to its left.") label(c, 38, 205, "Four lower-left samples use text matrices at 0 / 90 / 180 / 270 degrees.") label(c, 38, 73, "This tests explicit glyph placement, not vertical CID metrics or shaping.") label(c, 38, 55, "540 x 720 pt / each glyph has a documented baseline") c.save() record("japanese-layout.pdf", 2, {"page_sizes_pt": [[540, 720], [540, 720]], "horizontal_text": "日本語の文字配置と埋込みフォント", "vertical_columns": columns, "text_matrix_rotations_degrees": [0, 90, 180, 270], "font_embedded": True}, ["Manual vertical glyph placement does not test native vertical CID metrics or OpenType shaping.", "Ruby placement is explicit source geometry, not automatic ruby layout."]) c = new_canvas("graphics-compositing.pdf", (600, 760)) label(c, 35, 721, "IMAGES / ALPHA / CLIPPING / SHADING / CMYK", 17) image = Image.new("RGB", (96, 96), "white"); drawing = ImageDraw.Draw(image) for y in range(0, 96, 12): for x in range(0, 96, 12): drawing.rectangle((x, y, x + 11, y + 11), fill="#147d79" if (x + y) // 12 % 2 == 0 else "#e9b44c") drawing.line((0, 0, 95, 95), fill="black", width=4) c.drawImage(ImageReader(image), 40, 524, width=144, height=144) label(c, 40, 505, "96 x 96 RGB image, enlarged 1.5x") alpha = Image.new("RGBA", (96, 96), (0, 0, 0, 0)); d = ImageDraw.Draw(alpha) d.ellipse((4, 4, 92, 92), fill=(224, 63, 90, 150)); d.rectangle((15, 37, 81, 58), fill=(30, 70, 220, 190)) c.setFillColor(colors.HexColor("#dfe7ed")); c.rect(310, 524, 144, 144, stroke=0, fill=1) c.drawImage(ImageReader(alpha), 310, 524, width=144, height=144, mask="auto") label(c, 310, 505, "RGBA image with soft mask") c.setFillColor(colors.HexColor("#e8edf1")); c.rect(40, 330, 220, 135, fill=1, stroke=0) c.saveState(); c.setFillColor(colors.red); c.setFillAlpha(0.5); c.rect(55, 345, 95, 95, fill=1, stroke=0) c.setFillColor(colors.blue); c.setFillAlpha(0.5); c.rect(112, 365, 105, 75, fill=1, stroke=0); c.restoreState() label(c, 40, 308, "Two vector rectangles at 50% alpha") c.saveState(); path = c.beginPath(); path.circle(390, 395, 64); c.clipPath(path, stroke=0, fill=0) c.drawImage(ImageReader(image), 300, 310, width=180, height=180); c.restoreState() c.setStrokeColor(colors.HexColor("#263746")); c.circle(390, 395, 64, fill=0, stroke=1) label(c, 310, 308, "Raster image clipped to a circle") c.saveState(); path = c.beginPath(); path.rect(40, 205, 485, 62); c.clipPath(path, stroke=0, fill=0) c.linearGradient(40, 205, 525, 205, [colors.HexColor("#147d79"), colors.white, colors.HexColor("#d95050")], [0, 0.5, 1]); c.restoreState() label(c, 40, 185, "Axial PDF shading, clipped to a 485 x 62 pt band") swatches = [(1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1)] for index, value in enumerate(swatches): c.setFillColorCMYK(*value); c.rect(40 + index * 124, 76, 105, 67, stroke=0, fill=1) label(c, 40 + index * 124, 56, "CMYK " + "".join(map(str, value)), 10) c.save() record("graphics-compositing.pdf", 1, {"page_sizes_pt": [[600, 760]], "rgb_image_pixels": [96, 96], "alpha": 0.5, "alpha_probes": [ {"point_pt": [80, 370], "expected_rgb": [244, 119, 121], "region": "red over gray"}, {"point_pt": [180, 390], "expected_rgb": [116, 119, 248], "region": "blue over gray"}, {"point_pt": [130, 390], "expected_rgb": [122, 59, 188], "region": "blue over red over gray"}], "clip_circle_pt": [390, 395, 64], "axial_shading_band_pt": [40, 205, 485, 62], "cmyk_swatches": swatches}, ["CMYK output depends on renderer color conversion; no approved ICC reference is supplied.", "Pixel differences are evidence, not a calibrated quality threshold."]) geometries = [([420, 320], [30, 40, 390, 300], 0), ([560, 360], [20, 30, 520, 330], 90), ([300, 600], [15, 50, 285, 550], 270), ([480, 480], [60, 80, 420, 400], 180)] geometry_buffer = BytesIO(); c = canvas.Canvas(geometry_buffer, invariant=1, pageCompression=1) expectations = [] for number, (size, crop, rotation) in enumerate(geometries, 1): c.setPageSize(size) left, bottom, right, top = crop; width, height = right - left, top - bottom c.setFillColor(colors.HexColor("#ffd6d6")); c.rect(0, 0, *size, fill=1, stroke=0) c.setFillColor(colors.white); c.rect(left, bottom, width, height, fill=1, stroke=0) c.setStrokeColor(colors.black); c.setLineWidth(1); c.rect(left + 1, bottom + 1, width - 2, height - 2, fill=0) dots = [(left + 20, bottom + 20, [255, 0, 0]), (right - 20, bottom + 20, [0, 170, 0]), (right - 20, top - 20, [0, 0, 255]), (left + 20, top - 20, [255, 170, 0])] for x, y, rgb in dots: c.setFillColor(colors.Color(*(v / 255 for v in rgb))); c.circle(x, y, 8, fill=1, stroke=0) label(c, left + 35, bottom + height * 0.55, f"PAGE {number} / Rotate {rotation}", 15) label(c, left + 35, bottom + height * 0.55 - 25, f"CropBox {crop}", 10) label(c, left + 35, bottom + height * 0.55 - 45, "Pink margin must be excluded.", 10) expectations.append({"media_box_pt": [0, 0, *size], "crop_box_pt": crop, "rotation": rotation, "display_size_pt": [height, width] if rotation % 180 else [width, height], "markers_pdf_coordinates": [{"x": x, "y": y, "rgb": rgb} for x, y, rgb in dots]}) c.showPage() c.save(); source = PdfReader(BytesIO(geometry_buffer.getvalue())); writer = PdfWriter() for page, (_, crop, rotation) in zip(source.pages, geometries): page.cropbox = RectangleObject(crop); page[NameObject("/Rotate")] = NumberObject(rotation); writer.add_page(page) writer.add_metadata({"/Title": "DocView page geometry fixture", "/Author": "DocView tests"}) with (OUT / "page-geometry.pdf").open("wb") as output: writer.write(output) record("page-geometry.pdf", 4, {"pages": expectations}, ["Renderer comparisons must use CropBox and honor document Rotate."]) c = new_canvas("unembedded-font.pdf", (400, 300)) label(c, 30, 252, "UNEMBEDDED STANDARD PDF FONT", 15) label(c, 30, 205, "Helvetica: ABCDEFGHIJKLMNOPQRSTUVWXYZ", 12) label(c, 30, 176, "abcdefghijklmnopqrstuvwxyz 0123456789", 12) c.setFont("Times-Roman", 15); c.drawString(30, 137, "Times-Roman: replacement font baseline") c.setFont("Courier", 14); c.drawString(30, 100, "Courier: 0123456789 fixed pitch") label(c, 30, 46, "Standard-14 fonts are intentionally not embedded.", 11) c.save() record("unembedded-font.pdf", 1, {"page_sizes_pt": [[400, 300]], "base_fonts": ["Helvetica", "Times-Roman", "Courier"], "font_embedded": False}, ["This is a controlled Standard-14 substitution baseline, not arbitrary missing Japanese font coverage.", "Cross-OS fallback is not established by this Linux-only comparison."]) from icc_generator import generate as generate_icc icc_record, manifest["color_profile"] = generate_icc(OUT) manifest["documents"].append(icc_record) (OUT / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(f"Generated {len(manifest['documents'])} PDF fixtures under {OUT}")