#!/usr/bin/env python3 """Compare real production-worker TTC face 0/1 rendering with separate TTFs. Fonts/config/cache exist only inside TemporaryDirectory. No font is installed or retained in the evidence directory. Requires fontTools, Pillow, fontconfig, and the pdf-worker-evidence build target (which does not link PDFium itself). """ import argparse import hashlib import json import os from pathlib import Path import shutil import struct import subprocess import tempfile import time from xml.sax.saxutils import escape import fontTools from fontTools.ttLib import TTCollection, TTFont from PIL import Image, ImageChops, ImageStat ROOT = Path(__file__).resolve().parent.parent FAMILY = "DocView Collection Test" def digest(path): with path.open("rb") as source: return hashlib.file_digest(source, "sha256").hexdigest() def make_pdf(path): stream = (b"BT /Regular 24 Tf 30 150 Td (Collection face 0123456789 AVWA) Tj ET\n" b"BT /Bold 24 Tf 30 70 Td (Collection face 0123456789 AVWA) Tj ET\n") objects = [b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>", b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 430 240] /Resources << /Font << /Regular 4 0 R /Bold 5 0 R >> >> /Contents 6 0 R >>", b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>", b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>", b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"endstream"] output = bytearray(b"%PDF-1.7\n"); offsets = [0] for index, obj in enumerate(objects, 1): offsets.append(len(output)); output += f"{index} 0 obj\n".encode() + obj + b"\nendobj\n" xref = len(output); output += f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode() for offset in offsets[1:]: output += f"{offset:010d} 00000 n \n".encode() output += f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode() path.write_bytes(output) def rename_copy(source, target, style): # Format conversion is a modified font. Rename primary names so no # Reserved Font Name is used by our private derived test fonts. font = TTFont(source, recalcTimestamp=False) replacements = {1: FAMILY, 3: f"DocViewCollectionTest-{style}", 4: f"{FAMILY} {style}", 6: f"DocViewCollectionTest-{style}", 16: FAMILY, 18: f"{FAMILY} {style}", 21: FAMILY, 25: "DocViewCollectionTest"} for record in font["name"].names: if record.nameID in replacements: record.string = replacements[record.nameID].encode(record.getEncoding()) font.save(target); font.close() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--build-dir", type=Path, default=ROOT / os.environ.get("DOCVIEW_BUILD_DIR", "build")) parser.add_argument("--regular", type=Path, default=Path("/usr/share/fonts/liberation/LiberationSans-Regular.ttf")) parser.add_argument("--bold", type=Path, default=Path("/usr/share/fonts/liberation/LiberationSans-Bold.ttf")) parser.add_argument("--license", type=Path, default=Path("/usr/share/licenses/ttf-liberation/LICENSE")) parser.add_argument("--output", type=Path, default=ROOT / "tests/results/font-collection") args = parser.parse_args() executable = args.build_dir.resolve() / "pdf-worker-evidence" if not executable.is_file(): raise SystemExit("Build the pdf-worker-evidence target first.") license_text = args.license.read_text() assert "SIL OPEN FONT LICENSE Version 1.1" in license_text assert all(p.is_file() for p in (args.regular, args.bold)) assert shutil.disk_usage(tempfile.gettempdir()).free > 64 * 1024 * 1024 output = args.output.resolve(); output.mkdir(parents=True, exist_ok=True) pair_pdf = output / "regular-bold.pdf"; make_pdf(pair_pdf) cases = {"paired-styles": pair_pdf, "existing-standard14": ROOT / "tests/fixtures/pdf/extended/unembedded-font.pdf"} report = {"status": "incomplete", "fontTools_version": fontTools.__version__, "fontconfig_version": subprocess.run(["fc-match", "--version"], capture_output=True, text=True, check=True).stderr.strip(), "worker_sha256": digest(args.build_dir / "docview-pdf-worker"), "evidence_exporter_sha256": digest(executable), "font_sources": [{"path": str(p), "sha256": digest(p), "bytes": p.stat().st_size} for p in (args.regular, args.bold)], "license": {"path": str(args.license), "sha256": digest(args.license), "identifier": "OFL-1.1"}, "generated_family": FAMILY, "fonts_redistributed": False, "selection_transport": "production WorkerProcess + FontBroker + sandboxed PDF worker; no direct PDFium exporter", "runs": [], "comparisons": []} temporary = None with tempfile.TemporaryDirectory(prefix="docview-ttc-") as name: temporary = Path(name) standalone = temporary / "standalone"; collection_dir = temporary / "collection" standalone.mkdir(); collection_dir.mkdir() paths = [standalone / "regular.ttf", standalone / "bold.ttf"] for source, path, style in zip((args.regular, args.bold), paths, ("Regular", "Bold")): rename_copy(source, path, style) collection = TTCollection() collection.fonts = [TTFont(path, recalcTimestamp=False) for path in paths] ttc_path = collection_dir / "two-faces.ttc" collection.save(ttc_path, shareTables=False); collection.close() ttc_data = ttc_path.read_bytes() assert ttc_data[:4] == b"ttcf" and struct.unpack_from(">I", ttc_data, 8)[0] == 2 report["generated_fonts"] = { "standalone": [{"style": style, "bytes": p.stat().st_size, "sha256": digest(p)} for p, style in zip(paths, ("Regular", "Bold"))], "collection": {"bytes": len(ttc_data), "sha256": digest(ttc_path), "face_offsets": list(struct.unpack_from(">2I", ttc_data, 12)), "shared_tables": False}} for mode, fonts in (("standalone", standalone), ("collection", collection_dir)): cache = temporary / (mode + "-cache"); cache.mkdir() config = temporary / (mode + ".conf") config.write_text('\n\n' '\n' + escape(str(fonts)) + '\n' + escape(str(cache)) + '\nsans-serif' + FAMILY + '\n\n') env = os.environ.copy(); env["FONTCONFIG_FILE"] = str(config); env["FONTCONFIG_PATH"] = str(temporary) listing = subprocess.check_output(["fc-list", "--format=%{file}\t%{index}\t%{style}\n"], env=env, text=True) index = [] for line in listing.splitlines(): path, face, style = line.split("\t"); assert Path(path).parent == fonts index.append({"file": Path(path).name, "face_index": int(face), "style": style}) assert len(index) == 2 if mode == "collection": assert {item["face_index"] for item in index} == {0, 1} for case, source in cases.items(): destination = output / mode / case started = time.monotonic() process = subprocess.run([str(executable), str(source), str(destination)], env=env, text=True, capture_output=True, timeout=60) if process.returncode: raise RuntimeError(f"{mode}/{case}: exit {process.returncode}\n{process.stdout}\n{process.stderr}") metadata = json.loads((destination / "metadata.json").read_text()) selected = [s for s in metadata["fontSelections"] if s.get("found")] assert selected and all(s["actualFace"] == FAMILY for s in selected) assert metadata["fontBytesTransferred"] > 0 if mode == "collection" and case == "paired-styles": assert {s["faceIndex"] for s in selected} == {0, 1} assert all(s["faceIndex"] == (1 if s["requestedWeight"] >= 600 else 0) for s in selected) report["runs"].append({"mode": mode, "case": case, "source_sha256": digest(source), "elapsed_seconds": time.monotonic() - started, "font_index": index, "selections": selected, "font_bytes_transferred": metadata["fontBytesTransferred"]}) for case in cases: baseline = output / "standalone" / case / "page-1.png" collected = output / "collection" / case / "page-1.png" left = Image.open(baseline).convert("RGB"); right = Image.open(collected).convert("RGB") assert left.size == right.size difference = ImageChops.difference(left, right) histogram = ImageChops.lighter(ImageChops.lighter(difference.getchannel("R"), difference.getchannel("G")), difference.getchannel("B")).histogram() report["comparisons"].append({"case": case, "dimensions": list(left.size), "equal_pixels": histogram[0], "different_pixels": sum(histogram[1:]), "mean_absolute_channel_error": sum(ImageStat.Stat(difference).mean) / 3, "standalone_png_sha256": digest(baseline), "collection_png_sha256": digest(collected)}) assert difference.getbbox() is None, f"Raster mismatch in {case}" if case == "paired-styles": regular = right.crop((50, 120, 830, 200)).convert("L") bold = right.crop((50, 280, 830, 360)).convert("L") regular_ink = sum(regular.histogram()[:128]); bold_ink = sum(bold.histogram()[:128]) assert bold_ink > regular_ink > 0 and ImageChops.difference(regular, bold).getbbox() is not None report["style_probe"] = {"regular_dark_pixels": regular_ink, "bold_dark_pixels": bold_ink, "equal_text_at_equal_font_size": True, "regular_bold_distinct": True} report["temporary_fonts_removed"] = not temporary.exists() assert report["temporary_fonts_removed"] report["status"] = "pass" (output / "comparison.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n") print(json.dumps(report, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()