initial commit
This commit is contained in:
+189
@@ -0,0 +1,189 @@
|
||||
#!/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}")
|
||||
Reference in New Issue
Block a user