diff --git a/.gitattributes b/.gitattributes index d5eb476..910fe44 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,3 +10,6 @@ examples/aspnet/Assets/*.html text eol=lf examples/aspnet/Assets/*.css text eol=lf docs/assets/playground/LICENSES.txt text eol=lf whitespace=-blank-at-eol docs/assets/pdf-regression/*.json text eol=lf +# Evidence downloads and their manifests are verified by byte hashes. +docs/assets/renderer-comparison/** -text +docs/assets/renderer-comparison/*.svg -whitespace diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b7685d7..f351e0f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -165,6 +165,16 @@ jobs: path: | docs/assets/playground/build.json docs/assets/playground/verification.json + - name: Verify renderer comparison downloads and published measurements + run: | + python -m pip install pypdf==6.19.0 pypdfium2==5.14.0 Pillow==12.3.0 + python tools/verify_renderer_comparison.py + - uses: actions/upload-artifact@v4 + if: always() + with: + name: renderer-comparison-verification + path: output/renderer-comparison-verification/verification.json + retention-days: 14 - run: python -m mkdocs build --strict - run: python tools/check_site.py site - name: Include search notification ownership file diff --git a/docs/assets/renderer-comparison/environment.json b/docs/assets/renderer-comparison/environment.json new file mode 100644 index 0000000..c8697cf --- /dev/null +++ b/docs/assets/renderer-comparison/environment.json @@ -0,0 +1,62 @@ +{ + "utc": "2026-10-05T00:22:50.404685+00:00", + "python": "3.10.12 (main, Aug 31 2026, 10:18:17) [GCC 11.4.0]", + "platform": "Linux-6.18.40.1-microsoft-standard-WSL2-x86_64-with-glibc2.35", + "machine": "x86_64", + "cpu": "12th Gen Intel(R) Core(TM) i7-12700H", + "logical_cpus": 20, + "total_memory_bytes": 33521459200, + "source_revision": "4ed41e3c48f90144b21e6360464dd1d868a00085", + "source_sha256": { + "prepare_font.py": "89d69112614e298a18237b0dd6502f383057e33d7549f0d0020a3323f8186946", + "README.md": "c1c13f5e82a7c4a28f3fb7e0fa55d59b54ff0349d76faaad9dc49e7d17a62429", + "validate.py": "15b6dafe50987b640fe38d4dbb5a708ae243bc768c6d30bc3bc14dea31bca5db", + "fixtures.py": "b4d5331218337e5d7f97be299eab4b83e60aefeed64039db1af95cb1d15e8a16", + "compare.py": "75800f08731d2cc2a1848046ad807cd7c02a5dc32b460668d5825bdc4d92b601", + "requirements.txt": "1a2da83abba0d1efa0a9707e5764b93446c90d1d5acbf2bfcd54a8c9e509d314", + "worker.py": "c5b8ea567388a20443831be9a6fcaab8c90df1de4b451005cb22567de662d3f3", + "fonts/Inter-OFL.txt": "5b9321a4298cfeb6b34354164a1c3afc3db114569984c502b9b35d988fd58c57", + "fonts/Inter-Variable.ttf": "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031", + "fonts/sources.json": "43c15ac0a295edc5fbd6de17356439d8f4697529fe9761cbb5bce190079b18b4", + "fonts/Inter-Regular.ttf": "0b59f6b6fc9e8a9ad8032802d1797c4237d9288a3e7b7661517070627b577060" + }, + "packages": { + "pypdfium2": "5.14.0", + "zopfli": "0.4.3", + "pydyf": "0.12.1", + "cssselect2": "0.10.1", + "pyphen": "0.18.1", + "cffi": "2.1.1", + "tinyhtml5": "2.1.0", + "fullbleed": "2.5.6", + "tinycss2": "1.5.1", + "pillow": "12.3.0", + "greenlet": "3.5.6", + "fonttools": "4.65.0", + "pyee": "13.0.1", + "webencodings": "0.6.1", + "pycparser": "3.0", + "weasyprint": "70.0", + "typing_extensions": "4.16.0", + "pypdf": "6.19.0", + "psutil": "7.2.2", + "playwright": "1.63.0", + "brotli": "1.2.0" + }, + "path_policy": "venv bin plus standard Linux executable directories; no inherited Windows PATH", + "font_sha256": "0b59f6b6fc9e8a9ad8032802d1797c4237d9288a3e7b7661517070627b577060", + "input_sha256": { + "shared.css": "325e59f83f4ea3f5109ed51ecf88651ae1b21fa67c7e7460043b3cbfbab275ce", + "report.json": "e90d33ed9815c061a8fb069a0a5044eb352c4b841b291af589b07adde939c25b", + "ledger.html": "a4aa14353048479ba56bd898fbb33677628898f9a87a24f1eef8606cda3c6356", + "ledger.json": "6967572ec40b7c1417eaeddd98e411e369c72e062f3b03f1f4730672167ee414", + "report.html": "27fd9b769cdfcdfb4494249dcecd4149fef671c3f795e552d3a991e85960fa07", + "invoice.json": "3ea566a311ea5d2ac012cc6b07712d6ed8d1a0ea9f519c1e1f65f2d6d65ca2fc", + "invoice.html": "d5c0acc02c8a70bd70114929bacd0fe7831db51cff01bd720819f0d58404b988" + }, + "thread_environment": { + "FULLBLEED_THREADS": null, + "OMP_NUM_THREADS": null, + "RAYON_NUM_THREADS": null + } +} diff --git a/docs/assets/renderer-comparison/latency.png b/docs/assets/renderer-comparison/latency.png new file mode 100644 index 0000000..646d850 Binary files /dev/null and b/docs/assets/renderer-comparison/latency.png differ diff --git a/docs/assets/renderer-comparison/latency.svg b/docs/assets/renderer-comparison/latency.svg new file mode 100644 index 0000000..0330bf0 --- /dev/null +++ b/docs/assets/renderer-comparison/latency.svg @@ -0,0 +1,2675 @@ + + + + + + + + image/svg+xml + + + Matplotlib v3.8.4, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/renderer-comparison/manifest.json b/docs/assets/renderer-comparison/manifest.json new file mode 100644 index 0000000..4092ccc --- /dev/null +++ b/docs/assets/renderer-comparison/manifest.json @@ -0,0 +1,231 @@ +{ + "schema": "fullbleed.renderer_comparison.assets.v1", + "sourceCommit": "4ed41e3c48f90144b21e6360464dd1d868a00085", + "measuredAt": "2026-10-05T00:22:50.404685+00:00", + "packagedAt": "2026-10-05T00:27:34.853528+00:00", + "timedPdfs": 315, + "qualifiedPdfs": 414, + "representativePdfs": 9, + "representativePages": 22, + "archives": [ + { + "file": "measured-evidence.zip", + "bytes": 13556690, + "sha256": "f16aae3d7f07eb21f705bff9d2c8bf4f402809e8408e240fcfaad8da0285be68", + "members": 732 + }, + { + "file": "setup-diagnostics.zip", + "bytes": 12316176, + "sha256": "4c8d50c608a12d428b06f9e46109f2db802c193b1ef81da9bac9d647826201e1", + "members": 489 + } + ], + "files": [ + { + "path": "environment.json", + "bytes": 2906, + "sha256": "d000ee5deef32f6170e7d1285b1ae4c3e3e9b7476a85b767ce6172eacd930cba" + }, + { + "path": "measured-evidence.zip", + "bytes": 13556690, + "sha256": "f16aae3d7f07eb21f705bff9d2c8bf4f402809e8408e240fcfaad8da0285be68" + }, + { + "path": "negative-controls.json", + "bytes": 16782, + "sha256": "31334ca17e011a272a2eb5e2076e4c01c5887ad246a6be7b0115211a79d6615b" + }, + { + "path": "packages.txt", + "bytes": 343, + "sha256": "911e56fa0a691e858200ab22129cc36de17569cca35d611910b05a232ba317b4" + }, + { + "path": "pdfs/chromium/invoice.pdf", + "bytes": 9752, + "sha256": "a87c72c26c3653703f975cd9b16548670b511cc3308f64a98b0e36e254d7c55f" + }, + { + "path": "pdfs/chromium/ledger.pdf", + "bytes": 13753, + "sha256": "8965e4f74c80753ba8fe0ed27e9461b1275989105a8728f03afe930f5c74f81f" + }, + { + "path": "pdfs/chromium/report.pdf", + "bytes": 14241, + "sha256": "d3c819b3692a0446a4cfdaf192f02b0d91963bbb457e0621692465a046db9221" + }, + { + "path": "pdfs/fullbleed/invoice.pdf", + "bytes": 52552, + "sha256": "567ad58b2a1a9019388fe133c1489f521ece8c29725677dfcfa96d1b274d82d2" + }, + { + "path": "pdfs/fullbleed/ledger.pdf", + "bytes": 57559, + "sha256": "99c4a2fe8b8a33ff160ffcb0a1566770f7619896026ae8ea7eca2fcd12125a19" + }, + { + "path": "pdfs/fullbleed/report.pdf", + "bytes": 58115, + "sha256": "acc7e43a7efaea695e3da4877f907e500186d36d6b2ad30dbbf83b48bd42b238" + }, + { + "path": "pdfs/weasyprint/invoice.pdf", + "bytes": 8870, + "sha256": "2b0aebf6ff37c3687db7f0123c6f2adf13059633dba8de84a2536213faf6468d" + }, + { + "path": "pdfs/weasyprint/ledger.pdf", + "bytes": 13461, + "sha256": "6c2bd86664ce5b47f79575056edff4cdc5453458d962764d91731fc0739a3789" + }, + { + "path": "pdfs/weasyprint/report.pdf", + "bytes": 13162, + "sha256": "0b2c2c5b8894f74896021136f329db7b413fbd4bf86cbec8a9ca8320103de6cf" + }, + { + "path": "plan.json", + "bytes": 343, + "sha256": "a7176b29c681c094afd18a26f2ee254e88721b8834c02ac2d2e66e3264e411a7" + }, + { + "path": "previews/chromium/invoice/page-1.png", + "bytes": 63352, + "sha256": "1fd8ac6dd503b78ca76f69444996487c66e194c603e1b883ca7b37c8cc8298e5" + }, + { + "path": "previews/chromium/ledger/page-1.png", + "bytes": 122242, + "sha256": "21297448050f8bcef034230a50e06ad1914171d8bd175b0a63e7e3c79fdf2b74" + }, + { + "path": "previews/chromium/ledger/page-2.png", + "bytes": 127528, + "sha256": "084f65f1f2d008d2e38dc443c851545b7c4009b7113b9aa46cff66e2337e9aaf" + }, + { + "path": "previews/chromium/ledger/page-3.png", + "bytes": 129428, + "sha256": "3e26b7e0d0e6f9756e6d2e3874508031672d8d3503725f96e6ce6e6675741ec6" + }, + { + "path": "previews/chromium/ledger/page-4.png", + "bytes": 7963, + "sha256": "838041fbd570a25dfe3d4f0c850f55b693e3018774c954e9faf6272ccdabf28a" + }, + { + "path": "previews/chromium/report/page-1.png", + "bytes": 73351, + "sha256": "8736f33b88466343e09b0d6cc708a38288eb40ce0e8fe3dea4d3e54c09884a6e" + }, + { + "path": "previews/chromium/report/page-2.png", + "bytes": 73280, + "sha256": "2c4e2c77ba2a169079f0bbee9f6696f9a8670f527d070f7261f578cacd20f01a" + }, + { + "path": "previews/chromium/report/page-3.png", + "bytes": 71996, + "sha256": "62a85d1d23df7a0ac661bfc6244806981cd71f0c954847db03d30c343a874c0a" + }, + { + "path": "previews/fullbleed/invoice/page-1.png", + "bytes": 65577, + "sha256": "d5d14e3719d5610b1f98a01cf91caaf1a25cfea1deeb43a1b1a7e0900019cc91" + }, + { + "path": "previews/fullbleed/ledger/page-1.png", + "bytes": 127243, + "sha256": "7521aa5f81bb7210adfe053b1e88aecf2203cbd18b21fb62c478917585f8021e" + }, + { + "path": "previews/fullbleed/ledger/page-2.png", + "bytes": 140906, + "sha256": "4f1e2e5dc47253641a9400ed9925312650cfa108c3a9e7e88b0871704752dd96" + }, + { + "path": "previews/fullbleed/ledger/page-3.png", + "bytes": 136368, + "sha256": "fd6f0a5ca6f83169f8411c024465a4ecda76dc18873394d0651aee96bbdfbb9a" + }, + { + "path": "previews/fullbleed/report/page-1.png", + "bytes": 75262, + "sha256": "d4513c02d91a2f7c4a47b2b0ae58b00b0c9d13b519c0a01e891d8bb89e67fbc6" + }, + { + "path": "previews/fullbleed/report/page-2.png", + "bytes": 74781, + "sha256": "2b7d24ca15f011bb8141540a66f9b6f5fa6ed63d8f426665f0c2024dcb37849d" + }, + { + "path": "previews/fullbleed/report/page-3.png", + "bytes": 74197, + "sha256": "2cbe838ab3d6c9eb1e8d4a1603d9f86945f939765127f1164a371e010c30983f" + }, + { + "path": "previews/weasyprint/invoice/page-1.png", + "bytes": 64228, + "sha256": "f068b89ffa7111e6d4163b6e963465891f4ac259f259927e3409913dbbf9f99a" + }, + { + "path": "previews/weasyprint/ledger/page-1.png", + "bytes": 135823, + "sha256": "66c27805c8ee778040929aa12892a042af50e8c1940b977a745869f612ee0a2d" + }, + { + "path": "previews/weasyprint/ledger/page-2.png", + "bytes": 146917, + "sha256": "8bf6720237a37202af03c87886683a2ec8e7aa5598ed4dd4a16a53bcd928efa1" + }, + { + "path": "previews/weasyprint/ledger/page-3.png", + "bytes": 142547, + "sha256": "ff1047622797288e10d9e65074830ded47959ed0365b1890b4aafb42ff31db0a" + }, + { + "path": "previews/weasyprint/report/page-1.png", + "bytes": 74954, + "sha256": "df6c6a869847ce90f4239448d4d8ea794f2303f6986ff1616d00290f86735c65" + }, + { + "path": "previews/weasyprint/report/page-2.png", + "bytes": 74730, + "sha256": "f86cb89f78820018faa2bea5e0a7ead3a4d59f661104749a57ca30b2c339bf04" + }, + { + "path": "previews/weasyprint/report/page-3.png", + "bytes": 73644, + "sha256": "d7bc3e24d1a8311214f6196c766db5b9f095b70fb2ea48cb3c2cbb8d1134b82a" + }, + { + "path": "setup-diagnostics.zip", + "bytes": 12316176, + "sha256": "4c8d50c608a12d428b06f9e46109f2db802c193b1ef81da9bac9d647826201e1" + }, + { + "path": "summary.json", + "bytes": 6583, + "sha256": "08336e04033e9044e7776797bd24f03a79e7e0d0df5288d0efcfe244ff8d751c" + }, + { + "path": "latency.svg", + "bytes": 84500, + "sha256": "d23f5305478dba3dfc2913ad8cf426a401cce3fa4c2084f94aae378f00abefb9" + }, + { + "path": "latency.png", + "bytes": 103752, + "sha256": "2a071a0b2bf8253f7187802aa03893403178509a1e8247de633b706d92dd38d4" + }, + { + "path": "visual-review.json", + "bytes": 4490, + "sha256": "677f95bd405d1fda80c734f0cae89a3c554b3053ead21cfa1f321c4aed887232" + } + ] +} diff --git a/docs/assets/renderer-comparison/measured-evidence.zip b/docs/assets/renderer-comparison/measured-evidence.zip new file mode 100644 index 0000000..a5beb80 Binary files /dev/null and b/docs/assets/renderer-comparison/measured-evidence.zip differ diff --git a/docs/assets/renderer-comparison/negative-controls.json b/docs/assets/renderer-comparison/negative-controls.json new file mode 100644 index 0000000..a4cd435 --- /dev/null +++ b/docs/assets/renderer-comparison/negative-controls.json @@ -0,0 +1,381 @@ +{ + "fullbleed": { + "passed": true, + "wrong_token": [ + "RUN-999999: expected once, found 0", + "body text segment missing: October2026/RUN-999999" + ], + "missing_page": [ + "page count 2 outside declared range", + "ROW-069: expected once, found 0", + "ROW-070: expected once, found 0", + "ROW-071: expected once, found 0", + "ROW-072: expected once, found 0", + "ROW-073: expected once, found 0", + "ROW-074: expected once, found 0", + "ROW-075: expected once, found 0", + "ROW-076: expected once, found 0", + "ROW-077: expected once, found 0", + "ROW-078: expected once, found 0", + "ROW-079: expected once, found 0", + "ROW-080: expected once, found 0", + "ROW-081: expected once, found 0", + "ROW-082: expected once, found 0", + "ROW-083: expected once, found 0", + "ROW-084: expected once, found 0", + "ROW-085: expected once, found 0", + "ROW-086: expected once, found 0", + "ROW-087: expected once, found 0", + "ROW-088: expected once, found 0", + "ROW-089: expected once, found 0", + "ROW-090: expected once, found 0", + "ROW-091: expected once, found 0", + "ROW-092: expected once, found 0", + "ROW-093: expected once, found 0", + "ROW-094: expected once, found 0", + "ROW-095: expected once, found 0", + "ROW-096: expected once, found 0", + "ROW-097: expected once, found 0", + "ROW-098: expected once, found 0", + "ROW-099: expected once, found 0", + "ROW-100: expected once, found 0", + "record order differs from input", + "required text missing: Ledger total: USD 15,150.00", + "required text missing: LEDGER-END", + "body text segment missing: ROW-069", + "body text segment missing: Serviceentry069", + "body text segment missing: 207.00", + "body text segment missing: ROW-070", + "body text segment missing: Serviceentry070", + "body text segment missing: 210.00", + "body text segment missing: ROW-071", + "body text segment missing: Serviceentry071", + "body text segment missing: 213.00", + "body text segment missing: ROW-072", + "body text segment missing: Serviceentry072", + "body text segment missing: 216.00", + "body text segment missing: ROW-073", + "body text segment missing: Serviceentry073", + "body text segment missing: 219.00", + "body text segment missing: ROW-074", + "body text segment missing: Serviceentry074", + "body text segment missing: 222.00", + "body text segment missing: ROW-075", + "body text segment missing: Serviceentry075", + "body text segment missing: 225.00", + "body text segment missing: ROW-076", + "body text segment missing: Serviceentry076", + "body text segment missing: 228.00", + "body text segment missing: ROW-077", + "body text segment missing: Serviceentry077", + "body text segment missing: 231.00", + "body text segment missing: ROW-078", + "body text segment missing: Serviceentry078", + "body text segment missing: 234.00", + "body text segment missing: ROW-079", + "body text segment missing: Serviceentry079", + "body text segment missing: 237.00", + "body text segment missing: ROW-080", + "body text segment missing: Serviceentry080", + "body text segment missing: 240.00", + "body text segment missing: ROW-081", + "body text segment missing: Serviceentry081", + "body text segment missing: 243.00", + "body text segment missing: ROW-082", + "body text segment missing: Serviceentry082", + "body text segment missing: 246.00", + "body text segment missing: ROW-083", + "body text segment missing: Serviceentry083", + "body text segment missing: 249.00", + "body text segment missing: ROW-084", + "body text segment missing: Serviceentry084", + "body text segment missing: 252.00", + "body text segment missing: ROW-085", + "body text segment missing: Serviceentry085", + "body text segment missing: 255.00", + "body text segment missing: ROW-086", + "body text segment missing: Serviceentry086", + "body text segment missing: 258.00", + "body text segment missing: ROW-087", + "body text segment missing: Serviceentry087", + "body text segment missing: 261.00", + "body text segment missing: ROW-088", + "body text segment missing: Serviceentry088", + "body text segment missing: 264.00", + "body text segment missing: ROW-089", + "body text segment missing: Serviceentry089", + "body text segment missing: 267.00", + "body text segment missing: ROW-090", + "body text segment missing: Serviceentry090", + "body text segment missing: 270.00", + "body text segment missing: ROW-091", + "body text segment missing: Serviceentry091", + "body text segment missing: 273.00", + "body text segment missing: ROW-092", + "body text segment missing: Serviceentry092", + "body text segment missing: 276.00", + "body text segment missing: ROW-093", + "body text segment missing: Serviceentry093", + "body text segment missing: 279.00", + "body text segment missing: ROW-094", + "body text segment missing: Serviceentry094", + "body text segment missing: 282.00", + "body text segment missing: ROW-095", + "body text segment missing: Serviceentry095", + "body text segment missing: 285.00", + "body text segment missing: ROW-096", + "body text segment missing: Serviceentry096", + "body text segment missing: 288.00", + "body text segment missing: ROW-097", + "body text segment missing: Serviceentry097", + "body text segment missing: 291.00", + "body text segment missing: ROW-098", + "body text segment missing: Serviceentry098", + "body text segment missing: 294.00", + "body text segment missing: ROW-099", + "body text segment missing: Serviceentry099", + "body text segment missing: 297.00", + "body text segment missing: ROW-100", + "body text segment missing: Serviceentry100", + "body text segment missing: 300.00", + "body text segment missing: Ledgertotal:USD15,150.00", + "body text segment missing: LEDGER-END/All100entriesincluded.", + "row values differ or are separated: ROW-069", + "row values differ or are separated: ROW-070", + "row values differ or are separated: ROW-071", + "row values differ or are separated: ROW-072", + "row values differ or are separated: ROW-073", + "row values differ or are separated: ROW-074", + "row values differ or are separated: ROW-075", + "row values differ or are separated: ROW-076", + "row values differ or are separated: ROW-077", + "row values differ or are separated: ROW-078", + "row values differ or are separated: ROW-079", + "row values differ or are separated: ROW-080", + "row values differ or are separated: ROW-081", + "row values differ or are separated: ROW-082", + "row values differ or are separated: ROW-083", + "row values differ or are separated: ROW-084", + "row values differ or are separated: ROW-085", + "row values differ or are separated: ROW-086", + "row values differ or are separated: ROW-087", + "row values differ or are separated: ROW-088", + "row values differ or are separated: ROW-089", + "row values differ or are separated: ROW-090", + "row values differ or are separated: ROW-091", + "row values differ or are separated: ROW-092", + "row values differ or are separated: ROW-093", + "row values differ or are separated: ROW-094", + "row values differ or are separated: ROW-095", + "row values differ or are separated: ROW-096", + "row values differ or are separated: ROW-097", + "row values differ or are separated: ROW-098", + "row values differ or are separated: ROW-099", + "row values differ or are separated: ROW-100" + ], + "missing_font": [ + "font is not embedded: /ETCNII+Inter-Regular" + ], + "outside_page_inset": [ + "243 visible character boxes outside declared inset" + ] + }, + "chromium": { + "passed": true, + "wrong_token": [ + "RUN-999999: expected once, found 0", + "body text segment missing: October2026/RUN-999999" + ], + "missing_page": [ + "required text missing: LEDGER-END", + "body text segment missing: LEDGER-END/All100entriesincluded." + ], + "missing_font": [ + "font is not embedded: /AAAAAA+Inter-Regular" + ], + "outside_page_inset": [ + "244 visible character boxes outside declared inset" + ] + }, + "weasyprint": { + "passed": true, + "wrong_token": [ + "RUN-999999: expected once, found 0", + "body text segment missing: October2026/RUN-999999" + ], + "missing_page": [ + "page count 2 outside declared range", + "ROW-069: expected once, found 0", + "ROW-070: expected once, found 0", + "ROW-071: expected once, found 0", + "ROW-072: expected once, found 0", + "ROW-073: expected once, found 0", + "ROW-074: expected once, found 0", + "ROW-075: expected once, found 0", + "ROW-076: expected once, found 0", + "ROW-077: expected once, found 0", + "ROW-078: expected once, found 0", + "ROW-079: expected once, found 0", + "ROW-080: expected once, found 0", + "ROW-081: expected once, found 0", + "ROW-082: expected once, found 0", + "ROW-083: expected once, found 0", + "ROW-084: expected once, found 0", + "ROW-085: expected once, found 0", + "ROW-086: expected once, found 0", + "ROW-087: expected once, found 0", + "ROW-088: expected once, found 0", + "ROW-089: expected once, found 0", + "ROW-090: expected once, found 0", + "ROW-091: expected once, found 0", + "ROW-092: expected once, found 0", + "ROW-093: expected once, found 0", + "ROW-094: expected once, found 0", + "ROW-095: expected once, found 0", + "ROW-096: expected once, found 0", + "ROW-097: expected once, found 0", + "ROW-098: expected once, found 0", + "ROW-099: expected once, found 0", + "ROW-100: expected once, found 0", + "record order differs from input", + "required text missing: Ledger total: USD 15,150.00", + "required text missing: LEDGER-END", + "body text segment missing: ROW-069", + "body text segment missing: Serviceentry069", + "body text segment missing: 207.00", + "body text segment missing: ROW-070", + "body text segment missing: Serviceentry070", + "body text segment missing: 210.00", + "body text segment missing: ROW-071", + "body text segment missing: Serviceentry071", + "body text segment missing: 213.00", + "body text segment missing: ROW-072", + "body text segment missing: Serviceentry072", + "body text segment missing: 216.00", + "body text segment missing: ROW-073", + "body text segment missing: Serviceentry073", + "body text segment missing: 219.00", + "body text segment missing: ROW-074", + "body text segment missing: Serviceentry074", + "body text segment missing: 222.00", + "body text segment missing: ROW-075", + "body text segment missing: Serviceentry075", + "body text segment missing: 225.00", + "body text segment missing: ROW-076", + "body text segment missing: Serviceentry076", + "body text segment missing: 228.00", + "body text segment missing: ROW-077", + "body text segment missing: Serviceentry077", + "body text segment missing: 231.00", + "body text segment missing: ROW-078", + "body text segment missing: Serviceentry078", + "body text segment missing: 234.00", + "body text segment missing: ROW-079", + "body text segment missing: Serviceentry079", + "body text segment missing: 237.00", + "body text segment missing: ROW-080", + "body text segment missing: Serviceentry080", + "body text segment missing: 240.00", + "body text segment missing: ROW-081", + "body text segment missing: Serviceentry081", + "body text segment missing: 243.00", + "body text segment missing: ROW-082", + "body text segment missing: Serviceentry082", + "body text segment missing: 246.00", + "body text segment missing: ROW-083", + "body text segment missing: Serviceentry083", + "body text segment missing: 249.00", + "body text segment missing: ROW-084", + "body text segment missing: Serviceentry084", + "body text segment missing: 252.00", + "body text segment missing: ROW-085", + "body text segment missing: Serviceentry085", + "body text segment missing: 255.00", + "body text segment missing: ROW-086", + "body text segment missing: Serviceentry086", + "body text segment missing: 258.00", + "body text segment missing: ROW-087", + "body text segment missing: Serviceentry087", + "body text segment missing: 261.00", + "body text segment missing: ROW-088", + "body text segment missing: Serviceentry088", + "body text segment missing: 264.00", + "body text segment missing: ROW-089", + "body text segment missing: Serviceentry089", + "body text segment missing: 267.00", + "body text segment missing: ROW-090", + "body text segment missing: Serviceentry090", + "body text segment missing: 270.00", + "body text segment missing: ROW-091", + "body text segment missing: Serviceentry091", + "body text segment missing: 273.00", + "body text segment missing: ROW-092", + "body text segment missing: Serviceentry092", + "body text segment missing: 276.00", + "body text segment missing: ROW-093", + "body text segment missing: Serviceentry093", + "body text segment missing: 279.00", + "body text segment missing: ROW-094", + "body text segment missing: Serviceentry094", + "body text segment missing: 282.00", + "body text segment missing: ROW-095", + "body text segment missing: Serviceentry095", + "body text segment missing: 285.00", + "body text segment missing: ROW-096", + "body text segment missing: Serviceentry096", + "body text segment missing: 288.00", + "body text segment missing: ROW-097", + "body text segment missing: Serviceentry097", + "body text segment missing: 291.00", + "body text segment missing: ROW-098", + "body text segment missing: Serviceentry098", + "body text segment missing: 294.00", + "body text segment missing: ROW-099", + "body text segment missing: Serviceentry099", + "body text segment missing: 297.00", + "body text segment missing: ROW-100", + "body text segment missing: Serviceentry100", + "body text segment missing: 300.00", + "body text segment missing: Ledgertotal:USD15,150.00", + "body text segment missing: LEDGER-END/All100entriesincluded.", + "row values differ or are separated: ROW-069", + "row values differ or are separated: ROW-070", + "row values differ or are separated: ROW-071", + "row values differ or are separated: ROW-072", + "row values differ or are separated: ROW-073", + "row values differ or are separated: ROW-074", + "row values differ or are separated: ROW-075", + "row values differ or are separated: ROW-076", + "row values differ or are separated: ROW-077", + "row values differ or are separated: ROW-078", + "row values differ or are separated: ROW-079", + "row values differ or are separated: ROW-080", + "row values differ or are separated: ROW-081", + "row values differ or are separated: ROW-082", + "row values differ or are separated: ROW-083", + "row values differ or are separated: ROW-084", + "row values differ or are separated: ROW-085", + "row values differ or are separated: ROW-086", + "row values differ or are separated: ROW-087", + "row values differ or are separated: ROW-088", + "row values differ or are separated: ROW-089", + "row values differ or are separated: ROW-090", + "row values differ or are separated: ROW-091", + "row values differ or are separated: ROW-092", + "row values differ or are separated: ROW-093", + "row values differ or are separated: ROW-094", + "row values differ or are separated: ROW-095", + "row values differ or are separated: ROW-096", + "row values differ or are separated: ROW-097", + "row values differ or are separated: ROW-098", + "row values differ or are separated: ROW-099", + "row values differ or are separated: ROW-100" + ], + "missing_font": [ + "font is not embedded: /LNXIQZ+Inter" + ], + "outside_page_inset": [ + "243 visible character boxes outside declared inset" + ] + } +} diff --git a/docs/assets/renderer-comparison/packages.txt b/docs/assets/renderer-comparison/packages.txt new file mode 100644 index 0000000..cc7c6a4 --- /dev/null +++ b/docs/assets/renderer-comparison/packages.txt @@ -0,0 +1,21 @@ +brotli==1.2.0 +cffi==2.1.1 +cssselect2==0.10.1 +fonttools==4.65.0 +fullbleed==2.5.6 +greenlet==3.5.6 +pillow==12.3.0 +playwright==1.63.0 +psutil==7.2.2 +pycparser==3.0 +pydyf==0.12.1 +pyee==13.0.1 +pypdf==6.19.0 +pypdfium2==5.14.0 +pyphen==0.18.1 +tinycss2==1.5.1 +tinyhtml5==2.1.0 +typing_extensions==4.16.0 +weasyprint==70.0 +webencodings==0.6.1 +zopfli==0.4.3 diff --git a/docs/assets/renderer-comparison/pdfs/chromium/invoice.pdf b/docs/assets/renderer-comparison/pdfs/chromium/invoice.pdf new file mode 100644 index 0000000..114bb0e Binary files /dev/null and b/docs/assets/renderer-comparison/pdfs/chromium/invoice.pdf differ diff --git a/docs/assets/renderer-comparison/pdfs/chromium/ledger.pdf b/docs/assets/renderer-comparison/pdfs/chromium/ledger.pdf new file mode 100644 index 0000000..f758d24 Binary files /dev/null and b/docs/assets/renderer-comparison/pdfs/chromium/ledger.pdf differ diff --git a/docs/assets/renderer-comparison/pdfs/chromium/report.pdf b/docs/assets/renderer-comparison/pdfs/chromium/report.pdf new file mode 100644 index 0000000..4745b5b Binary files /dev/null and b/docs/assets/renderer-comparison/pdfs/chromium/report.pdf differ diff --git a/docs/assets/renderer-comparison/pdfs/fullbleed/invoice.pdf b/docs/assets/renderer-comparison/pdfs/fullbleed/invoice.pdf new file mode 100644 index 0000000..587a76a Binary files /dev/null and b/docs/assets/renderer-comparison/pdfs/fullbleed/invoice.pdf differ diff --git a/docs/assets/renderer-comparison/pdfs/fullbleed/ledger.pdf b/docs/assets/renderer-comparison/pdfs/fullbleed/ledger.pdf new file mode 100644 index 0000000..e0238c6 Binary files /dev/null and b/docs/assets/renderer-comparison/pdfs/fullbleed/ledger.pdf differ diff --git a/docs/assets/renderer-comparison/pdfs/fullbleed/report.pdf b/docs/assets/renderer-comparison/pdfs/fullbleed/report.pdf new file mode 100644 index 0000000..8191cfc Binary files /dev/null and b/docs/assets/renderer-comparison/pdfs/fullbleed/report.pdf differ diff --git a/docs/assets/renderer-comparison/pdfs/weasyprint/invoice.pdf b/docs/assets/renderer-comparison/pdfs/weasyprint/invoice.pdf new file mode 100644 index 0000000..b3bdc3b Binary files /dev/null and b/docs/assets/renderer-comparison/pdfs/weasyprint/invoice.pdf differ diff --git a/docs/assets/renderer-comparison/pdfs/weasyprint/ledger.pdf b/docs/assets/renderer-comparison/pdfs/weasyprint/ledger.pdf new file mode 100644 index 0000000..a6dd7ff Binary files /dev/null and b/docs/assets/renderer-comparison/pdfs/weasyprint/ledger.pdf differ diff --git a/docs/assets/renderer-comparison/pdfs/weasyprint/report.pdf b/docs/assets/renderer-comparison/pdfs/weasyprint/report.pdf new file mode 100644 index 0000000..3002a8f Binary files /dev/null and b/docs/assets/renderer-comparison/pdfs/weasyprint/report.pdf differ diff --git a/docs/assets/renderer-comparison/plan.json b/docs/assets/renderer-comparison/plan.json new file mode 100644 index 0000000..5367d6e --- /dev/null +++ b/docs/assets/renderer-comparison/plan.json @@ -0,0 +1,20 @@ +{ + "schema": "fullbleed.renderer_comparison.v1", + "smoke": false, + "seed": 20261004, + "cold_rounds": 5, + "warm_blocks": 3, + "warm_samples_per_block": 10, + "warmups_per_block": 2, + "memory_samples": 3, + "engines": [ + "fullbleed", + "weasyprint", + "chromium" + ], + "fixtures": [ + "invoice", + "ledger", + "report" + ] +} diff --git a/docs/assets/renderer-comparison/previews/chromium/invoice/page-1.png b/docs/assets/renderer-comparison/previews/chromium/invoice/page-1.png new file mode 100644 index 0000000..5027f31 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/chromium/invoice/page-1.png differ diff --git a/docs/assets/renderer-comparison/previews/chromium/ledger/page-1.png b/docs/assets/renderer-comparison/previews/chromium/ledger/page-1.png new file mode 100644 index 0000000..9a0a21d Binary files /dev/null and b/docs/assets/renderer-comparison/previews/chromium/ledger/page-1.png differ diff --git a/docs/assets/renderer-comparison/previews/chromium/ledger/page-2.png b/docs/assets/renderer-comparison/previews/chromium/ledger/page-2.png new file mode 100644 index 0000000..e5c1f6f Binary files /dev/null and b/docs/assets/renderer-comparison/previews/chromium/ledger/page-2.png differ diff --git a/docs/assets/renderer-comparison/previews/chromium/ledger/page-3.png b/docs/assets/renderer-comparison/previews/chromium/ledger/page-3.png new file mode 100644 index 0000000..549be8a Binary files /dev/null and b/docs/assets/renderer-comparison/previews/chromium/ledger/page-3.png differ diff --git a/docs/assets/renderer-comparison/previews/chromium/ledger/page-4.png b/docs/assets/renderer-comparison/previews/chromium/ledger/page-4.png new file mode 100644 index 0000000..f11033d Binary files /dev/null and b/docs/assets/renderer-comparison/previews/chromium/ledger/page-4.png differ diff --git a/docs/assets/renderer-comparison/previews/chromium/report/page-1.png b/docs/assets/renderer-comparison/previews/chromium/report/page-1.png new file mode 100644 index 0000000..a9ecef3 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/chromium/report/page-1.png differ diff --git a/docs/assets/renderer-comparison/previews/chromium/report/page-2.png b/docs/assets/renderer-comparison/previews/chromium/report/page-2.png new file mode 100644 index 0000000..b030d98 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/chromium/report/page-2.png differ diff --git a/docs/assets/renderer-comparison/previews/chromium/report/page-3.png b/docs/assets/renderer-comparison/previews/chromium/report/page-3.png new file mode 100644 index 0000000..e92f3e4 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/chromium/report/page-3.png differ diff --git a/docs/assets/renderer-comparison/previews/fullbleed/invoice/page-1.png b/docs/assets/renderer-comparison/previews/fullbleed/invoice/page-1.png new file mode 100644 index 0000000..f6cbbcd Binary files /dev/null and b/docs/assets/renderer-comparison/previews/fullbleed/invoice/page-1.png differ diff --git a/docs/assets/renderer-comparison/previews/fullbleed/ledger/page-1.png b/docs/assets/renderer-comparison/previews/fullbleed/ledger/page-1.png new file mode 100644 index 0000000..82c354d Binary files /dev/null and b/docs/assets/renderer-comparison/previews/fullbleed/ledger/page-1.png differ diff --git a/docs/assets/renderer-comparison/previews/fullbleed/ledger/page-2.png b/docs/assets/renderer-comparison/previews/fullbleed/ledger/page-2.png new file mode 100644 index 0000000..0aa37b7 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/fullbleed/ledger/page-2.png differ diff --git a/docs/assets/renderer-comparison/previews/fullbleed/ledger/page-3.png b/docs/assets/renderer-comparison/previews/fullbleed/ledger/page-3.png new file mode 100644 index 0000000..b85a71f Binary files /dev/null and b/docs/assets/renderer-comparison/previews/fullbleed/ledger/page-3.png differ diff --git a/docs/assets/renderer-comparison/previews/fullbleed/report/page-1.png b/docs/assets/renderer-comparison/previews/fullbleed/report/page-1.png new file mode 100644 index 0000000..5422ea6 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/fullbleed/report/page-1.png differ diff --git a/docs/assets/renderer-comparison/previews/fullbleed/report/page-2.png b/docs/assets/renderer-comparison/previews/fullbleed/report/page-2.png new file mode 100644 index 0000000..cf1830a Binary files /dev/null and b/docs/assets/renderer-comparison/previews/fullbleed/report/page-2.png differ diff --git a/docs/assets/renderer-comparison/previews/fullbleed/report/page-3.png b/docs/assets/renderer-comparison/previews/fullbleed/report/page-3.png new file mode 100644 index 0000000..713c730 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/fullbleed/report/page-3.png differ diff --git a/docs/assets/renderer-comparison/previews/weasyprint/invoice/page-1.png b/docs/assets/renderer-comparison/previews/weasyprint/invoice/page-1.png new file mode 100644 index 0000000..5b0cb19 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/weasyprint/invoice/page-1.png differ diff --git a/docs/assets/renderer-comparison/previews/weasyprint/ledger/page-1.png b/docs/assets/renderer-comparison/previews/weasyprint/ledger/page-1.png new file mode 100644 index 0000000..a30fb0f Binary files /dev/null and b/docs/assets/renderer-comparison/previews/weasyprint/ledger/page-1.png differ diff --git a/docs/assets/renderer-comparison/previews/weasyprint/ledger/page-2.png b/docs/assets/renderer-comparison/previews/weasyprint/ledger/page-2.png new file mode 100644 index 0000000..a73e8b6 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/weasyprint/ledger/page-2.png differ diff --git a/docs/assets/renderer-comparison/previews/weasyprint/ledger/page-3.png b/docs/assets/renderer-comparison/previews/weasyprint/ledger/page-3.png new file mode 100644 index 0000000..a08749a Binary files /dev/null and b/docs/assets/renderer-comparison/previews/weasyprint/ledger/page-3.png differ diff --git a/docs/assets/renderer-comparison/previews/weasyprint/report/page-1.png b/docs/assets/renderer-comparison/previews/weasyprint/report/page-1.png new file mode 100644 index 0000000..9e3ae82 Binary files /dev/null and b/docs/assets/renderer-comparison/previews/weasyprint/report/page-1.png differ diff --git a/docs/assets/renderer-comparison/previews/weasyprint/report/page-2.png b/docs/assets/renderer-comparison/previews/weasyprint/report/page-2.png new file mode 100644 index 0000000..f1b2e7b Binary files /dev/null and b/docs/assets/renderer-comparison/previews/weasyprint/report/page-2.png differ diff --git a/docs/assets/renderer-comparison/previews/weasyprint/report/page-3.png b/docs/assets/renderer-comparison/previews/weasyprint/report/page-3.png new file mode 100644 index 0000000..63304ae Binary files /dev/null and b/docs/assets/renderer-comparison/previews/weasyprint/report/page-3.png differ diff --git a/docs/assets/renderer-comparison/setup-diagnostics.zip b/docs/assets/renderer-comparison/setup-diagnostics.zip new file mode 100644 index 0000000..73e7776 Binary files /dev/null and b/docs/assets/renderer-comparison/setup-diagnostics.zip differ diff --git a/docs/assets/renderer-comparison/summary.json b/docs/assets/renderer-comparison/summary.json new file mode 100644 index 0000000..de1778e --- /dev/null +++ b/docs/assets/renderer-comparison/summary.json @@ -0,0 +1,313 @@ +{ + "schema": "fullbleed.renderer_comparison.v1", + "smoke": false, + "all_qualified": true, + "results": [ + { + "engine": "fullbleed", + "fixture": "invoice", + "cold_ms": { + "n": 5, + "median": 73.915637, + "min": 60.625044, + "max": 92.253373, + "q1": 62.729927, + "q3": 81.231563 + }, + "warm_ms": { + "n": 30, + "median": 12.892807000000001, + "min": 10.951137, + "max": 21.540902, + "q1": 12.03344525, + "q3": 14.42500975 + }, + "valid": true, + "failures": [], + "pages": [ + 1 + ], + "bytes": { + "n": 46, + "median": 52558.0, + "min": 52552, + "max": 52564, + "q1": 52557.0, + "q3": 52559.0 + }, + "sampled_peak_tree_rss_bytes": 28549120 + }, + { + "engine": "fullbleed", + "fixture": "ledger", + "cold_ms": { + "n": 5, + "median": 161.076359, + "min": 140.251548, + "max": 407.502735, + "q1": 157.261418, + "q3": 195.658708 + }, + "warm_ms": { + "n": 30, + "median": 87.2428455, + "min": 74.288807, + "max": 138.216923, + "q1": 84.3251435, + "q3": 98.5379275 + }, + "valid": true, + "failures": [], + "pages": [ + 3 + ], + "bytes": { + "n": 46, + "median": 57563.0, + "min": 57559, + "max": 57567, + "q1": 57562.0, + "q3": 57564.0 + }, + "sampled_peak_tree_rss_bytes": 29540352 + }, + { + "engine": "fullbleed", + "fixture": "report", + "cold_ms": { + "n": 5, + "median": 111.542901, + "min": 83.977163, + "max": 134.511696, + "q1": 103.990166, + "q3": 116.998154 + }, + "warm_ms": { + "n": 30, + "median": 27.451739500000002, + "min": 23.155751, + "max": 36.463563, + "q1": 25.977579249999998, + "q3": 30.99795475 + }, + "valid": true, + "failures": [], + "pages": [ + 3 + ], + "bytes": { + "n": 46, + "median": 58104.5, + "min": 57959, + "max": 58248, + "q1": 57963.0, + "q3": 58120.0 + }, + "sampled_peak_tree_rss_bytes": 29016064 + }, + { + "engine": "weasyprint", + "fixture": "invoice", + "cold_ms": { + "n": 5, + "median": 1011.983852, + "min": 890.089639, + "max": 1238.084758, + "q1": 922.09328, + "q3": 1201.915657 + }, + "warm_ms": { + "n": 30, + "median": 78.46518900000001, + "min": 62.508234, + "max": 105.08337, + "q1": 72.24807200000001, + "q3": 86.13786275000001 + }, + "valid": true, + "failures": [], + "pages": [ + 1 + ], + "bytes": { + "n": 46, + "median": 8875.0, + "min": 8870, + "max": 8883, + "q1": 8874.0, + "q3": 8876.0 + }, + "sampled_peak_tree_rss_bytes": 62996480 + }, + { + "engine": "weasyprint", + "fixture": "ledger", + "cold_ms": { + "n": 5, + "median": 1274.162006, + "min": 1151.265728, + "max": 1364.90247, + "q1": 1182.617099, + "q3": 1303.541178 + }, + "warm_ms": { + "n": 30, + "median": 290.74866699999995, + "min": 227.291711, + "max": 383.75004, + "q1": 270.3684075, + "q3": 309.63328125 + }, + "valid": true, + "failures": [], + "pages": [ + 3 + ], + "bytes": { + "n": 46, + "median": 13463.0, + "min": 13457, + "max": 13467, + "q1": 13462.0, + "q3": 13465.0 + }, + "sampled_peak_tree_rss_bytes": 70881280 + }, + { + "engine": "weasyprint", + "fixture": "report", + "cold_ms": { + "n": 5, + "median": 1023.569112, + "min": 996.680671, + "max": 1185.267557, + "q1": 1000.251512, + "q3": 1119.719468 + }, + "warm_ms": { + "n": 30, + "median": 164.8735705, + "min": 118.939696, + "max": 377.77906, + "q1": 127.62240924999999, + "q3": 183.39368625 + }, + "valid": true, + "failures": [], + "pages": [ + 3 + ], + "bytes": { + "n": 46, + "median": 13202.0, + "min": 13162, + "max": 13321, + "q1": 13165.0, + "q3": 13266.75 + }, + "sampled_peak_tree_rss_bytes": 64282624 + }, + { + "engine": "chromium", + "fixture": "invoice", + "cold_ms": { + "n": 5, + "median": 784.576027, + "min": 680.100773, + "max": 828.05302, + "q1": 739.08952, + "q3": 798.091566 + }, + "warm_ms": { + "n": 30, + "median": 38.7000235, + "min": 34.39278, + "max": 48.141352, + "q1": 36.02901625, + "q3": 42.01707175 + }, + "valid": true, + "failures": [], + "pages": [ + 1 + ], + "bytes": { + "n": 46, + "median": 9755.0, + "min": 9752, + "max": 9759, + "q1": 9754.0, + "q3": 9756.75 + }, + "sampled_peak_tree_rss_bytes": 658452480 + }, + { + "engine": "chromium", + "fixture": "ledger", + "cold_ms": { + "n": 5, + "median": 909.832075, + "min": 703.90335, + "max": 1107.074649, + "q1": 784.175319, + "q3": 933.179338 + }, + "warm_ms": { + "n": 30, + "median": 64.5408795, + "min": 43.743366, + "max": 97.388009, + "q1": 54.61024175, + "q3": 76.3826235 + }, + "valid": true, + "failures": [], + "pages": [ + 4 + ], + "bytes": { + "n": 46, + "median": 13757.0, + "min": 13751, + "max": 13760, + "q1": 13754.25, + "q3": 13758.0 + }, + "sampled_peak_tree_rss_bytes": 669442048 + }, + { + "engine": "chromium", + "fixture": "report", + "cold_ms": { + "n": 5, + "median": 811.294165, + "min": 694.549916, + "max": 892.617909, + "q1": 745.222046, + "q3": 822.856224 + }, + "warm_ms": { + "n": 30, + "median": 42.7556235, + "min": 37.555767, + "max": 121.037918, + "q1": 40.33947125, + "q3": 46.87561975 + }, + "valid": true, + "failures": [], + "pages": [ + 3 + ], + "bytes": { + "n": 46, + "median": 14283.5, + "min": 14241, + "max": 14489, + "q1": 14245.25, + "q3": 14356.0 + }, + "sampled_peak_tree_rss_bytes": 661147648 + } + ] +} diff --git a/docs/assets/renderer-comparison/visual-review.json b/docs/assets/renderer-comparison/visual-review.json new file mode 100644 index 0000000..09d80dd --- /dev/null +++ b/docs/assets/renderer-comparison/visual-review.json @@ -0,0 +1,115 @@ +{ + "pages": [ + { + "page": "chromium/invoice/page-1.png", + "sha256": "1fd8ac6dd503b78ca76f69444996487c66e194c603e1b883ca7b37c8cc8298e5", + "matches_inspected_qualification_page": true + }, + { + "page": "chromium/ledger/page-1.png", + "sha256": "21297448050f8bcef034230a50e06ad1914171d8bd175b0a63e7e3c79fdf2b74", + "matches_inspected_qualification_page": true + }, + { + "page": "chromium/ledger/page-2.png", + "sha256": "084f65f1f2d008d2e38dc443c851545b7c4009b7113b9aa46cff66e2337e9aaf", + "matches_inspected_qualification_page": true + }, + { + "page": "chromium/ledger/page-3.png", + "sha256": "3e26b7e0d0e6f9756e6d2e3874508031672d8d3503725f96e6ce6e6675741ec6", + "matches_inspected_qualification_page": true + }, + { + "page": "chromium/ledger/page-4.png", + "sha256": "838041fbd570a25dfe3d4f0c850f55b693e3018774c954e9faf6272ccdabf28a", + "matches_inspected_qualification_page": true + }, + { + "page": "chromium/report/page-1.png", + "sha256": "8736f33b88466343e09b0d6cc708a38288eb40ce0e8fe3dea4d3e54c09884a6e", + "matches_inspected_qualification_page": true + }, + { + "page": "chromium/report/page-2.png", + "sha256": "2c4e2c77ba2a169079f0bbee9f6696f9a8670f527d070f7261f578cacd20f01a", + "matches_inspected_qualification_page": true + }, + { + "page": "chromium/report/page-3.png", + "sha256": "62a85d1d23df7a0ac661bfc6244806981cd71f0c954847db03d30c343a874c0a", + "matches_inspected_qualification_page": true + }, + { + "page": "fullbleed/invoice/page-1.png", + "sha256": "d5d14e3719d5610b1f98a01cf91caaf1a25cfea1deeb43a1b1a7e0900019cc91", + "matches_inspected_qualification_page": true + }, + { + "page": "fullbleed/ledger/page-1.png", + "sha256": "7521aa5f81bb7210adfe053b1e88aecf2203cbd18b21fb62c478917585f8021e", + "matches_inspected_qualification_page": true + }, + { + "page": "fullbleed/ledger/page-2.png", + "sha256": "4f1e2e5dc47253641a9400ed9925312650cfa108c3a9e7e88b0871704752dd96", + "matches_inspected_qualification_page": true + }, + { + "page": "fullbleed/ledger/page-3.png", + "sha256": "fd6f0a5ca6f83169f8411c024465a4ecda76dc18873394d0651aee96bbdfbb9a", + "matches_inspected_qualification_page": true + }, + { + "page": "fullbleed/report/page-1.png", + "sha256": "d4513c02d91a2f7c4a47b2b0ae58b00b0c9d13b519c0a01e891d8bb89e67fbc6", + "matches_inspected_qualification_page": true + }, + { + "page": "fullbleed/report/page-2.png", + "sha256": "2b7d24ca15f011bb8141540a66f9b6f5fa6ed63d8f426665f0c2024dcb37849d", + "matches_inspected_qualification_page": true + }, + { + "page": "fullbleed/report/page-3.png", + "sha256": "2cbe838ab3d6c9eb1e8d4a1603d9f86945f939765127f1164a371e010c30983f", + "matches_inspected_qualification_page": true + }, + { + "page": "weasyprint/invoice/page-1.png", + "sha256": "f068b89ffa7111e6d4163b6e963465891f4ac259f259927e3409913dbbf9f99a", + "matches_inspected_qualification_page": true + }, + { + "page": "weasyprint/ledger/page-1.png", + "sha256": "66c27805c8ee778040929aa12892a042af50e8c1940b977a745869f612ee0a2d", + "matches_inspected_qualification_page": true + }, + { + "page": "weasyprint/ledger/page-2.png", + "sha256": "8bf6720237a37202af03c87886683a2ec8e7aa5598ed4dd4a16a53bcd928efa1", + "matches_inspected_qualification_page": true + }, + { + "page": "weasyprint/ledger/page-3.png", + "sha256": "ff1047622797288e10d9e65074830ded47959ed0365b1890b4aafb42ff31db0a", + "matches_inspected_qualification_page": true + }, + { + "page": "weasyprint/report/page-1.png", + "sha256": "df6c6a869847ce90f4239448d4d8ea794f2303f6986ff1616d00290f86735c65", + "matches_inspected_qualification_page": true + }, + { + "page": "weasyprint/report/page-2.png", + "sha256": "f86cb89f78820018faa2bea5e0a7ead3a4d59f661104749a57ca30b2c339bf04", + "matches_inspected_qualification_page": true + }, + { + "page": "weasyprint/report/page-3.png", + "sha256": "d7bc3e24d1a8311214f6196c766db5b9f095b70fb2ea48cb3c2cbb8d1134b82a", + "matches_inspected_qualification_page": true + } + ], + "review": "All 22 page images match the inspected qualification previews byte for byte. No clipped or overlapping content observed. Chromium ledger closing note is on page 4; table and invoice spacing differ slightly." +} diff --git a/docs/guides/comparison.md b/docs/guides/comparison.md index 9157583..4397375 100644 --- a/docs/guides/comparison.md +++ b/docs/guides/comparison.md @@ -4,7 +4,7 @@ description: Match your PDF task to Fullbleed, WeasyPrint, Playwright, ReportLab --- # Choose a PDF library for your task -Start with the job: authoring a new document, printing browser content, and modifying an existing PDF need different APIs. This guide is maintained by Fullbleed and describes selection considerations, not a measured performance ranking. +Start with the job: authoring a new document, printing browser content, and modifying an existing PDF need different APIs. This guide is maintained by Fullbleed and describes selection considerations. For a measured example, see the [Fullbleed, WeasyPrint, and Chromium comparison](renderer-comparison.md), including output checks, raw samples, and the cases where each tool did better. | Your main task | Tools to evaluate | What to check | | --- | --- | --- | @@ -15,7 +15,7 @@ Start with the job: authoring a new document, printing browser content, and modi ## When Fullbleed is a useful fit -Fullbleed combines a Rust document engine with a Python API, self-contained wheels, explicit assets, deterministic rendering, PNG previews, and structured diagnostics. Its compiled fixed and reflowing binding APIs support repeated document families. The current release is [MIT licensed](https://github.com/fullbleed-engine/fullbleed-official/blob/v2.4.0/LICENSE). +Fullbleed combines a Rust document engine with a Python API, self-contained wheels, explicit assets, deterministic rendering, PNG previews, and structured diagnostics. Its compiled fixed and reflowing binding APIs support repeated document families. The current release is [MIT licensed](https://github.com/fullbleed-engine/fullbleed-official/blob/v2.5.6/LICENSE). Try your own HTML/CSS against the [coverage report](../css-coverage.md). Include long content, page breaks, glyph coverage, and every required output profile in the evaluation. Use the [complete examples](../examples.md) to establish a working baseline. @@ -37,4 +37,4 @@ Check each project's current documentation for detailed capability and license t 5. Run the relevant standards validators on the final files. 6. Measure complete jobs, including startup, compilation, and file writes. -Fullbleed's [retained performance evidence](performance.md) covers specified Fullbleed versions and fixtures. It does not establish speed or quality superiority over the tools listed here. +The [three-renderer comparison](renderer-comparison.md) covers three shared static-document fixtures on one WSL host. The [historical performance evidence](performance.md) covers earlier Fullbleed versions and different workloads. Neither establishes a universal speed or quality ranking. diff --git a/docs/guides/performance.md b/docs/guides/performance.md index 8a1bef4..e2f21e3 100644 --- a/docs/guides/performance.md +++ b/docs/guides/performance.md @@ -1,5 +1,7 @@ # Performance pass results: 2026-08-04 +For a current shared-input example, see the [Fullbleed 2.5.6, WeasyPrint, and Chromium comparison](renderer-comparison.md). The historical report below uses different fixtures and measurement methods; its numbers should not be combined with that comparison. + Reference imported from [v2.5.6](https://github.com/fullbleed-engine/fullbleed-official/blob/a34344fed5b91ace5e1b6798f45aae271177273a/docs/performance-pass-2026-08-04.md). Check the installed runtime for your exact version. This report compares the Fullbleed 2.1.0 release source with the independently measured Fullbleed diff --git a/docs/guides/renderer-comparison.md b/docs/guides/renderer-comparison.md new file mode 100644 index 0000000..c1406cd --- /dev/null +++ b/docs/guides/renderer-comparison.md @@ -0,0 +1,99 @@ +--- +title: Fullbleed, WeasyPrint, and Chromium PDF comparison +description: Reproduce a maintainer-run comparison of three Python PDF renderers with shared HTML/CSS, validated output, raw samples, and downloadable PDFs. +--- +# Compare the PDFs before the timings + +This maintainer-run comparison uses Fullbleed 2.5.6, WeasyPrint 70.0, and Playwright 1.63.0 with Chromium 153.0.8010.12. All three receive the same static HTML/CSS, font, and synthetic records. It covers an invoice, a long ledger, and a three-page report on one Linux/WSL host. + +**On these fixtures, Fullbleed had the lowest fresh-process median for all three jobs and the lowest warm median for the invoice and report. Chromium had the lowest warm median for the ledger. WeasyPrint produced the smallest PDFs in all three jobs.** The complete results include 315 timed PDFs, plus 99 warm-up and memory-pass PDFs; all 414 passed the declared checks. + +## Measured latency + +![Median PDF latency with full observed ranges for three shared document fixtures](../assets/renderer-comparison/latency.svg) + +[Open the full-size chart](../assets/renderer-comparison/latency.svg). The same values and ranges are in the table below. + +Milliseconds per complete PDF; lower is faster. Fresh-process medians use five samples, and warm medians use thirty. The ranges include every sample, with no outliers removed. + +| Document | Renderer | Pages | Fresh median (range), ms | Warm median (range), ms | +| --- | --- | ---: | ---: | ---: | +| Invoice | Fullbleed | 1 | 73.9 (60.6–92.3) | 12.9 (11.0–21.5) | +| Invoice | WeasyPrint | 1 | 1,012.0 (890.1–1,238.1) | 78.5 (62.5–105.1) | +| Invoice | Chromium | 1 | 784.6 (680.1–828.1) | 38.7 (34.4–48.1) | +| Ledger | Fullbleed | 3 | 161.1 (140.3–407.5) | 87.2 (74.3–138.2) | +| Ledger | WeasyPrint | 3 | 1,274.2 (1,151.3–1,364.9) | 290.7 (227.3–383.8) | +| Ledger | Chromium | 4 | 909.8 (703.9–1,107.1) | 64.5 (43.7–97.4) | +| Report | Fullbleed | 3 | 111.5 (84.0–134.5) | 27.5 (23.2–36.5) | +| Report | WeasyPrint | 3 | 1,023.6 (996.7–1,185.3) | 164.9 (118.9–377.8) | +| Report | Chromium | 3 | 811.3 (694.5–892.6) | 42.8 (37.6–121.0) | + +The run started October 5, 2026 at 00:22 UTC on an Intel Core i7-12700H, 20 logical CPUs, about 31.2 GiB available to WSL2, Ubuntu 22.04, Python 3.10.12, and Pango 1.50.6. The kernel was `6.18.40.1-microsoft-standard-WSL2`. This was one development host, not a dedicated benchmark machine. [Environment and package versions](../assets/renderer-comparison/environment.json) and [raw results with quartiles](../assets/renderer-comparison/summary.json) are retained. + +## Output size and sampled memory + +PDF size is the median of the retained outputs. Memory is one separate sampled process-tree run for each pair, with the limitations described below. MiB and KiB use powers of 1024. + +| Document | Renderer | PDF, KiB | Sampled peak tree RSS, MiB | +| --- | --- | ---: | ---: | +| Invoice | Fullbleed | 51.3 | 27.2 | +| Invoice | WeasyPrint | 8.7 | 60.1 | +| Invoice | Chromium | 9.5 | 627.9 | +| Ledger | Fullbleed | 56.2 | 28.2 | +| Ledger | WeasyPrint | 13.1 | 67.6 | +| Ledger | Chromium | 13.4 | 638.4 | +| Report | Fullbleed | 56.7 | 27.7 | +| Report | WeasyPrint | 12.9 | 61.3 | +| Report | Chromium | 13.9 | 630.5 | + +## Inspect the files + +| Document | Fullbleed PDF | WeasyPrint PDF | Chromium PDF | +| --- | --- | --- | --- | +| Invoice | [1 page](../assets/renderer-comparison/pdfs/fullbleed/invoice.pdf) | [1 page](../assets/renderer-comparison/pdfs/weasyprint/invoice.pdf) | [1 page](../assets/renderer-comparison/pdfs/chromium/invoice.pdf) | +| Ledger | [3 pages](../assets/renderer-comparison/pdfs/fullbleed/ledger.pdf) | [3 pages](../assets/renderer-comparison/pdfs/weasyprint/ledger.pdf) | [4 pages](../assets/renderer-comparison/pdfs/chromium/ledger.pdf) | +| Report | [3 pages](../assets/renderer-comparison/pdfs/fullbleed/report.pdf) | [3 pages](../assets/renderer-comparison/pdfs/weasyprint/report.pdf) | [3 pages](../assets/renderer-comparison/pdfs/chromium/report.pdf) | + +[Download the measured evidence (12.9 MiB)](../assets/renderer-comparison/measured-evidence.zip): all 414 PDFs, source snapshot, raw timing and memory samples, input contracts, page previews, and validation results. [Download the initial setup diagnostics (11.7 MiB)](../assets/renderer-comparison/setup-diagnostics.zip) for the font and WSL findings. Both archives include a file manifest; [download checksums](../assets/renderer-comparison/manifest.json) are published separately. + +## What the output checks cover + +Every output must contain all body-text segments, every record ID exactly once and in order, the expected row values and totals, A4 page dimensions, the declared page count, and embedded Inter fonts. PDFium checks non-whitespace character boxes against the page inset. The ledger repeats table headings; report sections start on their declared pages. Validation uses pypdf and PDFium separately from the rendering engines. + +The acceptance contract permits three or four ledger pages. Chromium puts the closing note on page four; Fullbleed and WeasyPrint keep it on page three. This is a layout difference, and the timing table counts each complete document. It is not a claim of pixel equality. + +The harness also rejects deliberately wrong record expectations, a removed final page, missing font embedding, and text shifted beyond the declared inset. Representative pages were visually inspected for clipping, overlap, and readable table transitions. These checks are not PDF/A, PDF/UA, or PDF/X certification. + +## What each measurement includes + +**Fresh process** measures Python process launch through exit: imports, input reads, font setup, browser launch when applicable, rendering, writing the PDF, and cleanup. It does not flush operating-system caches or reboot the host. + +**Warm process** reuses a Fullbleed engine, a parsed WeasyPrint stylesheet and font configuration, or a Chromium browser and page. Each render gets changed HTML with a unique fixed-length token. Timing includes HTML parsing, layout, PDF creation, and a common Python file write. Chromium waits for its fonts to load. There are two warm-ups in each of three independently started ten-sample blocks. + +**Memory** is a separate run that samples summed process-tree RSS every 10 milliseconds from startup through two warm-ups and three renders. It includes the Playwright driver and browser children. Shared pages can be counted more than once, and short peaks can be missed. This is neither exact peak memory nor allocation per PDF. Memory sampling does not run during latency measurement. + +Jobs run sequentially in seeded shuffled order. Every raw sample and output is retained, including outliers. Writes are buffered without `fsync`. No HTTP service, concurrent workload, browser JavaScript content, compiled-template API, or VDP job is measured. + +## Two setup findings worth reproducing + +Inter's contextual alternates exposed an extraction problem in this Chromium version: a hyphen in an uppercase identifier such as `RUN-000001` became a private-use character in pypdf's extracted text. A static Inter instance reproduced it. Disabling the `calt` feature preserved the IDs. The measured fixtures use the same static Inter font and `font-feature-settings: 'calt' 0` in all three engines. The initial failing outputs are retained separately. + +WSL inherited 82 Windows PATH entries on this host. WeasyPrint's shared-library discovery searched them during import; an isolated diagnostic took about 11.8 seconds with that PATH and about 1 second with the standard Linux directories. Every measured worker receives the same Linux PATH policy. The initial import profiles are retained; the comparison does not present that WSL configuration penalty as renderer performance. + +## Reproduce with your own documents + +Start from the [source and methodology](https://github.com/fullbleed-engine/fullbleed-official/tree/4ed41e3c48f90144b21e6360464dd1d868a00085/benchmarks/pdf_renderers). Follow [WeasyPrint's installation instructions](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html) for its system libraries and [Playwright's browser setup](https://playwright.dev/python/docs/browsers) for Chromium. + +```sh +git clone https://github.com/fullbleed-engine/fullbleed-official.git +cd fullbleed-official +git checkout 495850aae161826a9244cc61cd5e98302f2736a3 +python3 -m venv .venv +.venv/bin/python -m pip install -r benchmarks/pdf_renderers/requirements.txt +.venv/bin/python -m playwright install chromium +.venv/bin/python benchmarks/pdf_renderers/compare.py --out target/my-comparison +``` + +The merged revision above contains the same benchmark files as the measured source snapshot. Run on a local Linux filesystem. `--smoke` exercises output qualification with fewer samples; it is not a performance run. Use the [recorded package list](../assets/renderer-comparison/packages.txt) as pip constraints (`-c packages.txt`) to reproduce the measured transitive dependency versions. Change the fixtures and acceptance contract to match your real documents before treating a result as relevant to your application. + +Fullbleed is a print-document engine. A browser remains relevant when JavaScript or live browser layout is the required behavior. See [Choose a PDF library](comparison.md) for that distinction and [CSS coverage](../css-coverage.md) for Fullbleed's tested surface. Three small fixtures on one host cannot establish a universal speed, memory, or quality ranking. diff --git a/mkdocs.yml b/mkdocs.yml index 3613df0..7f4ee6e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -101,6 +101,7 @@ nav: - Engine architecture: engine/overview.md - Evaluate: - Choose a PDF library: guides/comparison.md + - Three-renderer comparison: guides/renderer-comparison.md - Performance evidence: guides/performance.md - FAQ: faq.md - Contribute and support: support.md diff --git a/tools/plot_renderer_comparison.py b/tools/plot_renderer_comparison.py new file mode 100644 index 0000000..e9e7fa3 --- /dev/null +++ b/tools/plot_renderer_comparison.py @@ -0,0 +1,55 @@ +"""Rebuild the comparison figure from the published raw summary (matplotlib 3.8.4).""" +import json +from pathlib import Path + +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt + +ROOT = Path(__file__).resolve().parents[1] +ASSETS = ROOT / 'docs/assets/renderer-comparison' + + +def main(): + values = json.loads((ASSETS / 'summary.json').read_text())['results'] + entries = {(r['engine'], r['fixture']): r for r in values} + engines = ['fullbleed', 'weasyprint', 'chromium'] + labels = ['Fullbleed', 'WeasyPrint', 'Chromium'] + colors = ['#126c67', '#af523d', '#465e8e'] + plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 10, + 'svg.hashsalt': 'fullbleed-renderer-comparison-20261005'}) + fig, axes = plt.subplots(1, 2, figsize=(12, 5.8), constrained_layout=False) + for ax, key, title, limit in zip(axes, ['cold_ms', 'warm_ms'], + ['Fresh process · 5 samples', 'Warm process · 30 samples'], [1500, 440]): + for group, fixture in enumerate(['invoice', 'ledger', 'report']): + for i, (engine, color) in enumerate(zip(engines, colors)): + item = entries[engine, fixture][key] + y = group * 4 + i + ax.barh(y, item['median'], height=.58, color=color, zorder=2) + ax.errorbar(item['median'], y, + xerr=[[item['median'] - item['min']], [item['max'] - item['median']]], + color='#243542', linewidth=.85, capsize=2, zorder=3) + ax.text(item['max'] + limit * .025, y, f"{item['median']:,.1f}", + va='center', fontsize=9, color='#172b3b') + ax.set_yticks([1, 5, 9], ['Invoice', 'Ledger', 'Report']) + ax.invert_yaxis() + ax.set_xlim(0, limit) + ax.set_xlabel('Milliseconds per complete PDF (lower is faster)', labelpad=10) + ax.set_title(title, loc='left', fontsize=12, pad=16) + ax.grid(axis='x', color='#e2e8eb', zorder=0) + ax.tick_params(axis='both', length=0, pad=7) + for spine in ax.spines.values(): + spine.set_visible(False) + fig.suptitle('Same static documents. Different performance tradeoffs.', + x=.065, y=.98, ha='left', fontsize=17, color='#172b3b') + handles = [plt.Rectangle((0, 0), 1, 1, color=color) for color in colors] + fig.legend(handles, labels, loc='upper left', bbox_to_anchor=(.06, .925), ncol=3, frameon=False) + fig.text(.065, .045, 'Bars: medians. Whiskers: full observed ranges. Maintainer-run on one WSL2 host, 2026-10-05.', fontsize=9) + fig.text(.065, .014, 'Ledger: Fullbleed/WeasyPrint 3 pages; Chromium 4. Versions, PDFs, and raw data: docs.fullbleed.dev/guides/renderer-comparison/', fontsize=8) + fig.subplots_adjust(left=.08, right=.98, top=.77, bottom=.17, wspace=.28) + for suffix in ['svg', 'png']: + fig.savefig(ASSETS / f'latency.{suffix}', dpi=160, metadata={'Date': None} if suffix == 'svg' else {}) + + +if __name__ == '__main__': + main() diff --git a/tools/verify_renderer_comparison.py b/tools/verify_renderer_comparison.py new file mode 100644 index 0000000..81fbb4b --- /dev/null +++ b/tools/verify_renderer_comparison.py @@ -0,0 +1,132 @@ +"""Check downloadable evidence, recompute statistics, and re-read the nine PDFs.""" +from collections import defaultdict +import hashlib +import importlib.util +import json +from pathlib import Path +import statistics +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +ASSETS = ROOT / 'docs/assets/renderer-comparison' +OUT = ROOT / 'output/renderer-comparison-verification' + + +def sha(data): + return hashlib.sha256(data).hexdigest() + + +def stats(values): + q = statistics.quantiles(values, n=4, method='inclusive') + return {'n': len(values), 'median': statistics.median(values), 'min': min(values), + 'max': max(values), 'q1': q[0], 'q3': q[2]} + + +def main(): + OUT.mkdir(parents=True, exist_ok=False) + manifest = json.loads((ASSETS / 'manifest.json').read_text()) + for item in manifest['files']: + data = (ASSETS / item['path']).read_bytes() + assert len(data) == item['bytes'] and sha(data) == item['sha256'], item['path'] + archive_counts = {} + for item in manifest['archives']: + with zipfile.ZipFile(ASSETS / item['file']) as z: + assert z.testzip() is None + listing = json.loads(z.read('MANIFEST.json')) + assert listing['sourceCommit'] == manifest['sourceCommit'] + assert len(listing['files']) == item['members'] + assert set(z.namelist()) == {'MANIFEST.json', *(f['path'] for f in listing['files'])} + for member in listing['files']: + data = z.read(member['path']) + assert len(data) == member['bytes'] and sha(data) == member['sha256'], member['path'] + archive_counts[item['file']] = len(listing['files']) + if item['file'] == 'measured-evidence.zip': + for name in z.namelist(): + assert (OUT / name).resolve().is_relative_to(OUT.resolve()) + z.extractall(OUT) + measured = OUT / 'measured' + environment = json.loads((measured / 'environment.json').read_text()) + assert environment['source_revision'] == manifest['sourceCommit'] + for name, digest in environment['source_sha256'].items(): + assert sha((measured / 'source' / name).read_bytes()) == digest + summary = json.loads((measured / 'summary.json').read_text()) + assert summary == json.loads((ASSETS / 'summary.json').read_text()) + assert summary['all_qualified'] and not summary['smoke'] + jobs = json.loads((measured / 'jobs.json').read_text()) + assert len(jobs) == 81 + collected = defaultdict(lambda: {'cold': [], 'warm': [], 'bytes': [], 'pages': set()}) + representatives = {} + token_set = set() + pdf_count = 0 + for job in jobs: + assert job['exit_code'] == 0 + key = job['engine'], job['fixture'] + if job['engine'] == 'chromium': + assert job['metadata']['browser_version'] == '153.0.8010.12' + pair = collected[key] + if job['phase'] == 'cold': + pair['cold'].append(job['process_ms']) + if job['phase'] == 'memory': + samples = json.loads((measured / 'jobs' / job['job'] / 'rss-samples.json').read_text()) + pair['rss'] = max(s['rss_bytes'] for s in samples) + assert pair['rss'] == job['sampled_peak_tree_rss_bytes'] + for row in job['rows']: + assert row['token'] not in token_set + token_set.add(row['token']) + q = row['qualification'] + assert q['passed'] and not q['failures'] + data = (measured / row['path']).read_bytes() + assert sha(data) == q['sha256'] and len(data) == q['bytes'] + pair['bytes'].append(len(data)) + pair['pages'].add(q['pages']) + if job['phase'] == 'warm' and not row['warmup']: + pair['warm'].append(row['render_write_ms']) + representatives.setdefault(key, row) + pdf_count += 1 + assert pdf_count == manifest['qualifiedPdfs'] == 414 + assert sum(len(p['cold']) + len(p['warm']) for p in collected.values()) == manifest['timedPdfs'] == 315 + article = (ROOT / 'docs/guides/renderer-comparison.md').read_text(encoding='utf-8') + names = {'fullbleed': 'Fullbleed', 'weasyprint': 'WeasyPrint', 'chromium': 'Chromium'} + for result in summary['results']: + key = result['engine'], result['fixture'] + actual = collected[key] + assert len(actual['cold']) == 5 and len(actual['warm']) == 30 + for key_name in ['cold', 'warm']: + assert stats(actual[key_name]) == result[f'{key_name}_ms'] + assert stats(actual['bytes']) == result['bytes'] + assert sorted(actual['pages']) == result['pages'] + assert actual['rss'] == result['sampled_peak_tree_rss_bytes'] + cold, warm = result['cold_ms'], result['warm_ms'] + row = f"| {result['fixture'].title()} | {names[result['engine']]} | {result['pages'][0]} | " + row += f"{cold['median']:,.1f} ({cold['min']:,.1f}–{cold['max']:,.1f}) | " + row += f"{warm['median']:,.1f} ({warm['min']:,.1f}–{warm['max']:,.1f}) |" + assert row in article, row + size_row = f"| {result['fixture'].title()} | {names[result['engine']]} | {result['bytes']['median']/1024:.1f} | {actual['rss']/1024**2:.1f} |" + assert size_row in article, size_row + spec = importlib.util.spec_from_file_location('comparison_validation', measured / 'source/validate.py') + validation = importlib.util.module_from_spec(spec) + spec.loader.exec_module(validation) + page_count = 0 + for (engine, fixture), row in representatives.items(): + path = ASSETS / 'pdfs' / engine / f'{fixture}.pdf' + assert sha(path.read_bytes()) == row['qualification']['sha256'] + contract = json.loads((measured / 'inputs' / f'{fixture}.json').read_text()) + result = validation.check_pdf(path, contract, row['token']) + assert result['passed'], result + page_count += result['pages'] + assert page_count == manifest['representativePages'] == 22 + controls = json.loads((measured / 'negative-controls.json').read_text()) + for control in controls.values(): + assert control['passed'] + for name in ['wrong_token', 'missing_page', 'missing_font', 'outside_page_inset']: + assert control[name] + report = {'ok': True, 'sourceCommit': manifest['sourceCommit'], 'archives': archive_counts, + 'retainedPdfsHashed': pdf_count, 'timedSamplesRecomputed': 315, + 'representativePdfsRevalidated': len(representatives), 'representativePages': page_count, + 'publishedTableRowsVerified': 18} + (OUT / 'verification.json').write_text(json.dumps(report, indent=2) + '\n') + print(json.dumps(report, indent=2)) + + +if __name__ == '__main__': + main()