initial commit

This commit is contained in:
2026-09-21 13:41:40 +09:00
commit 855c7328df
411 changed files with 85352 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
#!/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('<?xml version="1.0"?>\n<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">\n'
'<fontconfig>\n<dir>' + escape(str(fonts)) + '</dir>\n<cachedir>' + escape(str(cache))
+ '</cachedir>\n<alias><family>sans-serif</family><prefer><family>' + FAMILY
+ '</family></prefer></alias>\n</fontconfig>\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()