Skip to content

Instantly share code, notes, and snippets.

@moreaki
Last active April 22, 2026 20:56
Show Gist options
  • Select an option

  • Save moreaki/b7fc6b57074fd9b08804030e0b0bd792 to your computer and use it in GitHub Desktop.

Select an option

Save moreaki/b7fc6b57074fd9b08804030e0b0bd792 to your computer and use it in GitHub Desktop.
Reusable WeasyPrint SVG positioned text proof and timing fixture

WeasyPrint SVG Positioned Text Proof

This gist contains a reusable visual and timing fixture for SVG positioned text with bidirectional text, invisible controls, combining marks, ligatures, and complex-script clusters.

The files are intentionally standalone. They do not depend on a specific local checkout path.

Requirements

  • Python 3
  • WeasyPrint available as weasyprint, or passed with --weasyprint
  • ImageMagick available as magick, or passed with --magick
  • Pillow, only for the optional before/after image comparison in render-proof.py
  • Fonts with coverage for the scripts you want to inspect. The fixture uses fallback stacks including Arial, DejaVu Sans, Noto Arabic, Noto Devanagari, and Noto CJK families where available.

Render Visual Proof

Render the fixture with the currently installed WeasyPrint:

python3 render-proof.py after

Render before and after with two different checkouts or environments:

WEASYPRINT=/path/to/before/venv/bin/weasyprint python3 render-proof.py before --output-dir proof-output
WEASYPRINT=/path/to/after/venv/bin/weasyprint python3 render-proof.py after --output-dir proof-output

If you want to run WeasyPrint from a checkout without installing it, pass a command string and set PYTHONPATH:

PYTHONPATH=/path/to/WeasyPrint python3 render-proof.py after --weasyprint "python3 -m weasyprint"

Outputs:

  • positioned-bidi-clusters-before.pdf / .png
  • positioned-bidi-clusters-after.pdf / .png
  • positioned-bidi-clusters-diff.png when both PNGs exist
  • positioned-bidi-clusters-<label>-report.json

The diff file is useful for local inspection but is usually too noisy for a PR comment; before/after screenshots are easier for reviewers.

Run Timing Fixture

Generate a representative positioned-SVG timing document and render it multiple times:

python3 measure-positioned-bidi.py --label after --lines 176 --runs 5

Compare two environments:

WEASYPRINT=/path/to/before/venv/bin/weasyprint python3 measure-positioned-bidi.py --label before --output-dir timing-output
WEASYPRINT=/path/to/after/venv/bin/weasyprint python3 measure-positioned-bidi.py --label after --output-dir timing-output

Outputs:

  • positioned-bidi-timing.html
  • positioned-bidi-timing-<label>-<run>.pdf
  • positioned-bidi-timing-<label>.json

Treat these timings as a small representative sample, not a formal benchmark.

#!/usr/bin/env python3
"""Generate and time a representative positioned-SVG text workload."""
from __future__ import annotations
import argparse
import json
import os
import shlex
import shutil
import statistics
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parent
SOURCE = ROOT / 'positioned-bidi-timing.html'
DEFAULT_WEASYPRINT = os.environ.get('WEASYPRINT') or shutil.which('weasyprint')
@dataclass(frozen=True)
class Case:
name: str
text: str
font: str
size: int = 26
direction: str = 'ltr'
anchor: str = 'start'
step: int = 18
CASES = (
Case('Latin combining', 'Cafe\u0301 naive e\u0301lite', 'Arial, DejaVu Sans, sans-serif', 25),
Case('Latin ligatures', 'office affinity flag flight', 'Arial, DejaVu Sans, sans-serif', 25),
Case('Hebrew RTL', 'שלום עולם 12-34', 'Arial, DejaVu Sans, sans-serif', 28, 'rtl', 'start', 18),
Case('Arabic AL', 'سلام لا العربية 12-34', 'Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif', 30, 'rtl', 'end', 17),
Case('Arabic numbers', 'س١٢/٣٤م و ٥٦،٧٨', 'Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif', 30, 'rtl', 'end', 17),
Case('Persian ZWNJ', 'می\u200cخواهم کتاب\u200cها', 'Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif', 30, 'rtl', 'end', 17),
Case('Devanagari Hindi', 'किक्षि संस्कृत हिन्दी', 'Devanagari MT, Noto Sans Devanagari, Nirmala UI, sans-serif', 30, 'ltr', 'start', 19),
Case('Sanskrit phrase', 'संस्कृतम् नमस्ते', 'Devanagari MT, Noto Sans Devanagari, Nirmala UI, sans-serif', 30, 'ltr', 'start', 19),
Case('Korean Jamo', '한글 시험', 'Apple SD Gothic Neo, Noto Sans CJK KR, Malgun Gothic, sans-serif', 29, 'ltr', 'start', 18),
Case('CJK mixed', '中文日本語 한국어', 'Noto Sans CJK SC, Noto Sans CJK JP, Noto Sans CJK KR, Apple SD Gothic Neo, sans-serif', 29),
Case('Bidi isolate', 'A\u2067שלום\u2069B C\u202bعربى\u202cD', 'Arial, Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif', 26),
)
def escape(text):
return (
text.replace('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('"', '&quot;')
)
def command(value):
"""Return a subprocess command from a path or a shell-like string."""
return shlex.split(str(value))
def positions(case, x_origin):
values = [x_origin + index * case.step for index, _ in enumerate(case.text)]
if case.direction == 'rtl':
values.reverse()
return ' '.join(str(value) for value in values)
def write_source(source, lines):
rows = []
y = 56
for index in range(lines):
case = CASES[index % len(CASES)]
if index and index % 32 == 0:
y += 64
x_origin = 1040 if case.direction == 'rtl' else 260
rows.append(f'''
<text class="label" x="36" y="{y}">{escape(case.name)}</text>
<text x="{positions(case, x_origin)}" y="{y}"
direction="{case.direction}" text-anchor="{case.anchor}"
font-family="{escape(case.font)}" font-size="{case.size}"
fill="#1d4ed8">{escape(case.text)}</text>
''')
y += 40
height = y + 40
source.write_text(f'''<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>SVG positioned bidi timing fixture</title>
<style>
@page {{ size: 1200px {height}px; margin: 0 }}
html, body {{ margin: 0; background: white }}
svg {{ display: block; width: 1200px; height: {height}px }}
.label {{
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: 700;
}}
</style>
<svg viewBox="0 0 1200 {height}" xmlns="http://www.w3.org/2000/svg">
<style>
.label {{
font-family: Arial, sans-serif;
font-size: 14px;
font-weight: 700;
fill: #29354a;
}}
</style>
{''.join(rows)}
</svg>
</html>
''', encoding='utf-8')
def render_once(weasyprint, source, output):
start = time.perf_counter()
subprocess.run((*command(weasyprint), str(source), str(output)), check=True)
return time.perf_counter() - start
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--label', default='before')
parser.add_argument('--lines', type=int, default=176)
parser.add_argument('--runs', type=int, default=5)
parser.add_argument(
'--source', type=Path, default=SOURCE,
help='Generated HTML timing fixture path.')
parser.add_argument(
'--output-dir', type=Path, default=ROOT,
help='Directory for generated PDFs and JSON report.')
parser.add_argument(
'--weasyprint', default=DEFAULT_WEASYPRINT,
help='WeasyPrint executable. Defaults to $WEASYPRINT or weasyprint on PATH.')
args = parser.parse_args()
if args.runs < 1:
raise SystemExit('--runs must be at least 1')
if not args.weasyprint:
raise SystemExit(
'Could not find WeasyPrint. Install it, put it on PATH, set '
'WEASYPRINT=/path/to/weasyprint, or pass --weasyprint.')
args.source = args.source.resolve()
args.output_dir = args.output_dir.resolve()
args.output_dir.mkdir(parents=True, exist_ok=True)
write_source(args.source, args.lines)
timings = []
for index in range(args.runs):
output = args.output_dir / f'positioned-bidi-timing-{args.label}-{index + 1}.pdf'
timings.append(render_once(args.weasyprint, args.source, output))
report = {
'label': args.label,
'source': str(args.source),
'lines': args.lines,
'runs': args.runs,
'timings_seconds': timings,
'mean_seconds': statistics.mean(timings),
'median_seconds': statistics.median(timings),
'min_seconds': min(timings),
'max_seconds': max(timings),
}
report_path = args.output_dir / f'positioned-bidi-timing-{args.label}.json'
report_path.write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
if __name__ == '__main__':
main()
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>SVG positioned bidi and cluster proof</title>
<style>
@page {
size: 1200px 1600px;
margin: 0;
}
html,
body {
margin: 0;
background: white;
color: #172033;
font-family: Arial, DejaVu Sans, sans-serif;
}
svg {
display: block;
width: 1200px;
height: 1600px;
}
</style>
<svg viewBox="0 0 1200 1600" xmlns="http://www.w3.org/2000/svg">
<style>
.title {
font-family: Arial, DejaVu Sans, sans-serif;
font-size: 30px;
font-weight: 700;
fill: #172033;
}
.subtitle,
.column,
.label,
.note {
font-family: Arial, DejaVu Sans, sans-serif;
font-size: 18px;
fill: #42506a;
}
.column {
font-weight: 700;
fill: #172033;
}
.label {
font-weight: 700;
fill: #29354a;
}
.note {
font-size: 14px;
fill: #68758d;
}
.guide {
stroke: #d7dce7;
stroke-width: 1;
}
.separator {
stroke: #eef1f6;
stroke-width: 1;
}
.reference {
fill: #166534;
}
.positioned {
fill: #1d4ed8;
}
</style>
<text class="title" x="48" y="54">SVG positioned text: bidi, controls, and shaped clusters</text>
<text class="subtitle" x="48" y="84">Before/after source fixture. In the after render, each blue row should match the green reference shape and spacing.</text>
<text class="column" x="370" y="126">Reference / intended rendering</text>
<text class="column" x="770" y="126">Positioned SVG input under test</text>
<line class="guide" x1="350" y1="142" x2="350" y2="1510"/>
<line class="guide" x1="750" y1="142" x2="750" y2="1510"/>
<g transform="translate(48 180)">
<text class="label" x="0" y="0">Combining mark: decomposed e + acute stays one cluster</text>
<line class="separator" x1="0" y1="20" x2="1100" y2="20"/>
<text class="reference" x="350 390" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="42">éa</text>
<text class="positioned" x="750 1040 790" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="42">éa</text>
<text class="note" x="350" y="92">precomposed U+00E9 + a</text>
<text class="note" x="750" y="92">decomposed e U+0301 + a with x list</text>
</g>
<g transform="translate(48 310)">
<text class="label" x="0" y="0">Latin ligature: extra coordinates must not split one shaped glyph</text>
<line class="separator" x1="0" y1="20" x2="1100" y2="20"/>
<text class="reference" x="350" y="58" font-family="Helvetica Neue, DejaVu Sans, sans-serif" font-size="42">o</text>
<text class="reference" x="385" y="58" font-family="Helvetica Neue, DejaVu Sans, sans-serif" font-size="42">ffi</text>
<text class="reference" x="435" y="58" font-family="Helvetica Neue, DejaVu Sans, sans-serif" font-size="42">c</text>
<text class="reference" x="465" y="58" font-family="Helvetica Neue, DejaVu Sans, sans-serif" font-size="42">e</text>
<text class="positioned" x="750 785 1040 1080 835 865" y="58"
font-family="Helvetica Neue, DejaVu Sans, sans-serif" font-size="42">office</text>
<text class="note" x="350" y="92">whole run shaped with ffi ligature</text>
<text class="note" x="750" y="92">extra x values inside ffi must be ignored</text>
</g>
<g transform="translate(48 440)">
<text class="label" x="0" y="0">Hebrew RTL: coordinate list follows visual order after bidi reordering</text>
<line class="separator" x1="0" y1="20" x2="1100" y2="20"/>
<text class="reference" x="350" y="58" direction="rtl"
text-anchor="end" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ם</text>
<text class="reference" x="382" y="58" direction="rtl"
text-anchor="end" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ו</text>
<text class="reference" x="414" y="58" direction="rtl"
text-anchor="end" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ל</text>
<text class="reference" x="446" y="58" direction="rtl"
text-anchor="end" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ש</text>
<text class="positioned" x="846 814 782 750" y="58" direction="rtl"
text-anchor="start" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">שלום</text>
<text class="note" x="350" y="92">x values already in visual order</text>
<text class="note" x="750" y="92">logical-order x list must be reordered</text>
</g>
<g transform="translate(48 570)">
<text class="label" x="0" y="0">Arabic AL shaping: lam-alef is one shaped cluster</text>
<line class="separator" x1="0" y1="20" x2="1100" y2="20"/>
<text class="reference" x="430" y="62" direction="rtl" text-anchor="end"
font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="48">لا</text>
<text class="positioned" x="830 880" y="62" direction="rtl" text-anchor="end"
font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="48">لا</text>
<text class="note" x="350" y="96">one lam-alef cluster</text>
<text class="note" x="750" y="96">second x value must be ignored for the cluster</text>
</g>
<g transform="translate(48 705)">
<text class="label" x="0" y="0">RTL weak types: European and Arabic-Indic numbers with separators</text>
<line class="separator" x1="0" y1="20" x2="1100" y2="20"/>
<text class="reference" x="350" y="54" direction="rtl" text-anchor="end"
font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">م</text>
<text class="reference" x="378 400" y="54" font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">34</text>
<text class="reference" x="428" y="54" font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">-</text>
<text class="reference" x="452 474" y="54" font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">12</text>
<text class="reference" x="512" y="54" direction="rtl" text-anchor="end"
font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">س</text>
<text class="positioned" x="912 852 874 828 778 800 750" y="54"
direction="rtl" text-anchor="end" font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif"
font-size="30">س12-34م</text>
<text class="reference" x="350" y="96" direction="rtl" text-anchor="end"
font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">م</text>
<text class="reference" x="378 400" y="96" font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">١٢</text>
<text class="reference" x="428" y="96" font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">/</text>
<text class="reference" x="452 474" y="96" font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">٣٤</text>
<text class="reference" x="512" y="96" direction="rtl" text-anchor="end"
font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif" font-size="30">س</text>
<text class="positioned" x="912 778 800 828 852 874 750" y="96"
direction="rtl" text-anchor="end" font-family="Geeza Pro, Noto Naskh Arabic, DejaVu Sans, sans-serif"
font-size="30">س١٢/٣٤م</text>
<text class="note" x="350" y="128">EN/ES and AN/CS representative cases</text>
</g>
<g transform="translate(48 885)">
<text class="label" x="0" y="0">RTL neutral punctuation: brackets and spaces reorder with their text</text>
<line class="separator" x1="0" y1="20" x2="1100" y2="20"/>
<text class="reference" x="350" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">)</text>
<text class="reference" x="378 402 426" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">abc</text>
<text class="reference" x="470" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">(</text>
<text class="reference" x="520" y="58" direction="rtl" text-anchor="end"
font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ג</text>
<text class="reference" x="560" y="58" direction="rtl" text-anchor="end"
font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ב</text>
<text class="reference" x="600" y="58" direction="rtl" text-anchor="end"
font-family="Arial, DejaVu Sans, sans-serif" font-size="34">א</text>
<text class="positioned" x="1000 960 920 890 850 778 802 826 750" y="58"
direction="rtl" text-anchor="end" font-family="Arial, DejaVu Sans, sans-serif"
font-size="34">אבג (abc)</text>
</g>
<g transform="translate(48 1015)">
<text class="label" x="0" y="0">Boundary neutrals and explicit controls should not consume visible placement</text>
<line class="separator" x1="0" y1="20" x2="1100" y2="20"/>
<text class="reference" x="350" y="54" font-family="Arial, DejaVu Sans, sans-serif" font-size="38">AB</text>
<text class="positioned" x="750 1040" y="54" font-family="Arial, DejaVu Sans, sans-serif" font-size="38">A&#x200D;B</text>
<text class="note" x="350" y="84">ZWJ ignored visually</text>
<text class="note" x="750" y="84">bad x for ZWJ must not move B</text>
<text class="reference" x="350 378 406 434" y="126" font-family="Arial, DejaVu Sans, sans-serif" font-size="38">ABCD</text>
<text class="positioned" x="750 1040 778 806 1080 834" y="126"
font-family="Arial, DejaVu Sans, sans-serif" font-size="38">A&#x202A;BC&#x202C;D</text>
<text class="note" x="350" y="156">LRE/PDF controls</text>
<text class="note" x="750" y="156">controls must be invisible and zero-placement</text>
</g>
<g transform="translate(48 1230)">
<text class="label" x="0" y="0">Bidi isolate controls: visual order plus invisible isolate boundaries</text>
<line class="separator" x1="0" y1="20" x2="1100" y2="20"/>
<text class="reference" x="350" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">A</text>
<text class="reference" x="378" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ש</text>
<text class="reference" x="418" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ל</text>
<text class="reference" x="458" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ו</text>
<text class="reference" x="498" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">ם</text>
<text class="reference" x="538" y="58" font-family="Arial, DejaVu Sans, sans-serif" font-size="34">B</text>
<text class="positioned" x="750 1040 778 818 858 898 1080 938" y="58"
font-family="Arial, DejaVu Sans, sans-serif" font-size="34">A&#x2067;שלום&#x2069;B</text>
<text class="note" x="350" y="92">RLI/PDI keep Hebrew isolated</text>
<text class="note" x="750" y="92">bad x values on controls should be ignored</text>
</g>
<g transform="translate(48 1360)">
<text class="label" x="0" y="0">Complex-script clusters and CJK guard cases</text>
<line class="separator" x1="0" y1="20" x2="1100" y2="20"/>
<text class="reference" x="350 384" y="54" font-family="Apple SD Gothic Neo, Noto Sans CJK KR, Malgun Gothic, sans-serif" font-size="36">한글</text>
<text class="positioned" x="750 1040 1040 784 1040 1040" y="54"
font-family="Apple SD Gothic Neo, Noto Sans CJK KR, Malgun Gothic, sans-serif" font-size="36">한글</text>
<text class="reference" x="350 390 430" y="108" font-family="Devanagari MT, Noto Sans Devanagari, Nirmala UI, sans-serif" font-size="38">संस्कृत</text>
<text class="positioned" x="750 1040 790 1040 1040 1040 830" y="108"
font-family="Devanagari MT, Noto Sans Devanagari, Nirmala UI, sans-serif" font-size="38">संस्कृत</text>
<text class="reference" x="350 386 422 458 494" y="160"
font-family="Noto Sans CJK SC, Noto Sans CJK JP, Noto Sans CJK KR, Songti SC, Hiragino Sans, Apple SD Gothic Neo, sans-serif"
font-size="34">中文日本어</text>
<text class="positioned" x="750 786 822 858 894" y="160"
font-family="Noto Sans CJK SC, Noto Sans CJK JP, Noto Sans CJK KR, Songti SC, Hiragino Sans, Apple SD Gothic Neo, sans-serif"
font-size="34">中文日本어</text>
<text class="note" x="350" y="192">CJK should remain ordinary per-character positioning</text>
</g>
</svg>
</html>
#!/usr/bin/env python3
"""Render and compare the SVG positioned-bidi visual proof."""
from __future__ import annotations
import argparse
import json
import os
import shlex
import shutil
import subprocess
from pathlib import Path
from PIL import Image, ImageChops
ROOT = Path(__file__).resolve().parent
SOURCE = ROOT / 'positioned-bidi-clusters.html'
DEFAULT_WEASYPRINT = os.environ.get('WEASYPRINT') or shutil.which('weasyprint')
DEFAULT_MAGICK = os.environ.get('MAGICK') or shutil.which('magick')
def run(command):
subprocess.run(command, check=True)
def command(value):
"""Return a subprocess command from a path or a shell-like string."""
return shlex.split(str(value))
def render(args):
args.output_dir.mkdir(parents=True, exist_ok=True)
pdf = args.output_dir / f'positioned-bidi-clusters-{args.label}.pdf'
png = args.output_dir / f'positioned-bidi-clusters-{args.label}.png'
if not args.weasyprint:
raise SystemExit(
'Could not find WeasyPrint. Install it, put it on PATH, set '
'WEASYPRINT=/path/to/weasyprint, or pass --weasyprint.')
if not args.magick:
raise SystemExit(
'Could not find ImageMagick. Install magick, set '
'MAGICK=/path/to/magick, or pass --magick.')
run((*command(args.weasyprint), str(args.source), str(pdf)))
run((
*command(args.magick), '-density', str(args.density), f'{pdf}[0]',
'-background', 'white', '-alpha', 'remove',
'-resize', args.size, str(png),
))
return pdf, png
def compare(before, after, output_dir):
before_image = Image.open(before).convert('RGB')
after_image = Image.open(after).convert('RGB')
if before_image.size != after_image.size:
raise SystemExit(
f'Image sizes differ: {before_image.size} != {after_image.size}')
diff = ImageChops.difference(before_image, after_image)
diff_pixels = (
diff.get_flattened_data()
if hasattr(diff, 'get_flattened_data') else diff.getdata())
changed_pixels = sum(1 for pixel in diff_pixels if pixel != (0, 0, 0))
total_pixels = before_image.width * before_image.height
bbox = diff.getbbox()
diff_png = output_dir / 'positioned-bidi-clusters-diff.png'
# Amplify differences so small glyph changes are visible in the attachment.
diff.point(lambda value: min(value * 6, 255)).save(diff_png)
return {
'before': str(before),
'after': str(after),
'diff': str(diff_png),
'width': before_image.width,
'height': before_image.height,
'changed_pixels': changed_pixels,
'total_pixels': total_pixels,
'changed_percent': changed_pixels / total_pixels * 100,
'changed_bbox': bbox,
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'label', choices=('before', 'after'),
help='Output label for the rendered proof.')
parser.add_argument(
'--source', type=Path, default=SOURCE,
help='HTML proof source. Defaults to positioned-bidi-clusters.html next to this script.')
parser.add_argument(
'--output-dir', type=Path, default=ROOT,
help='Directory for generated PDF, PNG, and JSON report files.')
parser.add_argument(
'--weasyprint', default=DEFAULT_WEASYPRINT,
help='WeasyPrint executable. Defaults to $WEASYPRINT or weasyprint on PATH.')
parser.add_argument(
'--magick', default=DEFAULT_MAGICK,
help='ImageMagick executable. Defaults to $MAGICK or magick on PATH.')
parser.add_argument(
'--density', type=int, default=144,
help='ImageMagick input density used to rasterize the PDF.')
parser.add_argument(
'--size', default='1200x1600',
help='Final PNG size passed to ImageMagick -resize.')
args = parser.parse_args()
args.source = args.source.resolve()
args.output_dir = args.output_dir.resolve()
pdf, png = render(args)
report = {'rendered_pdf': str(pdf), 'rendered_png': str(png)}
before = args.output_dir / 'positioned-bidi-clusters-before.png'
after = args.output_dir / 'positioned-bidi-clusters-after.png'
if before.exists() and after.exists():
report['comparison'] = compare(before, after, args.output_dir)
report_path = args.output_dir / f'positioned-bidi-clusters-{args.label}-report.json'
report_path.write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment