diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1b7dbd..07a268f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,10 @@ jobs: - run: python -m pip install pypdf==6.19.0 pypdfium2==5.14.0 fonttools==4.65.0 pillow==12.3.0 - run: python -m unittest discover -s test -p 'test_*.py' - run: npm run verify:text + - name: Verify the installed engine fixes against independent expectations + run: | + node tools/render-engine-regressions.mjs + python tools/verify-engine-regressions.py - name: Verify inline wrapping from the installed package run: | npm run render:inline @@ -56,7 +60,7 @@ jobs: npm run render:fonts -- --package-root output/font-baseline-consumer/node_modules/fullbleed --out output/font-baseline npm run verify:fonts -- --evidence-root output/font-baseline --allow-legacy-metadata npm run render:fonts - npm run verify:fonts -- --baseline output/font-baseline --reviewed-layout test/fixtures/inline-layout-2.5.11.json + npm run verify:fonts -- --baseline output/font-baseline --reviewed-layout test/fixtures/inline-layout-2.5.22.json - name: Verify installed custom font families against explicit faces run: | npm run render:families @@ -96,6 +100,7 @@ jobs: output/standard-baseline-consumer/package-lock.json output/standard-baseline/ output/standard-fonts/ + output/engine-regressions/ retention-days: 7 installed: @@ -136,13 +141,15 @@ jobs: run: | python -m pip install pypdf==6.19.0 pypdfium2==5.14.0 fonttools==4.65.0 pillow==12.3.0 npm run render:fonts - npm run verify:fonts -- --reviewed-layout test/fixtures/inline-layout-2.5.11.json + npm run verify:fonts -- --reviewed-layout test/fixtures/inline-layout-2.5.22.json npm run render:families npm run verify:families npm run render:inline npm run verify:inline npm run render:standard npm run verify:standard + node tools/render-engine-regressions.mjs + python tools/verify-engine-regressions.py - uses: actions/upload-artifact@v4 if: always() && matrix.os == 'windows-latest' && matrix.node == '22' with: @@ -152,6 +159,7 @@ jobs: output/font-families/ output/inline-wrapping/ output/standard-fonts/ + output/engine-regressions/ retention-days: 7 - uses: actions/upload-artifact@v4 if: always() @@ -208,6 +216,7 @@ jobs: - run: python -m playwright install --with-deps ${{ matrix.browser }} timeout-minutes: 5 - run: python tools/check-browser.py --browser ${{ matrix.browser }} --label ci + - run: python tools/verify-engine-regressions.py output/browser-${{ matrix.browser }}-ci --browser - uses: actions/upload-artifact@v4 if: always() with: diff --git a/README.md b/README.md index d01d2be..98418fd 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ also includes a ZIP for local development. Using Vue? The [Vue and TypeScript starter](examples/vue) includes editable templates, automatic previews, downloads, and a reusable `usePdfPreview` composable. -This is an optional integration around the published **Fullbleed 2.5.11** engine. -The package has its own version, **0.4.0**. Node applications can use a shared +This is an optional integration around the published **Fullbleed 2.5.22** engine. +The package has its own version, **0.4.1**. Node applications can use a shared render queue to bound active renders and waiting requests, include queue time in deadlines, cancel waiting jobs, and stop rendering cleanly during shutdown. The browser entry renders in a Web Worker, and the Node API supports worker and @@ -25,6 +25,11 @@ process isolation. See the [browser guide](docs/browser.md), [font-family checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/font-families.md), and [embedded-font checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/font-subsets.md) for the retained fixtures. +Version 0.4.1 brings the engine's recent layout fixes to both JavaScript entries. +The [engine update checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/engine-2.5.22.md) +cover nested counters, clipped effects, border-image tiling, and floated initials +with independent text, color, and Chrome print expectations. + ## Install and render Use Node.js 22 or newer. Install the [npm package](https://www.npmjs.com/package/fullbleed): @@ -33,7 +38,7 @@ Use Node.js 22 or newer. Install the [npm package](https://www.npmjs.com/package npm install fullbleed ``` -For a version-pinned installation, use `npm install --save-exact fullbleed@0.4.0`. +For a version-pinned installation, use `npm install --save-exact fullbleed@0.4.1`. Package archives and retained PDF/PNG evidence are attached to each [GitHub release](https://github.com/fullbleed-engine/fullbleed-node/releases). The [installation verification records](https://github.com/fullbleed-engine/fullbleed-node/tree/main/verification) diff --git a/docs/browser.md b/docs/browser.md index 71d8a36..2a820f2 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -1,7 +1,7 @@ # Generate PDFs in a browser -The `fullbleed/browser` entry in Fullbleed 0.3.2 renders static HTML/CSS in a -dedicated Web Worker. It uses the same pinned 2.5.11 engine and bundled fonts as +The `fullbleed/browser` entry in Fullbleed 0.4.1 renders static HTML/CSS in a +dedicated Web Worker. It uses the same pinned 2.5.22 engine and bundled fonts as the Node entry. The result is a `Uint8Array`, suitable for a PDF `Blob`, plus optional PNG previews. No PDF server or account is required. @@ -17,7 +17,7 @@ Install the package in your web project, then copy its browser files into the directory your application serves as static files: ```sh -npm install --save-exact fullbleed@0.3.2 +npm install --save-exact fullbleed@0.4.1 npx fullbleed-browser-assets public/fullbleed ``` diff --git a/docs/engine-2.5.22.md b/docs/engine-2.5.22.md new file mode 100644 index 0000000..aeb3f6b --- /dev/null +++ b/docs/engine-2.5.22.md @@ -0,0 +1,55 @@ +# Engine 2.5.22 in JavaScript + +Package 0.4.1 bundles the unmodified published Fullbleed 2.5.22 crate. Its Node +and browser entries use the same WebAssembly engine. Install it with: + +```sh +npm install --save-exact fullbleed@0.4.1 +``` + +The adapter now enables the engine's `svg_raster` feature so SVG border images +can use its native raster fallback. This adds no external rendering dependency. +The build and copied browser manifests record the enabled feature. + +The public JavaScript API and its memory limit are unchanged. Review existing +PDF baselines when upgrading: corrected layout can change document appearance. + +The [fixture provenance](../test/fixtures/engine-2.5.22.json) retains thirteen +focused cases from the engine release, including the original expected results: + +| Document behavior | Independent check | +| --- | --- | +| Numbered sections and nested counters | Extracted labels, including resets across pages | +| Filtered content inside clipped containers | Interior pixel colors in finalized previews and independently rendered PDFs | +| Border images with insufficient space or a zero edge | Raster and SVG tile colors and empty regions | +| Decorated left/right initials and pagination | Character positions and colored bounds from retained Chrome print references using identical font bytes | + +The Node fixture runner renders through worker and process isolation and checks +repeat output and equality between the two modes. The native adapter comparison +covers these same fixtures. The browser checks download the actual PDFs and +previews, compare their hashes with the installed npm package, then apply the +independent expectations to the downloads. + +The custom Noto Sans font is a licensed test asset. It is excluded from the npm +tarball and does not add an application dependency. + +From a built source checkout, with the verifier dependencies installed: + +```sh +npm run verify:pack +node tools/render-engine-regressions.mjs +python tools/verify-engine-regressions.py +npm run verify:browser:prepare +python tools/check-browser.py --browser chrome --label candidate +python tools/verify-engine-regressions.py output/browser-chrome-candidate --browser +``` + +Use a fresh output directory for each retained run. `render-engine-regressions.mjs` +accepts `--out` and `--package-root` to check a separate installed package. For +the public 0.4.0 comparison, pass `--negative-control` to the Python verifier; +it requires at least one failing case in each of the four fix families. + +These are targeted integration checks. They do not establish complete CSS +parity or PDF/UA, PDF/A, PDF/X, or PDF/VT conformance. The known runtime diagnostic +in [issue #7](https://github.com/fullbleed-engine/fullbleed-node/issues/7) remains +separate from this engine update. diff --git a/docs/font-subsets.md b/docs/font-subsets.md index e243a73..f6012c1 100644 --- a/docs/font-subsets.md +++ b/docs/font-subsets.md @@ -1,7 +1,7 @@ # Checking the installed package's embedded fonts Node package 0.1.4 introduced the Fullbleed 2.5.7 font compaction described here; -0.3.2 retains these checks with engine 2.5.11. The engine's font subsetting +0.4.1 retains these checks with engine 2.5.22. The engine's font subsetting removes unused metadata and metrics while retaining original glyph IDs and the glyph outlines, instructions, advances, character mappings, and font notices needed by the document. The JavaScript API and four bundled font faces are @@ -20,8 +20,8 @@ horizontal metrics, and header extrema. Both readers must extract the same text. The repeated PDFs must be byte-identical. In the comparison with 0.1.3, all seven PDFs must be smaller and retain their normalized text and page counts. The five font-only specimens must also retain exact text, native previews, and PDFium -page pixels. The invoice and report match the unchanged, reviewed 2.5.11 layout in -[`test/fixtures/inline-layout-2.5.11.json`](../test/fixtures/inline-layout-2.5.11.json), +page pixels. The invoice and report match the unchanged, reviewed 2.5.22 layout in +[`test/fixtures/inline-layout-2.5.22.json`](../test/fixtures/inline-layout-2.5.22.json), including PDF hashes. Their tracked labels and footers stay on one line after the inline-sizing correction. Reports explicitly record that these two layouts differ from 0.1.3; they do not report unchanged pixels. Source HTML/CSS and font @@ -37,7 +37,7 @@ npm install --prefix output/font-baseline-consumer --ignore-scripts --no-audit - npm run render:fonts -- --package-root output/font-baseline-consumer/node_modules/fullbleed --out output/font-baseline npm run verify:fonts -- --evidence-root output/font-baseline --allow-legacy-metadata npm run render:fonts -npm run verify:fonts -- --baseline output/font-baseline --reviewed-layout test/fixtures/inline-layout-2.5.11.json +npm run verify:fonts -- --baseline output/font-baseline --reviewed-layout test/fixtures/inline-layout-2.5.22.json ``` The legacy flag applies only to the old package's metadata; outlines, metrics, diff --git a/engine/Cargo.lock b/engine/Cargo.lock index c56cf6e..c691995 100644 --- a/engine/Cargo.lock +++ b/engine/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "fullbleed" -version = "2.5.11" +version = "2.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a672e0401fb42234b2e5a619adbe3a431db646780bf3398a44c70d8855c63270" +checksum = "103bc98750b704511b10460a91d6a05e8138648eaecac6c7dc96415aab75b455" dependencies = [ "fullbleed_audit_contract", "subsetter", @@ -15,7 +15,7 @@ dependencies = [ [[package]] name = "fullbleed-node-adapter" -version = "0.4.0" +version = "0.4.1" dependencies = [ "fullbleed", ] diff --git a/engine/Cargo.toml b/engine/Cargo.toml index b39a2d7..f584829 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "fullbleed-node-adapter" -version = "0.4.0" +version = "0.4.1" edition = "2024" license = "MIT" publish = false [dependencies] -fullbleed = "=2.5.11" +fullbleed = { version = "=2.5.22", features = ["svg_raster"] } [profile.release] lto = true diff --git a/package-lock.json b/package-lock.json index 03f2574..556a5c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fullbleed", - "version": "0.4.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fullbleed", - "version": "0.4.0", + "version": "0.4.1", "license": "MIT", "dependencies": { "@bjorn3/browser_wasi_shim": "0.4.2" diff --git a/package.json b/package.json index f89ad72..22571c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fullbleed", - "version": "0.4.0", + "version": "0.4.1", "description": "Generate PDFs from HTML and CSS in Node.js or a browser worker with the Fullbleed Rust engine, bundled fonts, and optional PNG previews.", "type": "module", "main": "./src/index.cjs", @@ -77,7 +77,7 @@ "access": "public" }, "fullbleed": { - "engineVersion": "2.5.11" + "engineVersion": "2.5.22" }, "devDependencies": { "@types/node": "22.20.5", diff --git a/src/copy-browser-assets.cjs b/src/copy-browser-assets.cjs index dd7bce1..67193fd 100644 --- a/src/copy-browser-assets.cjs +++ b/src/copy-browser-assets.cjs @@ -35,7 +35,7 @@ async function main() { await writeFile(path, file.data); } await writeFile(join(destination, 'build.json'), JSON.stringify({ schema: manifest.schema, - packageVersion: pkg.version, engineVersion: manifest.engineVersion, + packageVersion: pkg.version, engineVersion: manifest.engineVersion, engineFeatures: manifest.engineFeatures, files: Object.fromEntries(files.map(({ name, bytes, sha256 }) => [name, { bytes, sha256 }])) }, null, 2) + '\n'); console.log(JSON.stringify({ ok: true, directory: destination, packageVersion: pkg.version, engineVersion: manifest.engineVersion, files: files.length + 1, bytes: files.reduce((sum, file) => sum + file.bytes, 0) })); diff --git a/test/engine-regressions.mjs b/test/engine-regressions.mjs new file mode 100644 index 0000000..8bf4656 --- /dev/null +++ b/test/engine-regressions.mjs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +import { readFile } from 'node:fs/promises'; + +export const engineRegressionSource = JSON.parse(await readFile(new URL('./fixtures/engine-2.5.22.json', import.meta.url), 'utf8')); +export const engineRegressionFixtures = engineRegressionSource.cases.map(({ name, pages, html, css, previewDpi, fontFiles = [] }) => + ({ name, pages, html, css, previewDpi, fontFiles })); + +export async function loadEngineRegressionFixtures() { + return Promise.all(engineRegressionFixtures.map(async ({ fontFiles, ...fixture }) => ({ ...fixture, + fonts: await Promise.all(fontFiles.map(name => readFile(new URL('./fonts/' + name, import.meta.url)))), + }))); +} diff --git a/test/fixtures/engine-2.5.22.json b/test/fixtures/engine-2.5.22.json new file mode 100644 index 0000000..4ef19ee --- /dev/null +++ b/test/fixtures/engine-2.5.22.json @@ -0,0 +1,6052 @@ +{ + "schema": "fullbleed.javascript.engine-regressions.v1", + "upstreamRelease": "2.5.22", + "upstreamCommit": "e41c1b4b4395c9bd3c024935c926ba46cb791440", + "sources": [ + { + "path": "tests/fixtures/first-letter-floats/chrome.json", + "sha256": "08052b68e932fe981520ea53463360dde0faf72e63b0a4498e218708b77604ee", + "url": "https://github.com/fullbleed-engine/fullbleed-official/blob/e41c1b4b4395c9bd3c024935c926ba46cb791440/tests/fixtures/first-letter-floats/chrome.json" + }, + { + "path": "tools/smoke_counter_scopes.py", + "sha256": "19fd1b5b196b53992fa301caa5ed8dd874acd1e539987707d64478a9f82324f4", + "url": "https://github.com/fullbleed-engine/fullbleed-official/blob/e41c1b4b4395c9bd3c024935c926ba46cb791440/tools/smoke_counter_scopes.py" + }, + { + "path": "tools/smoke_filtered_overflow.py", + "sha256": "411f6251170eb17a1bc2a97565d96b5066270605576223c1fee8977928f9f487", + "url": "https://github.com/fullbleed-engine/fullbleed-official/blob/e41c1b4b4395c9bd3c024935c926ba46cb791440/tools/smoke_filtered_overflow.py" + }, + { + "path": "tools/smoke_border_image_tiling.py", + "sha256": "0f5b3c035fba92318f8b7dfc160741494f1be62bfa441b3cd7d3fc48d32e6340", + "url": "https://github.com/fullbleed-engine/fullbleed-official/blob/e41c1b4b4395c9bd3c024935c926ba46cb791440/tools/smoke_border_image_tiling.py" + } + ], + "chromeReference": { + "browser": "Chrome 154.0.8037.98", + "date": "2026-10-09", + "media": "print", + "method": "Fresh print PDFs, explicit identical font, independent PDFium character ink and color-region bounds." + }, + "font": { + "file": "NotoSans-Regular.ttf", + "sha256": "bfb7bb691513f12e734dc346c03a03f784912432d7e3fa8e56efcf906fe86b3d", + "source": "https://github.com/fullbleed-engine/fullbleed-official/blob/e41c1b4b4395c9bd3c024935c926ba46cb791440/python/fullbleed_assets/fonts/NotoSans-Regular.ttf", + "license": "NotoSans-OFL.txt", + "licenseSha256": "f8a660d97cc1640e808490fdbb7f1c2b27ba7936146850a59e76ef4075ede435", + "licenseSource": "https://github.com/fullbleed-engine/fullbleed-official/blob/e41c1b4b4395c9bd3c024935c926ba46cb791440/src/preview_fonts/LICENSE-NotoSans.txt", + "scope": "Test-only explicit font; excluded from npm tarball." + }, + "cases": [ + { + "name": "engine-first-letter-decorated-left", + "family": "first-letter", + "html": "

Drop cap leads this short paragraph while the remaining text wraps beside the enlarged initial letter.

", + "css": "@page{size:432px 208px;margin:0}*{margin:0;box-sizing:border-box}p{margin:24px;width:300px;font-family:\"Noto Sans\";font-size:20px;line-height:1.35;color:#283618;}p::first-letter{color:#bc6c25;float:left;font-size:44px;line-height:1;padding:3px 7px;border:2px solid #e04020;background:#ffe080;margin:2px 5px 4px 3px;}", + "pages": 1, + "previewDpi": 150, + "fontFiles": [ + "NotoSans-Regular.ttf" + ], + "expected": { + "chromePages": [ + { + "size": [ + 324.0, + 156.0 + ], + "chars": [ + [ + "D", + 30.201, + 103.5, + 49.077, + 127.062 + ], + [ + "r", + 62.869, + 122.25, + 67.564, + 130.44 + ], + [ + "o", + 68.314, + 122.1, + 75.754, + 130.44 + ], + [ + "p", + 77.839, + 118.65, + 84.964, + 130.44 + ], + [ + "c", + 90.514, + 122.1, + 96.394, + 130.44 + ], + [ + "a", + 97.579, + 122.1, + 104.089, + 130.425 + ], + [ + "p", + 106.579, + 118.65, + 113.704, + 130.44 + ], + [ + "l", + 119.704, + 122.25, + 121.024, + 133.65 + ], + [ + "e", + 123.124, + 122.1, + 129.994, + 130.44 + ], + [ + "a", + 131.449, + 122.1, + 137.959, + 130.425 + ], + [ + "d", + 139.999, + 122.1, + 147.124, + 133.65 + ], + [ + "s", + 149.164, + 122.1, + 154.909, + 130.44 + ], + [ + "t", + 159.724, + 122.1, + 164.569, + 132.135 + ], + [ + "h", + 166.174, + 122.25, + 172.954, + 133.65 + ], + [ + "i", + 175.339, + 122.25, + 176.884, + 133.305 + ], + [ + "s", + 178.804, + 122.1, + 184.549, + 130.44 + ], + [ + "s", + 189.889, + 122.1, + 195.634, + 130.44 + ], + [ + "h", + 197.584, + 122.25, + 204.364, + 133.65 + ], + [ + "o", + 206.404, + 122.1, + 213.844, + 130.44 + ], + [ + "r", + 215.929, + 122.25, + 220.624, + 130.44 + ], + [ + "t", + 221.089, + 122.1, + 225.934, + 132.135 + ], + [ + "p", + 62.869, + 98.4, + 69.994, + 110.19 + ], + [ + "a", + 71.509, + 101.85, + 78.019, + 110.175 + ], + [ + "r", + 80.509, + 102.0, + 85.204, + 110.19 + ], + [ + "a", + 85.819, + 101.85, + 92.329, + 110.175 + ], + [ + "g", + 94.369, + 98.4, + 101.494, + 110.19 + ], + [ + "r", + 104.044, + 102.0, + 108.739, + 110.19 + ], + [ + "a", + 109.354, + 101.85, + 115.864, + 110.175 + ], + [ + "p", + 118.354, + 98.4, + 125.479, + 110.19 + ], + [ + "h", + 127.579, + 102.0, + 134.359, + 113.4 + ], + [ + "w", + 139.639, + 102.015, + 151.099, + 110.055 + ], + [ + "h", + 152.539, + 102.0, + 159.319, + 113.4 + ], + [ + "i", + 161.704, + 102.0, + 163.249, + 113.055 + ], + [ + "l", + 165.679, + 102.0, + 166.999, + 113.4 + ], + [ + "e", + 169.099, + 101.85, + 175.969, + 110.19 + ], + [ + "t", + 180.874, + 101.85, + 185.719, + 111.885 + ], + [ + "h", + 187.324, + 102.0, + 194.104, + 113.4 + ], + [ + "e", + 196.144, + 101.85, + 203.014, + 110.19 + ], + [ + "r", + 62.869, + 81.75, + 67.564, + 89.94 + ], + [ + "e", + 68.314, + 81.6, + 75.184, + 89.94 + ], + [ + "m", + 77.224, + 81.75, + 88.759, + 89.94 + ], + [ + "a", + 90.664, + 81.6, + 97.174, + 89.925 + ], + [ + "i", + 99.559, + 81.75, + 101.104, + 92.805 + ], + [ + "n", + 103.534, + 81.75, + 110.314, + 89.94 + ], + [ + "i", + 112.699, + 81.75, + 114.244, + 92.805 + ], + [ + "n", + 116.674, + 81.75, + 123.454, + 89.94 + ], + [ + "g", + 125.494, + 78.15, + 132.619, + 89.94 + ], + [ + "t", + 138.034, + 81.6, + 142.879, + 91.635 + ], + [ + "e", + 144.034, + 81.6, + 150.904, + 89.94 + ], + [ + "x", + 151.639, + 81.75, + 159.034, + 89.79 + ], + [ + "t", + 159.544, + 81.6, + 164.389, + 91.635 + ], + [ + "w", + 168.784, + 81.765, + 180.244, + 89.805 + ], + [ + "r", + 181.684, + 81.75, + 186.379, + 89.94 + ], + [ + "a", + 186.994, + 81.6, + 193.504, + 89.925 + ], + [ + "p", + 195.994, + 78.15, + 203.119, + 89.94 + ], + [ + "s", + 204.709, + 81.6, + 210.454, + 89.94 + ], + [ + "b", + 19.275, + 61.35, + 26.4, + 72.9 + ], + [ + "e", + 28.05, + 61.35, + 34.92, + 69.69 + ], + [ + "s", + 36.45, + 61.35, + 42.195, + 69.69 + ], + [ + "i", + 44.04, + 61.5, + 45.585, + 72.555 + ], + [ + "d", + 47.565, + 61.35, + 54.69, + 72.9 + ], + [ + "e", + 56.79, + 61.35, + 63.66, + 69.69 + ], + [ + "t", + 68.565, + 61.35, + 73.41, + 71.385 + ], + [ + "h", + 75.015, + 61.5, + 81.795, + 72.9 + ], + [ + "e", + 83.835, + 61.35, + 90.705, + 69.69 + ], + [ + "e", + 96.195, + 61.35, + 103.065, + 69.69 + ], + [ + "n", + 105.105, + 61.5, + 111.885, + 69.69 + ], + [ + "l", + 114.375, + 61.5, + 115.695, + 72.9 + ], + [ + "a", + 117.66, + 61.35, + 124.17, + 69.675 + ], + [ + "r", + 126.66, + 61.5, + 131.355, + 69.69 + ], + [ + "g", + 132.255, + 57.9, + 139.38, + 69.69 + ], + [ + "e", + 141.48, + 61.35, + 148.35, + 69.69 + ], + [ + "d", + 149.94, + 61.35, + 157.065, + 72.9 + ], + [ + "i", + 163.41, + 61.5, + 164.955, + 72.555 + ], + [ + "n", + 167.385, + 61.5, + 174.165, + 69.69 + ], + [ + "i", + 176.55, + 61.5, + 178.095, + 72.555 + ], + [ + "t", + 179.49, + 61.35, + 184.335, + 71.385 + ], + [ + "i", + 185.835, + 61.5, + 187.38, + 72.555 + ], + [ + "a", + 189.225, + 61.35, + 195.735, + 69.675 + ], + [ + "l", + 198.225, + 61.5, + 199.545, + 72.9 + ], + [ + "l", + 19.275, + 41.25, + 20.595, + 52.65 + ], + [ + "e", + 22.695, + 41.1, + 29.565, + 49.44 + ], + [ + "t", + 30.57, + 41.1, + 35.415, + 51.135 + ], + [ + "t", + 35.985, + 41.1, + 40.83, + 51.135 + ], + [ + "e", + 41.985, + 41.1, + 48.855, + 49.44 + ], + [ + "r", + 50.895, + 41.25, + 55.59, + 49.44 + ], + [ + ".", + 54.495, + 41.04, + 56.355, + 43.065 + ] + ], + "colorBoundsPt": { + "initial": [ + 30.24, + 29.28, + 48.96, + 52.32 + ], + "border": [ + 20.64, + 19.68, + 57.6, + 60.0 + ], + "background": [ + 22.56, + 21.6, + 55.68, + 58.08 + ] + } + } + ], + "tolerancePt": 0.75 + } + }, + { + "name": "engine-first-letter-decorated-right", + "family": "first-letter", + "html": "

Drop cap leads this short paragraph while the remaining text wraps beside the enlarged initial letter.

", + "css": "@page{size:432px 208px;margin:0}*{margin:0;box-sizing:border-box}p{margin:24px;width:300px;font-family:\"Noto Sans\";font-size:20px;line-height:1.35;color:#283618;}p::first-letter{color:#bc6c25;float:right;font-size:44px;line-height:1;padding:3px 7px;border:2px solid #e04020;background:#ffe080;margin:2px 5px 4px 3px;}", + "pages": 1, + "previewDpi": 150, + "fontFiles": [ + "NotoSans-Regular.ttf" + ], + "expected": { + "chromePages": [ + { + "size": [ + 324.0, + 156.0 + ], + "chars": [ + [ + "D", + 211.607, + 103.5, + 230.483, + 127.062 + ], + [ + "r", + 19.275, + 122.25, + 23.97, + 130.44 + ], + [ + "o", + 24.72, + 122.1, + 32.16, + 130.44 + ], + [ + "p", + 34.245, + 118.65, + 41.37, + 130.44 + ], + [ + "c", + 46.92, + 122.1, + 52.8, + 130.44 + ], + [ + "a", + 53.985, + 122.1, + 60.495, + 130.425 + ], + [ + "p", + 62.985, + 118.65, + 70.11, + 130.44 + ], + [ + "l", + 76.11, + 122.25, + 77.43, + 133.65 + ], + [ + "e", + 79.53, + 122.1, + 86.4, + 130.44 + ], + [ + "a", + 87.855, + 122.1, + 94.365, + 130.425 + ], + [ + "d", + 96.405, + 122.1, + 103.53, + 133.65 + ], + [ + "s", + 105.57, + 122.1, + 111.315, + 130.44 + ], + [ + "t", + 116.13, + 122.1, + 120.975, + 132.135 + ], + [ + "h", + 122.58, + 122.25, + 129.36, + 133.65 + ], + [ + "i", + 131.745, + 122.25, + 133.29, + 133.305 + ], + [ + "s", + 135.21, + 122.1, + 140.955, + 130.44 + ], + [ + "s", + 146.295, + 122.1, + 152.04, + 130.44 + ], + [ + "h", + 153.99, + 122.25, + 160.77, + 133.65 + ], + [ + "o", + 162.81, + 122.1, + 170.25, + 130.44 + ], + [ + "r", + 172.335, + 122.25, + 177.03, + 130.44 + ], + [ + "t", + 177.495, + 122.1, + 182.34, + 132.135 + ], + [ + "p", + 19.275, + 98.4, + 26.4, + 110.19 + ], + [ + "a", + 27.915, + 101.85, + 34.425, + 110.175 + ], + [ + "r", + 36.915, + 102.0, + 41.61, + 110.19 + ], + [ + "a", + 42.225, + 101.85, + 48.735, + 110.175 + ], + [ + "g", + 50.775, + 98.4, + 57.9, + 110.19 + ], + [ + "r", + 60.45, + 102.0, + 65.145, + 110.19 + ], + [ + "a", + 65.76, + 101.85, + 72.27, + 110.175 + ], + [ + "p", + 74.76, + 98.4, + 81.885, + 110.19 + ], + [ + "h", + 83.985, + 102.0, + 90.765, + 113.4 + ], + [ + "w", + 96.045, + 102.015, + 107.505, + 110.055 + ], + [ + "h", + 108.945, + 102.0, + 115.725, + 113.4 + ], + [ + "i", + 118.11, + 102.0, + 119.655, + 113.055 + ], + [ + "l", + 122.085, + 102.0, + 123.405, + 113.4 + ], + [ + "e", + 125.505, + 101.85, + 132.375, + 110.19 + ], + [ + "t", + 137.28, + 101.85, + 142.125, + 111.885 + ], + [ + "h", + 143.73, + 102.0, + 150.51, + 113.4 + ], + [ + "e", + 152.55, + 101.85, + 159.42, + 110.19 + ], + [ + "r", + 19.275, + 81.75, + 23.97, + 89.94 + ], + [ + "e", + 24.72, + 81.6, + 31.59, + 89.94 + ], + [ + "m", + 33.63, + 81.75, + 45.165, + 89.94 + ], + [ + "a", + 47.07, + 81.6, + 53.58, + 89.925 + ], + [ + "i", + 55.965, + 81.75, + 57.51, + 92.805 + ], + [ + "n", + 59.94, + 81.75, + 66.72, + 89.94 + ], + [ + "i", + 69.105, + 81.75, + 70.65, + 92.805 + ], + [ + "n", + 73.08, + 81.75, + 79.86, + 89.94 + ], + [ + "g", + 81.9, + 78.15, + 89.025, + 89.94 + ], + [ + "t", + 94.44, + 81.6, + 99.285, + 91.635 + ], + [ + "e", + 100.44, + 81.6, + 107.31, + 89.94 + ], + [ + "x", + 108.045, + 81.75, + 115.44, + 89.79 + ], + [ + "t", + 115.95, + 81.6, + 120.795, + 91.635 + ], + [ + "w", + 125.19, + 81.765, + 136.65, + 89.805 + ], + [ + "r", + 138.09, + 81.75, + 142.785, + 89.94 + ], + [ + "a", + 143.4, + 81.6, + 149.91, + 89.925 + ], + [ + "p", + 152.4, + 78.15, + 159.525, + 89.94 + ], + [ + "s", + 161.115, + 81.6, + 166.86, + 89.94 + ], + [ + "b", + 19.275, + 61.35, + 26.4, + 72.9 + ], + [ + "e", + 28.05, + 61.35, + 34.92, + 69.69 + ], + [ + "s", + 36.45, + 61.35, + 42.195, + 69.69 + ], + [ + "i", + 44.04, + 61.5, + 45.585, + 72.555 + ], + [ + "d", + 47.565, + 61.35, + 54.69, + 72.9 + ], + [ + "e", + 56.79, + 61.35, + 63.66, + 69.69 + ], + [ + "t", + 68.565, + 61.35, + 73.41, + 71.385 + ], + [ + "h", + 75.015, + 61.5, + 81.795, + 72.9 + ], + [ + "e", + 83.835, + 61.35, + 90.705, + 69.69 + ], + [ + "e", + 96.195, + 61.35, + 103.065, + 69.69 + ], + [ + "n", + 105.105, + 61.5, + 111.885, + 69.69 + ], + [ + "l", + 114.375, + 61.5, + 115.695, + 72.9 + ], + [ + "a", + 117.66, + 61.35, + 124.17, + 69.675 + ], + [ + "r", + 126.66, + 61.5, + 131.355, + 69.69 + ], + [ + "g", + 132.255, + 57.9, + 139.38, + 69.69 + ], + [ + "e", + 141.48, + 61.35, + 148.35, + 69.69 + ], + [ + "d", + 149.94, + 61.35, + 157.065, + 72.9 + ], + [ + "i", + 163.41, + 61.5, + 164.955, + 72.555 + ], + [ + "n", + 167.385, + 61.5, + 174.165, + 69.69 + ], + [ + "i", + 176.55, + 61.5, + 178.095, + 72.555 + ], + [ + "t", + 179.49, + 61.35, + 184.335, + 71.385 + ], + [ + "i", + 185.835, + 61.5, + 187.38, + 72.555 + ], + [ + "a", + 189.225, + 61.35, + 195.735, + 69.675 + ], + [ + "l", + 198.225, + 61.5, + 199.545, + 72.9 + ], + [ + "l", + 19.275, + 41.25, + 20.595, + 52.65 + ], + [ + "e", + 22.695, + 41.1, + 29.565, + 49.44 + ], + [ + "t", + 30.57, + 41.1, + 35.415, + 51.135 + ], + [ + "t", + 35.985, + 41.1, + 40.83, + 51.135 + ], + [ + "e", + 41.985, + 41.1, + 48.855, + 49.44 + ], + [ + "r", + 50.895, + 41.25, + 55.59, + 49.44 + ], + [ + ".", + 54.495, + 41.04, + 56.355, + 43.065 + ] + ], + "colorBoundsPt": { + "initial": [ + 211.68, + 29.28, + 230.4, + 52.32 + ], + "border": [ + 202.08, + 19.68, + 239.04, + 60.0 + ], + "background": [ + 204.0, + 21.6, + 237.12, + 58.08 + ] + } + } + ], + "tolerancePt": 0.75 + } + }, + { + "name": "engine-first-letter-pagination", + "family": "first-letter", + "html": "

Drop cap leads this short paragraph while the remaining text wraps beside the enlarged initial letter.Drop cap leads this short paragraph while the remaining text wraps beside the enlarged initial letter.Drop cap leads this short paragraph while the remaining text wraps beside the enlarged initial letter.Drop cap leads this short paragraph while the remaining text wraps beside the enlarged initial letter.Drop cap leads this short paragraph while the remaining text wraps beside the enlarged initial letter.Drop cap leads this short paragraph while the remaining text wraps beside the enlarged initial letter.Drop cap leads this short paragraph while the remaining text wraps beside the enlarged initial letter.

", + "css": "@page{size:432px 208px;margin:0}*{margin:0;box-sizing:border-box}p{margin:24px;width:300px;font-family:\"Noto Sans\";font-size:20px;line-height:1.35;color:#283618;}p::first-letter{color:#bc6c25;float:right;font-size:56px;line-height:1;padding-left:6px;}", + "pages": 4, + "previewDpi": 150, + "fontFiles": [ + "NotoSans-Regular.ttf" + ], + "expected": { + "chromePages": [ + { + "size": [ + 324.0, + 156.0 + ], + "chars": [ + [ + "D", + 216.406, + 100.5, + 240.43, + 130.488 + ], + [ + "r", + 19.275, + 122.25, + 23.97, + 130.44 + ], + [ + "o", + 24.72, + 122.1, + 32.16, + 130.44 + ], + [ + "p", + 34.245, + 118.65, + 41.37, + 130.44 + ], + [ + "c", + 46.92, + 122.1, + 52.8, + 130.44 + ], + [ + "a", + 53.985, + 122.1, + 60.495, + 130.425 + ], + [ + "p", + 62.985, + 118.65, + 70.11, + 130.44 + ], + [ + "l", + 76.11, + 122.25, + 77.43, + 133.65 + ], + [ + "e", + 79.53, + 122.1, + 86.4, + 130.44 + ], + [ + "a", + 87.855, + 122.1, + 94.365, + 130.425 + ], + [ + "d", + 96.405, + 122.1, + 103.53, + 133.65 + ], + [ + "s", + 105.57, + 122.1, + 111.315, + 130.44 + ], + [ + "t", + 116.13, + 122.1, + 120.975, + 132.135 + ], + [ + "h", + 122.58, + 122.25, + 129.36, + 133.65 + ], + [ + "i", + 131.745, + 122.25, + 133.29, + 133.305 + ], + [ + "s", + 135.21, + 122.1, + 140.955, + 130.44 + ], + [ + "s", + 146.295, + 122.1, + 152.04, + 130.44 + ], + [ + "h", + 153.99, + 122.25, + 160.77, + 133.65 + ], + [ + "o", + 162.81, + 122.1, + 170.25, + 130.44 + ], + [ + "r", + 172.335, + 122.25, + 177.03, + 130.44 + ], + [ + "t", + 177.495, + 122.1, + 182.34, + 132.135 + ], + [ + "p", + 19.275, + 98.4, + 26.4, + 110.19 + ], + [ + "a", + 27.915, + 101.85, + 34.425, + 110.175 + ], + [ + "r", + 36.915, + 102.0, + 41.61, + 110.19 + ], + [ + "a", + 42.225, + 101.85, + 48.735, + 110.175 + ], + [ + "g", + 50.775, + 98.4, + 57.9, + 110.19 + ], + [ + "r", + 60.45, + 102.0, + 65.145, + 110.19 + ], + [ + "a", + 65.76, + 101.85, + 72.27, + 110.175 + ], + [ + "p", + 74.76, + 98.4, + 81.885, + 110.19 + ], + [ + "h", + 83.985, + 102.0, + 90.765, + 113.4 + ], + [ + "w", + 96.045, + 102.015, + 107.505, + 110.055 + ], + [ + "h", + 108.945, + 102.0, + 115.725, + 113.4 + ], + [ + "i", + 118.11, + 102.0, + 119.655, + 113.055 + ], + [ + "l", + 122.085, + 102.0, + 123.405, + 113.4 + ], + [ + "e", + 125.505, + 101.85, + 132.375, + 110.19 + ], + [ + "t", + 137.28, + 101.85, + 142.125, + 111.885 + ], + [ + "h", + 143.73, + 102.0, + 150.51, + 113.4 + ], + [ + "e", + 152.55, + 101.85, + 159.42, + 110.19 + ], + [ + "r", + 19.275, + 81.75, + 23.97, + 89.94 + ], + [ + "e", + 24.72, + 81.6, + 31.59, + 89.94 + ], + [ + "m", + 33.63, + 81.75, + 45.165, + 89.94 + ], + [ + "a", + 47.07, + 81.6, + 53.58, + 89.925 + ], + [ + "i", + 55.965, + 81.75, + 57.51, + 92.805 + ], + [ + "n", + 59.94, + 81.75, + 66.72, + 89.94 + ], + [ + "i", + 69.105, + 81.75, + 70.65, + 92.805 + ], + [ + "n", + 73.08, + 81.75, + 79.86, + 89.94 + ], + [ + "g", + 81.9, + 78.15, + 89.025, + 89.94 + ], + [ + "t", + 94.44, + 81.6, + 99.285, + 91.635 + ], + [ + "e", + 100.44, + 81.6, + 107.31, + 89.94 + ], + [ + "x", + 108.045, + 81.75, + 115.44, + 89.79 + ], + [ + "t", + 115.95, + 81.6, + 120.795, + 91.635 + ], + [ + "w", + 125.19, + 81.765, + 136.65, + 89.805 + ], + [ + "r", + 138.09, + 81.75, + 142.785, + 89.94 + ], + [ + "a", + 143.4, + 81.6, + 149.91, + 89.925 + ], + [ + "p", + 152.4, + 78.15, + 159.525, + 89.94 + ], + [ + "s", + 161.115, + 81.6, + 166.86, + 89.94 + ], + [ + "b", + 19.275, + 61.35, + 26.4, + 72.9 + ], + [ + "e", + 28.05, + 61.35, + 34.92, + 69.69 + ], + [ + "s", + 36.45, + 61.35, + 42.195, + 69.69 + ], + [ + "i", + 44.04, + 61.5, + 45.585, + 72.555 + ], + [ + "d", + 47.565, + 61.35, + 54.69, + 72.9 + ], + [ + "e", + 56.79, + 61.35, + 63.66, + 69.69 + ], + [ + "t", + 68.565, + 61.35, + 73.41, + 71.385 + ], + [ + "h", + 75.015, + 61.5, + 81.795, + 72.9 + ], + [ + "e", + 83.835, + 61.35, + 90.705, + 69.69 + ], + [ + "e", + 96.195, + 61.35, + 103.065, + 69.69 + ], + [ + "n", + 105.105, + 61.5, + 111.885, + 69.69 + ], + [ + "l", + 114.375, + 61.5, + 115.695, + 72.9 + ], + [ + "a", + 117.66, + 61.35, + 124.17, + 69.675 + ], + [ + "r", + 126.66, + 61.5, + 131.355, + 69.69 + ], + [ + "g", + 132.255, + 57.9, + 139.38, + 69.69 + ], + [ + "e", + 141.48, + 61.35, + 148.35, + 69.69 + ], + [ + "d", + 149.94, + 61.35, + 157.065, + 72.9 + ], + [ + "i", + 163.41, + 61.5, + 164.955, + 72.555 + ], + [ + "n", + 167.385, + 61.5, + 174.165, + 69.69 + ], + [ + "i", + 176.55, + 61.5, + 178.095, + 72.555 + ], + [ + "t", + 179.49, + 61.35, + 184.335, + 71.385 + ], + [ + "i", + 185.835, + 61.5, + 187.38, + 72.555 + ], + [ + "a", + 189.225, + 61.35, + 195.735, + 69.675 + ], + [ + "l", + 198.225, + 61.5, + 199.545, + 72.9 + ], + [ + "l", + 19.275, + 41.25, + 20.595, + 52.65 + ], + [ + "e", + 22.695, + 41.1, + 29.565, + 49.44 + ], + [ + "t", + 30.57, + 41.1, + 35.415, + 51.135 + ], + [ + "t", + 35.985, + 41.1, + 40.83, + 51.135 + ], + [ + "e", + 41.985, + 41.1, + 48.855, + 49.44 + ], + [ + "r", + 50.895, + 41.25, + 55.59, + 49.44 + ], + [ + ".", + 54.495, + 41.04, + 56.355, + 43.065 + ], + [ + "D", + 58.89, + 41.25, + 67.47, + 51.96 + ], + [ + "r", + 69.66, + 41.25, + 74.355, + 49.44 + ], + [ + "o", + 75.105, + 41.1, + 82.545, + 49.44 + ], + [ + "p", + 84.63, + 37.65, + 91.755, + 49.44 + ], + [ + "c", + 97.305, + 41.1, + 103.185, + 49.44 + ], + [ + "a", + 104.37, + 41.1, + 110.88, + 49.425 + ], + [ + "p", + 113.37, + 37.65, + 120.495, + 49.44 + ], + [ + "l", + 126.495, + 41.25, + 127.815, + 52.65 + ], + [ + "e", + 129.915, + 41.1, + 136.785, + 49.44 + ], + [ + "a", + 138.24, + 41.1, + 144.75, + 49.425 + ], + [ + "d", + 146.79, + 41.1, + 153.915, + 52.65 + ], + [ + "s", + 155.955, + 41.1, + 161.7, + 49.44 + ], + [ + "t", + 166.515, + 41.1, + 171.36, + 51.135 + ], + [ + "h", + 172.965, + 41.25, + 179.745, + 52.65 + ], + [ + "i", + 182.13, + 41.25, + 183.675, + 52.305 + ], + [ + "s", + 185.595, + 41.1, + 191.34, + 49.44 + ], + [ + "s", + 196.68, + 41.1, + 202.425, + 49.44 + ], + [ + "h", + 204.375, + 41.25, + 211.155, + 52.65 + ], + [ + "o", + 213.195, + 41.1, + 220.635, + 49.44 + ], + [ + "r", + 222.72, + 41.25, + 227.415, + 49.44 + ], + [ + "t", + 227.88, + 41.1, + 232.725, + 51.135 + ], + [ + "p", + 19.275, + 17.4, + 26.4, + 29.19 + ], + [ + "a", + 27.915, + 20.85, + 34.425, + 29.175 + ], + [ + "r", + 36.915, + 21.0, + 41.61, + 29.19 + ], + [ + "a", + 42.225, + 20.85, + 48.735, + 29.175 + ], + [ + "g", + 50.775, + 17.4, + 57.9, + 29.19 + ], + [ + "r", + 60.45, + 21.0, + 65.145, + 29.19 + ], + [ + "a", + 65.76, + 20.85, + 72.27, + 29.175 + ], + [ + "p", + 74.76, + 17.4, + 81.885, + 29.19 + ], + [ + "h", + 83.985, + 21.0, + 90.765, + 32.4 + ], + [ + "w", + 96.045, + 21.015, + 107.505, + 29.055 + ], + [ + "h", + 108.945, + 21.0, + 115.725, + 32.4 + ], + [ + "i", + 118.11, + 21.0, + 119.655, + 32.055 + ], + [ + "l", + 122.085, + 21.0, + 123.405, + 32.4 + ], + [ + "e", + 125.505, + 20.85, + 132.375, + 29.19 + ], + [ + "t", + 137.28, + 20.85, + 142.125, + 30.885 + ], + [ + "h", + 143.73, + 21.0, + 150.51, + 32.4 + ], + [ + "e", + 152.55, + 20.85, + 159.42, + 29.19 + ], + [ + "r", + 165.36, + 21.0, + 170.055, + 29.19 + ], + [ + "e", + 170.805, + 20.85, + 177.675, + 29.19 + ], + [ + "m", + 179.715, + 21.0, + 191.25, + 29.19 + ], + [ + "a", + 193.155, + 20.85, + 199.665, + 29.175 + ], + [ + "i", + 202.05, + 21.0, + 203.595, + 32.055 + ], + [ + "n", + 206.025, + 21.0, + 212.805, + 29.19 + ], + [ + "i", + 215.19, + 21.0, + 216.735, + 32.055 + ], + [ + "n", + 219.165, + 21.0, + 225.945, + 29.19 + ], + [ + "g", + 227.985, + 17.4, + 235.11, + 29.19 + ] + ], + "colorBoundsPt": { + "initial": [ + 216.48, + 25.92, + 240.48, + 55.2 + ], + "border": null, + "background": null + } + }, + { + "size": [ + 324.0, + 156.0 + ], + "chars": [ + [ + "t", + 18.24, + 140.1, + 23.085, + 150.135 + ], + [ + "e", + 24.24, + 140.1, + 31.11, + 148.44 + ], + [ + "x", + 31.845, + 140.25, + 39.24, + 148.29 + ], + [ + "t", + 39.75, + 140.1, + 44.595, + 150.135 + ], + [ + "w", + 48.99, + 140.265, + 60.45, + 148.305 + ], + [ + "r", + 61.89, + 140.25, + 66.585, + 148.44 + ], + [ + "a", + 67.2, + 140.1, + 73.71, + 148.425 + ], + [ + "p", + 76.2, + 136.65, + 83.325, + 148.44 + ], + [ + "s", + 84.915, + 140.1, + 90.66, + 148.44 + ], + [ + "b", + 96.51, + 140.1, + 103.635, + 151.65 + ], + [ + "e", + 105.285, + 140.1, + 112.155, + 148.44 + ], + [ + "s", + 113.685, + 140.1, + 119.43, + 148.44 + ], + [ + "i", + 121.275, + 140.25, + 122.82, + 151.305 + ], + [ + "d", + 124.8, + 140.1, + 131.925, + 151.65 + ], + [ + "e", + 134.025, + 140.1, + 140.895, + 148.44 + ], + [ + "t", + 145.8, + 140.1, + 150.645, + 150.135 + ], + [ + "h", + 152.25, + 140.25, + 159.03, + 151.65 + ], + [ + "e", + 161.07, + 140.1, + 167.94, + 148.44 + ], + [ + "e", + 173.43, + 140.1, + 180.3, + 148.44 + ], + [ + "n", + 182.34, + 140.25, + 189.12, + 148.44 + ], + [ + "l", + 191.61, + 140.25, + 192.93, + 151.65 + ], + [ + "a", + 194.895, + 140.1, + 201.405, + 148.425 + ], + [ + "r", + 203.895, + 140.25, + 208.59, + 148.44 + ], + [ + "g", + 209.49, + 136.65, + 216.615, + 148.44 + ], + [ + "e", + 218.715, + 140.1, + 225.585, + 148.44 + ], + [ + "d", + 227.175, + 140.1, + 234.3, + 151.65 + ], + [ + "i", + 19.17, + 120.0, + 20.715, + 131.055 + ], + [ + "n", + 23.145, + 120.0, + 29.925, + 128.19 + ], + [ + "i", + 32.31, + 120.0, + 33.855, + 131.055 + ], + [ + "t", + 35.25, + 119.85, + 40.095, + 129.885 + ], + [ + "i", + 41.595, + 120.0, + 43.14, + 131.055 + ], + [ + "a", + 44.985, + 119.85, + 51.495, + 128.175 + ], + [ + "l", + 53.985, + 120.0, + 55.305, + 131.4 + ], + [ + "l", + 61.755, + 120.0, + 63.075, + 131.4 + ], + [ + "e", + 65.175, + 119.85, + 72.045, + 128.19 + ], + [ + "t", + 73.05, + 119.85, + 77.895, + 129.885 + ], + [ + "t", + 78.465, + 119.85, + 83.31, + 129.885 + ], + [ + "e", + 84.465, + 119.85, + 91.335, + 128.19 + ], + [ + "r", + 93.375, + 120.0, + 98.07, + 128.19 + ], + [ + ".", + 96.975, + 119.79, + 98.835, + 121.815 + ], + [ + "D", + 101.37, + 120.0, + 109.95, + 130.71 + ], + [ + "r", + 112.14, + 120.0, + 116.835, + 128.19 + ], + [ + "o", + 117.585, + 119.85, + 125.025, + 128.19 + ], + [ + "p", + 127.11, + 116.4, + 134.235, + 128.19 + ], + [ + "c", + 139.785, + 119.85, + 145.665, + 128.19 + ], + [ + "a", + 146.85, + 119.85, + 153.36, + 128.175 + ], + [ + "p", + 155.85, + 116.4, + 162.975, + 128.19 + ], + [ + "l", + 168.975, + 120.0, + 170.295, + 131.4 + ], + [ + "e", + 172.395, + 119.85, + 179.265, + 128.19 + ], + [ + "a", + 180.72, + 119.85, + 187.23, + 128.175 + ], + [ + "d", + 189.27, + 119.85, + 196.395, + 131.4 + ], + [ + "s", + 198.435, + 119.85, + 204.18, + 128.19 + ], + [ + "t", + 208.995, + 119.85, + 213.84, + 129.885 + ], + [ + "h", + 215.445, + 120.0, + 222.225, + 131.4 + ], + [ + "i", + 224.61, + 120.0, + 226.155, + 131.055 + ], + [ + "s", + 228.075, + 119.85, + 233.82, + 128.19 + ], + [ + "s", + 18.765, + 99.6, + 24.51, + 107.94 + ], + [ + "h", + 26.46, + 99.75, + 33.24, + 111.15 + ], + [ + "o", + 35.28, + 99.6, + 42.72, + 107.94 + ], + [ + "r", + 44.805, + 99.75, + 49.5, + 107.94 + ], + [ + "t", + 49.965, + 99.6, + 54.81, + 109.635 + ], + [ + "p", + 60.315, + 96.15, + 67.44, + 107.94 + ], + [ + "a", + 68.955, + 99.6, + 75.465, + 107.925 + ], + [ + "r", + 77.955, + 99.75, + 82.65, + 107.94 + ], + [ + "a", + 83.265, + 99.6, + 89.775, + 107.925 + ], + [ + "g", + 91.815, + 96.15, + 98.94, + 107.94 + ], + [ + "r", + 101.49, + 99.75, + 106.185, + 107.94 + ], + [ + "a", + 106.8, + 99.6, + 113.31, + 107.925 + ], + [ + "p", + 115.8, + 96.15, + 122.925, + 107.94 + ], + [ + "h", + 125.025, + 99.75, + 131.805, + 111.15 + ], + [ + "w", + 137.085, + 99.765, + 148.545, + 107.805 + ], + [ + "h", + 149.985, + 99.75, + 156.765, + 111.15 + ], + [ + "i", + 159.15, + 99.75, + 160.695, + 110.805 + ], + [ + "l", + 163.125, + 99.75, + 164.445, + 111.15 + ], + [ + "e", + 166.545, + 99.6, + 173.415, + 107.94 + ], + [ + "t", + 178.32, + 99.6, + 183.165, + 109.635 + ], + [ + "h", + 184.77, + 99.75, + 191.55, + 111.15 + ], + [ + "e", + 193.59, + 99.6, + 200.46, + 107.94 + ], + [ + "r", + 19.275, + 79.5, + 23.97, + 87.69 + ], + [ + "e", + 24.72, + 79.35, + 31.59, + 87.69 + ], + [ + "m", + 33.63, + 79.5, + 45.165, + 87.69 + ], + [ + "a", + 47.07, + 79.35, + 53.58, + 87.675 + ], + [ + "i", + 55.965, + 79.5, + 57.51, + 90.555 + ], + [ + "n", + 59.94, + 79.5, + 66.72, + 87.69 + ], + [ + "i", + 69.105, + 79.5, + 70.65, + 90.555 + ], + [ + "n", + 73.08, + 79.5, + 79.86, + 87.69 + ], + [ + "g", + 81.9, + 75.9, + 89.025, + 87.69 + ], + [ + "t", + 94.44, + 79.35, + 99.285, + 89.385 + ], + [ + "e", + 100.44, + 79.35, + 107.31, + 87.69 + ], + [ + "x", + 108.045, + 79.5, + 115.44, + 87.54 + ], + [ + "t", + 115.95, + 79.35, + 120.795, + 89.385 + ], + [ + "w", + 125.19, + 79.515, + 136.65, + 87.555 + ], + [ + "r", + 138.09, + 79.5, + 142.785, + 87.69 + ], + [ + "a", + 143.4, + 79.35, + 149.91, + 87.675 + ], + [ + "p", + 152.4, + 75.9, + 159.525, + 87.69 + ], + [ + "s", + 161.115, + 79.35, + 166.86, + 87.69 + ], + [ + "b", + 172.71, + 79.35, + 179.835, + 90.9 + ], + [ + "e", + 181.485, + 79.35, + 188.355, + 87.69 + ], + [ + "s", + 189.885, + 79.35, + 195.63, + 87.69 + ], + [ + "i", + 197.475, + 79.5, + 199.02, + 90.555 + ], + [ + "d", + 201.0, + 79.35, + 208.125, + 90.9 + ], + [ + "e", + 210.225, + 79.35, + 217.095, + 87.69 + ], + [ + "t", + 18.24, + 59.1, + 23.085, + 69.135 + ], + [ + "h", + 24.69, + 59.25, + 31.47, + 70.65 + ], + [ + "e", + 33.51, + 59.1, + 40.38, + 67.44 + ], + [ + "e", + 45.87, + 59.1, + 52.74, + 67.44 + ], + [ + "n", + 54.78, + 59.25, + 61.56, + 67.44 + ], + [ + "l", + 64.05, + 59.25, + 65.37, + 70.65 + ], + [ + "a", + 67.335, + 59.1, + 73.845, + 67.425 + ], + [ + "r", + 76.335, + 59.25, + 81.03, + 67.44 + ], + [ + "g", + 81.93, + 55.65, + 89.055, + 67.44 + ], + [ + "e", + 91.155, + 59.1, + 98.025, + 67.44 + ], + [ + "d", + 99.615, + 59.1, + 106.74, + 70.65 + ], + [ + "i", + 113.085, + 59.25, + 114.63, + 70.305 + ], + [ + "n", + 117.06, + 59.25, + 123.84, + 67.44 + ], + [ + "i", + 126.225, + 59.25, + 127.77, + 70.305 + ], + [ + "t", + 129.165, + 59.1, + 134.01, + 69.135 + ], + [ + "i", + 135.51, + 59.25, + 137.055, + 70.305 + ], + [ + "a", + 138.9, + 59.1, + 145.41, + 67.425 + ], + [ + "l", + 147.9, + 59.25, + 149.22, + 70.65 + ], + [ + "l", + 155.67, + 59.25, + 156.99, + 70.65 + ], + [ + "e", + 159.09, + 59.1, + 165.96, + 67.44 + ], + [ + "t", + 166.965, + 59.1, + 171.81, + 69.135 + ], + [ + "t", + 172.38, + 59.1, + 177.225, + 69.135 + ], + [ + "e", + 178.38, + 59.1, + 185.25, + 67.44 + ], + [ + "r", + 187.29, + 59.25, + 191.985, + 67.44 + ], + [ + ".", + 190.89, + 59.04, + 192.75, + 61.065 + ], + [ + "D", + 195.285, + 59.25, + 203.865, + 69.96 + ], + [ + "r", + 206.055, + 59.25, + 210.75, + 67.44 + ], + [ + "o", + 211.5, + 59.1, + 218.94, + 67.44 + ], + [ + "p", + 221.025, + 55.65, + 228.15, + 67.44 + ], + [ + "c", + 18.825, + 38.85, + 24.705, + 47.19 + ], + [ + "a", + 25.89, + 38.85, + 32.4, + 47.175 + ], + [ + "p", + 34.89, + 35.4, + 42.015, + 47.19 + ], + [ + "l", + 48.015, + 39.0, + 49.335, + 50.4 + ], + [ + "e", + 51.435, + 38.85, + 58.305, + 47.19 + ], + [ + "a", + 59.76, + 38.85, + 66.27, + 47.175 + ], + [ + "d", + 68.31, + 38.85, + 75.435, + 50.4 + ], + [ + "s", + 77.475, + 38.85, + 83.22, + 47.19 + ], + [ + "t", + 88.035, + 38.85, + 92.88, + 48.885 + ], + [ + "h", + 94.485, + 39.0, + 101.265, + 50.4 + ], + [ + "i", + 103.65, + 39.0, + 105.195, + 50.055 + ], + [ + "s", + 107.115, + 38.85, + 112.86, + 47.19 + ], + [ + "s", + 118.2, + 38.85, + 123.945, + 47.19 + ], + [ + "h", + 125.895, + 39.0, + 132.675, + 50.4 + ], + [ + "o", + 134.715, + 38.85, + 142.155, + 47.19 + ], + [ + "r", + 144.24, + 39.0, + 148.935, + 47.19 + ], + [ + "t", + 149.4, + 38.85, + 154.245, + 48.885 + ], + [ + "p", + 159.75, + 35.4, + 166.875, + 47.19 + ], + [ + "a", + 168.39, + 38.85, + 174.9, + 47.175 + ], + [ + "r", + 177.39, + 39.0, + 182.085, + 47.19 + ], + [ + "a", + 182.7, + 38.85, + 189.21, + 47.175 + ], + [ + "g", + 191.25, + 35.4, + 198.375, + 47.19 + ], + [ + "r", + 200.925, + 39.0, + 205.62, + 47.19 + ], + [ + "a", + 206.235, + 38.85, + 212.745, + 47.175 + ], + [ + "p", + 215.235, + 35.4, + 222.36, + 47.19 + ], + [ + "h", + 224.46, + 39.0, + 231.24, + 50.4 + ], + [ + "w", + 18.165, + 18.765, + 29.625, + 26.805 + ], + [ + "h", + 31.065, + 18.75, + 37.845, + 30.15 + ], + [ + "i", + 40.23, + 18.75, + 41.775, + 29.805 + ], + [ + "l", + 44.205, + 18.75, + 45.525, + 30.15 + ], + [ + "e", + 47.625, + 18.6, + 54.495, + 26.94 + ], + [ + "t", + 59.4, + 18.6, + 64.245, + 28.635 + ], + [ + "h", + 65.85, + 18.75, + 72.63, + 30.15 + ], + [ + "e", + 74.67, + 18.6, + 81.54, + 26.94 + ], + [ + "r", + 87.48, + 18.75, + 92.175, + 26.94 + ], + [ + "e", + 92.925, + 18.6, + 99.795, + 26.94 + ], + [ + "m", + 101.835, + 18.75, + 113.37, + 26.94 + ], + [ + "a", + 115.275, + 18.6, + 121.785, + 26.925 + ], + [ + "i", + 124.17, + 18.75, + 125.715, + 29.805 + ], + [ + "n", + 128.145, + 18.75, + 134.925, + 26.94 + ], + [ + "i", + 137.31, + 18.75, + 138.855, + 29.805 + ], + [ + "n", + 141.285, + 18.75, + 148.065, + 26.94 + ], + [ + "g", + 150.105, + 15.15, + 157.23, + 26.94 + ], + [ + "t", + 162.645, + 18.6, + 167.49, + 28.635 + ], + [ + "e", + 168.645, + 18.6, + 175.515, + 26.94 + ], + [ + "x", + 176.25, + 18.75, + 183.645, + 26.79 + ], + [ + "t", + 184.155, + 18.6, + 189.0, + 28.635 + ], + [ + "w", + 193.395, + 18.765, + 204.855, + 26.805 + ], + [ + "r", + 206.295, + 18.75, + 210.99, + 26.94 + ], + [ + "a", + 211.605, + 18.6, + 218.115, + 26.925 + ], + [ + "p", + 220.605, + 15.15, + 227.73, + 26.94 + ], + [ + "s", + 229.32, + 18.6, + 235.065, + 26.94 + ] + ], + "colorBoundsPt": { + "initial": null, + "border": null, + "background": null + } + }, + { + "size": [ + 324.0, + 156.0 + ], + "chars": [ + [ + "b", + 19.275, + 140.1, + 26.4, + 151.65 + ], + [ + "e", + 28.05, + 140.1, + 34.92, + 148.44 + ], + [ + "s", + 36.45, + 140.1, + 42.195, + 148.44 + ], + [ + "i", + 44.04, + 140.25, + 45.585, + 151.305 + ], + [ + "d", + 47.565, + 140.1, + 54.69, + 151.65 + ], + [ + "e", + 56.79, + 140.1, + 63.66, + 148.44 + ], + [ + "t", + 68.565, + 140.1, + 73.41, + 150.135 + ], + [ + "h", + 75.015, + 140.25, + 81.795, + 151.65 + ], + [ + "e", + 83.835, + 140.1, + 90.705, + 148.44 + ], + [ + "e", + 96.195, + 140.1, + 103.065, + 148.44 + ], + [ + "n", + 105.105, + 140.25, + 111.885, + 148.44 + ], + [ + "l", + 114.375, + 140.25, + 115.695, + 151.65 + ], + [ + "a", + 117.66, + 140.1, + 124.17, + 148.425 + ], + [ + "r", + 126.66, + 140.25, + 131.355, + 148.44 + ], + [ + "g", + 132.255, + 136.65, + 139.38, + 148.44 + ], + [ + "e", + 141.48, + 140.1, + 148.35, + 148.44 + ], + [ + "d", + 149.94, + 140.1, + 157.065, + 151.65 + ], + [ + "i", + 163.41, + 140.25, + 164.955, + 151.305 + ], + [ + "n", + 167.385, + 140.25, + 174.165, + 148.44 + ], + [ + "i", + 176.55, + 140.25, + 178.095, + 151.305 + ], + [ + "t", + 179.49, + 140.1, + 184.335, + 150.135 + ], + [ + "i", + 185.835, + 140.25, + 187.38, + 151.305 + ], + [ + "a", + 189.225, + 140.1, + 195.735, + 148.425 + ], + [ + "l", + 198.225, + 140.25, + 199.545, + 151.65 + ], + [ + "l", + 19.275, + 120.0, + 20.595, + 131.4 + ], + [ + "e", + 22.695, + 119.85, + 29.565, + 128.19 + ], + [ + "t", + 30.57, + 119.85, + 35.415, + 129.885 + ], + [ + "t", + 35.985, + 119.85, + 40.83, + 129.885 + ], + [ + "e", + 41.985, + 119.85, + 48.855, + 128.19 + ], + [ + "r", + 50.895, + 120.0, + 55.59, + 128.19 + ], + [ + ".", + 54.495, + 119.79, + 56.355, + 121.815 + ], + [ + "D", + 58.89, + 120.0, + 67.47, + 130.71 + ], + [ + "r", + 69.66, + 120.0, + 74.355, + 128.19 + ], + [ + "o", + 75.105, + 119.85, + 82.545, + 128.19 + ], + [ + "p", + 84.63, + 116.4, + 91.755, + 128.19 + ], + [ + "c", + 97.305, + 119.85, + 103.185, + 128.19 + ], + [ + "a", + 104.37, + 119.85, + 110.88, + 128.175 + ], + [ + "p", + 113.37, + 116.4, + 120.495, + 128.19 + ], + [ + "l", + 126.495, + 120.0, + 127.815, + 131.4 + ], + [ + "e", + 129.915, + 119.85, + 136.785, + 128.19 + ], + [ + "a", + 138.24, + 119.85, + 144.75, + 128.175 + ], + [ + "d", + 146.79, + 119.85, + 153.915, + 131.4 + ], + [ + "s", + 155.955, + 119.85, + 161.7, + 128.19 + ], + [ + "t", + 166.515, + 119.85, + 171.36, + 129.885 + ], + [ + "h", + 172.965, + 120.0, + 179.745, + 131.4 + ], + [ + "i", + 182.13, + 120.0, + 183.675, + 131.055 + ], + [ + "s", + 185.595, + 119.85, + 191.34, + 128.19 + ], + [ + "s", + 196.68, + 119.85, + 202.425, + 128.19 + ], + [ + "h", + 204.375, + 120.0, + 211.155, + 131.4 + ], + [ + "o", + 213.195, + 119.85, + 220.635, + 128.19 + ], + [ + "r", + 222.72, + 120.0, + 227.415, + 128.19 + ], + [ + "t", + 227.88, + 119.85, + 232.725, + 129.885 + ], + [ + "p", + 19.275, + 96.15, + 26.4, + 107.94 + ], + [ + "a", + 27.915, + 99.6, + 34.425, + 107.925 + ], + [ + "r", + 36.915, + 99.75, + 41.61, + 107.94 + ], + [ + "a", + 42.225, + 99.6, + 48.735, + 107.925 + ], + [ + "g", + 50.775, + 96.15, + 57.9, + 107.94 + ], + [ + "r", + 60.45, + 99.75, + 65.145, + 107.94 + ], + [ + "a", + 65.76, + 99.6, + 72.27, + 107.925 + ], + [ + "p", + 74.76, + 96.15, + 81.885, + 107.94 + ], + [ + "h", + 83.985, + 99.75, + 90.765, + 111.15 + ], + [ + "w", + 96.045, + 99.765, + 107.505, + 107.805 + ], + [ + "h", + 108.945, + 99.75, + 115.725, + 111.15 + ], + [ + "i", + 118.11, + 99.75, + 119.655, + 110.805 + ], + [ + "l", + 122.085, + 99.75, + 123.405, + 111.15 + ], + [ + "e", + 125.505, + 99.6, + 132.375, + 107.94 + ], + [ + "t", + 137.28, + 99.6, + 142.125, + 109.635 + ], + [ + "h", + 143.73, + 99.75, + 150.51, + 111.15 + ], + [ + "e", + 152.55, + 99.6, + 159.42, + 107.94 + ], + [ + "r", + 165.36, + 99.75, + 170.055, + 107.94 + ], + [ + "e", + 170.805, + 99.6, + 177.675, + 107.94 + ], + [ + "m", + 179.715, + 99.75, + 191.25, + 107.94 + ], + [ + "a", + 193.155, + 99.6, + 199.665, + 107.925 + ], + [ + "i", + 202.05, + 99.75, + 203.595, + 110.805 + ], + [ + "n", + 206.025, + 99.75, + 212.805, + 107.94 + ], + [ + "i", + 215.19, + 99.75, + 216.735, + 110.805 + ], + [ + "n", + 219.165, + 99.75, + 225.945, + 107.94 + ], + [ + "g", + 227.985, + 96.15, + 235.11, + 107.94 + ], + [ + "t", + 18.24, + 79.35, + 23.085, + 89.385 + ], + [ + "e", + 24.24, + 79.35, + 31.11, + 87.69 + ], + [ + "x", + 31.845, + 79.5, + 39.24, + 87.54 + ], + [ + "t", + 39.75, + 79.35, + 44.595, + 89.385 + ], + [ + "w", + 48.99, + 79.515, + 60.45, + 87.555 + ], + [ + "r", + 61.89, + 79.5, + 66.585, + 87.69 + ], + [ + "a", + 67.2, + 79.35, + 73.71, + 87.675 + ], + [ + "p", + 76.2, + 75.9, + 83.325, + 87.69 + ], + [ + "s", + 84.915, + 79.35, + 90.66, + 87.69 + ], + [ + "b", + 96.51, + 79.35, + 103.635, + 90.9 + ], + [ + "e", + 105.285, + 79.35, + 112.155, + 87.69 + ], + [ + "s", + 113.685, + 79.35, + 119.43, + 87.69 + ], + [ + "i", + 121.275, + 79.5, + 122.82, + 90.555 + ], + [ + "d", + 124.8, + 79.35, + 131.925, + 90.9 + ], + [ + "e", + 134.025, + 79.35, + 140.895, + 87.69 + ], + [ + "t", + 145.8, + 79.35, + 150.645, + 89.385 + ], + [ + "h", + 152.25, + 79.5, + 159.03, + 90.9 + ], + [ + "e", + 161.07, + 79.35, + 167.94, + 87.69 + ], + [ + "e", + 173.43, + 79.35, + 180.3, + 87.69 + ], + [ + "n", + 182.34, + 79.5, + 189.12, + 87.69 + ], + [ + "l", + 191.61, + 79.5, + 192.93, + 90.9 + ], + [ + "a", + 194.895, + 79.35, + 201.405, + 87.675 + ], + [ + "r", + 203.895, + 79.5, + 208.59, + 87.69 + ], + [ + "g", + 209.49, + 75.9, + 216.615, + 87.69 + ], + [ + "e", + 218.715, + 79.35, + 225.585, + 87.69 + ], + [ + "d", + 227.175, + 79.35, + 234.3, + 90.9 + ], + [ + "i", + 19.17, + 59.25, + 20.715, + 70.305 + ], + [ + "n", + 23.145, + 59.25, + 29.925, + 67.44 + ], + [ + "i", + 32.31, + 59.25, + 33.855, + 70.305 + ], + [ + "t", + 35.25, + 59.1, + 40.095, + 69.135 + ], + [ + "i", + 41.595, + 59.25, + 43.14, + 70.305 + ], + [ + "a", + 44.985, + 59.1, + 51.495, + 67.425 + ], + [ + "l", + 53.985, + 59.25, + 55.305, + 70.65 + ], + [ + "l", + 61.755, + 59.25, + 63.075, + 70.65 + ], + [ + "e", + 65.175, + 59.1, + 72.045, + 67.44 + ], + [ + "t", + 73.05, + 59.1, + 77.895, + 69.135 + ], + [ + "t", + 78.465, + 59.1, + 83.31, + 69.135 + ], + [ + "e", + 84.465, + 59.1, + 91.335, + 67.44 + ], + [ + "r", + 93.375, + 59.25, + 98.07, + 67.44 + ], + [ + ".", + 96.975, + 59.04, + 98.835, + 61.065 + ], + [ + "D", + 101.37, + 59.25, + 109.95, + 69.96 + ], + [ + "r", + 112.14, + 59.25, + 116.835, + 67.44 + ], + [ + "o", + 117.585, + 59.1, + 125.025, + 67.44 + ], + [ + "p", + 127.11, + 55.65, + 134.235, + 67.44 + ], + [ + "c", + 139.785, + 59.1, + 145.665, + 67.44 + ], + [ + "a", + 146.85, + 59.1, + 153.36, + 67.425 + ], + [ + "p", + 155.85, + 55.65, + 162.975, + 67.44 + ], + [ + "l", + 168.975, + 59.25, + 170.295, + 70.65 + ], + [ + "e", + 172.395, + 59.1, + 179.265, + 67.44 + ], + [ + "a", + 180.72, + 59.1, + 187.23, + 67.425 + ], + [ + "d", + 189.27, + 59.1, + 196.395, + 70.65 + ], + [ + "s", + 198.435, + 59.1, + 204.18, + 67.44 + ], + [ + "t", + 208.995, + 59.1, + 213.84, + 69.135 + ], + [ + "h", + 215.445, + 59.25, + 222.225, + 70.65 + ], + [ + "i", + 224.61, + 59.25, + 226.155, + 70.305 + ], + [ + "s", + 228.075, + 59.1, + 233.82, + 67.44 + ], + [ + "s", + 18.765, + 38.85, + 24.51, + 47.19 + ], + [ + "h", + 26.46, + 39.0, + 33.24, + 50.4 + ], + [ + "o", + 35.28, + 38.85, + 42.72, + 47.19 + ], + [ + "r", + 44.805, + 39.0, + 49.5, + 47.19 + ], + [ + "t", + 49.965, + 38.85, + 54.81, + 48.885 + ], + [ + "p", + 60.315, + 35.4, + 67.44, + 47.19 + ], + [ + "a", + 68.955, + 38.85, + 75.465, + 47.175 + ], + [ + "r", + 77.955, + 39.0, + 82.65, + 47.19 + ], + [ + "a", + 83.265, + 38.85, + 89.775, + 47.175 + ], + [ + "g", + 91.815, + 35.4, + 98.94, + 47.19 + ], + [ + "r", + 101.49, + 39.0, + 106.185, + 47.19 + ], + [ + "a", + 106.8, + 38.85, + 113.31, + 47.175 + ], + [ + "p", + 115.8, + 35.4, + 122.925, + 47.19 + ], + [ + "h", + 125.025, + 39.0, + 131.805, + 50.4 + ], + [ + "w", + 137.085, + 39.015, + 148.545, + 47.055 + ], + [ + "h", + 149.985, + 39.0, + 156.765, + 50.4 + ], + [ + "i", + 159.15, + 39.0, + 160.695, + 50.055 + ], + [ + "l", + 163.125, + 39.0, + 164.445, + 50.4 + ], + [ + "e", + 166.545, + 38.85, + 173.415, + 47.19 + ], + [ + "t", + 178.32, + 38.85, + 183.165, + 48.885 + ], + [ + "h", + 184.77, + 39.0, + 191.55, + 50.4 + ], + [ + "e", + 193.59, + 38.85, + 200.46, + 47.19 + ], + [ + "r", + 19.275, + 18.75, + 23.97, + 26.94 + ], + [ + "e", + 24.72, + 18.6, + 31.59, + 26.94 + ], + [ + "m", + 33.63, + 18.75, + 45.165, + 26.94 + ], + [ + "a", + 47.07, + 18.6, + 53.58, + 26.925 + ], + [ + "i", + 55.965, + 18.75, + 57.51, + 29.805 + ], + [ + "n", + 59.94, + 18.75, + 66.72, + 26.94 + ], + [ + "i", + 69.105, + 18.75, + 70.65, + 29.805 + ], + [ + "n", + 73.08, + 18.75, + 79.86, + 26.94 + ], + [ + "g", + 81.9, + 15.15, + 89.025, + 26.94 + ], + [ + "t", + 94.44, + 18.6, + 99.285, + 28.635 + ], + [ + "e", + 100.44, + 18.6, + 107.31, + 26.94 + ], + [ + "x", + 108.045, + 18.75, + 115.44, + 26.79 + ], + [ + "t", + 115.95, + 18.6, + 120.795, + 28.635 + ], + [ + "w", + 125.19, + 18.765, + 136.65, + 26.805 + ], + [ + "r", + 138.09, + 18.75, + 142.785, + 26.94 + ], + [ + "a", + 143.4, + 18.6, + 149.91, + 26.925 + ], + [ + "p", + 152.4, + 15.15, + 159.525, + 26.94 + ], + [ + "s", + 161.115, + 18.6, + 166.86, + 26.94 + ], + [ + "b", + 172.71, + 18.6, + 179.835, + 30.15 + ], + [ + "e", + 181.485, + 18.6, + 188.355, + 26.94 + ], + [ + "s", + 189.885, + 18.6, + 195.63, + 26.94 + ], + [ + "i", + 197.475, + 18.75, + 199.02, + 29.805 + ], + [ + "d", + 201.0, + 18.6, + 208.125, + 30.15 + ], + [ + "e", + 210.225, + 18.6, + 217.095, + 26.94 + ] + ], + "colorBoundsPt": { + "initial": null, + "border": null, + "background": null + } + }, + { + "size": [ + 324.0, + 156.0 + ], + "chars": [ + [ + "t", + 18.24, + 140.1, + 23.085, + 150.135 + ], + [ + "h", + 24.69, + 140.25, + 31.47, + 151.65 + ], + [ + "e", + 33.51, + 140.1, + 40.38, + 148.44 + ], + [ + "e", + 45.87, + 140.1, + 52.74, + 148.44 + ], + [ + "n", + 54.78, + 140.25, + 61.56, + 148.44 + ], + [ + "l", + 64.05, + 140.25, + 65.37, + 151.65 + ], + [ + "a", + 67.335, + 140.1, + 73.845, + 148.425 + ], + [ + "r", + 76.335, + 140.25, + 81.03, + 148.44 + ], + [ + "g", + 81.93, + 136.65, + 89.055, + 148.44 + ], + [ + "e", + 91.155, + 140.1, + 98.025, + 148.44 + ], + [ + "d", + 99.615, + 140.1, + 106.74, + 151.65 + ], + [ + "i", + 113.085, + 140.25, + 114.63, + 151.305 + ], + [ + "n", + 117.06, + 140.25, + 123.84, + 148.44 + ], + [ + "i", + 126.225, + 140.25, + 127.77, + 151.305 + ], + [ + "t", + 129.165, + 140.1, + 134.01, + 150.135 + ], + [ + "i", + 135.51, + 140.25, + 137.055, + 151.305 + ], + [ + "a", + 138.9, + 140.1, + 145.41, + 148.425 + ], + [ + "l", + 147.9, + 140.25, + 149.22, + 151.65 + ], + [ + "l", + 155.67, + 140.25, + 156.99, + 151.65 + ], + [ + "e", + 159.09, + 140.1, + 165.96, + 148.44 + ], + [ + "t", + 166.965, + 140.1, + 171.81, + 150.135 + ], + [ + "t", + 172.38, + 140.1, + 177.225, + 150.135 + ], + [ + "e", + 178.38, + 140.1, + 185.25, + 148.44 + ], + [ + "r", + 187.29, + 140.25, + 191.985, + 148.44 + ], + [ + ".", + 190.89, + 140.04, + 192.75, + 142.065 + ], + [ + "D", + 195.285, + 140.25, + 203.865, + 150.96 + ], + [ + "r", + 206.055, + 140.25, + 210.75, + 148.44 + ], + [ + "o", + 211.5, + 140.1, + 218.94, + 148.44 + ], + [ + "p", + 221.025, + 136.65, + 228.15, + 148.44 + ], + [ + "c", + 18.825, + 119.85, + 24.705, + 128.19 + ], + [ + "a", + 25.89, + 119.85, + 32.4, + 128.175 + ], + [ + "p", + 34.89, + 116.4, + 42.015, + 128.19 + ], + [ + "l", + 48.015, + 120.0, + 49.335, + 131.4 + ], + [ + "e", + 51.435, + 119.85, + 58.305, + 128.19 + ], + [ + "a", + 59.76, + 119.85, + 66.27, + 128.175 + ], + [ + "d", + 68.31, + 119.85, + 75.435, + 131.4 + ], + [ + "s", + 77.475, + 119.85, + 83.22, + 128.19 + ], + [ + "t", + 88.035, + 119.85, + 92.88, + 129.885 + ], + [ + "h", + 94.485, + 120.0, + 101.265, + 131.4 + ], + [ + "i", + 103.65, + 120.0, + 105.195, + 131.055 + ], + [ + "s", + 107.115, + 119.85, + 112.86, + 128.19 + ], + [ + "s", + 118.2, + 119.85, + 123.945, + 128.19 + ], + [ + "h", + 125.895, + 120.0, + 132.675, + 131.4 + ], + [ + "o", + 134.715, + 119.85, + 142.155, + 128.19 + ], + [ + "r", + 144.24, + 120.0, + 148.935, + 128.19 + ], + [ + "t", + 149.4, + 119.85, + 154.245, + 129.885 + ], + [ + "p", + 159.75, + 116.4, + 166.875, + 128.19 + ], + [ + "a", + 168.39, + 119.85, + 174.9, + 128.175 + ], + [ + "r", + 177.39, + 120.0, + 182.085, + 128.19 + ], + [ + "a", + 182.7, + 119.85, + 189.21, + 128.175 + ], + [ + "g", + 191.25, + 116.4, + 198.375, + 128.19 + ], + [ + "r", + 200.925, + 120.0, + 205.62, + 128.19 + ], + [ + "a", + 206.235, + 119.85, + 212.745, + 128.175 + ], + [ + "p", + 215.235, + 116.4, + 222.36, + 128.19 + ], + [ + "h", + 224.46, + 120.0, + 231.24, + 131.4 + ], + [ + "w", + 18.165, + 99.765, + 29.625, + 107.805 + ], + [ + "h", + 31.065, + 99.75, + 37.845, + 111.15 + ], + [ + "i", + 40.23, + 99.75, + 41.775, + 110.805 + ], + [ + "l", + 44.205, + 99.75, + 45.525, + 111.15 + ], + [ + "e", + 47.625, + 99.6, + 54.495, + 107.94 + ], + [ + "t", + 59.4, + 99.6, + 64.245, + 109.635 + ], + [ + "h", + 65.85, + 99.75, + 72.63, + 111.15 + ], + [ + "e", + 74.67, + 99.6, + 81.54, + 107.94 + ], + [ + "r", + 87.48, + 99.75, + 92.175, + 107.94 + ], + [ + "e", + 92.925, + 99.6, + 99.795, + 107.94 + ], + [ + "m", + 101.835, + 99.75, + 113.37, + 107.94 + ], + [ + "a", + 115.275, + 99.6, + 121.785, + 107.925 + ], + [ + "i", + 124.17, + 99.75, + 125.715, + 110.805 + ], + [ + "n", + 128.145, + 99.75, + 134.925, + 107.94 + ], + [ + "i", + 137.31, + 99.75, + 138.855, + 110.805 + ], + [ + "n", + 141.285, + 99.75, + 148.065, + 107.94 + ], + [ + "g", + 150.105, + 96.15, + 157.23, + 107.94 + ], + [ + "t", + 162.645, + 99.6, + 167.49, + 109.635 + ], + [ + "e", + 168.645, + 99.6, + 175.515, + 107.94 + ], + [ + "x", + 176.25, + 99.75, + 183.645, + 107.79 + ], + [ + "t", + 184.155, + 99.6, + 189.0, + 109.635 + ], + [ + "w", + 193.395, + 99.765, + 204.855, + 107.805 + ], + [ + "r", + 206.295, + 99.75, + 210.99, + 107.94 + ], + [ + "a", + 211.605, + 99.6, + 218.115, + 107.925 + ], + [ + "p", + 220.605, + 96.15, + 227.73, + 107.94 + ], + [ + "s", + 229.32, + 99.6, + 235.065, + 107.94 + ], + [ + "b", + 19.275, + 79.35, + 26.4, + 90.9 + ], + [ + "e", + 28.05, + 79.35, + 34.92, + 87.69 + ], + [ + "s", + 36.45, + 79.35, + 42.195, + 87.69 + ], + [ + "i", + 44.04, + 79.5, + 45.585, + 90.555 + ], + [ + "d", + 47.565, + 79.35, + 54.69, + 90.9 + ], + [ + "e", + 56.79, + 79.35, + 63.66, + 87.69 + ], + [ + "t", + 68.565, + 79.35, + 73.41, + 89.385 + ], + [ + "h", + 75.015, + 79.5, + 81.795, + 90.9 + ], + [ + "e", + 83.835, + 79.35, + 90.705, + 87.69 + ], + [ + "e", + 96.195, + 79.35, + 103.065, + 87.69 + ], + [ + "n", + 105.105, + 79.5, + 111.885, + 87.69 + ], + [ + "l", + 114.375, + 79.5, + 115.695, + 90.9 + ], + [ + "a", + 117.66, + 79.35, + 124.17, + 87.675 + ], + [ + "r", + 126.66, + 79.5, + 131.355, + 87.69 + ], + [ + "g", + 132.255, + 75.9, + 139.38, + 87.69 + ], + [ + "e", + 141.48, + 79.35, + 148.35, + 87.69 + ], + [ + "d", + 149.94, + 79.35, + 157.065, + 90.9 + ], + [ + "i", + 163.41, + 79.5, + 164.955, + 90.555 + ], + [ + "n", + 167.385, + 79.5, + 174.165, + 87.69 + ], + [ + "i", + 176.55, + 79.5, + 178.095, + 90.555 + ], + [ + "t", + 179.49, + 79.35, + 184.335, + 89.385 + ], + [ + "i", + 185.835, + 79.5, + 187.38, + 90.555 + ], + [ + "a", + 189.225, + 79.35, + 195.735, + 87.675 + ], + [ + "l", + 198.225, + 79.5, + 199.545, + 90.9 + ], + [ + "l", + 19.275, + 59.25, + 20.595, + 70.65 + ], + [ + "e", + 22.695, + 59.1, + 29.565, + 67.44 + ], + [ + "t", + 30.57, + 59.1, + 35.415, + 69.135 + ], + [ + "t", + 35.985, + 59.1, + 40.83, + 69.135 + ], + [ + "e", + 41.985, + 59.1, + 48.855, + 67.44 + ], + [ + "r", + 50.895, + 59.25, + 55.59, + 67.44 + ], + [ + ".", + 54.495, + 59.04, + 56.355, + 61.065 + ] + ], + "colorBoundsPt": { + "initial": null, + "border": null, + "background": null + } + } + ], + "tolerancePt": 0.75 + } + }, + { + "name": "engine-counter-sibling-reset", + "family": "counter", + "html": "

Record Ada

Alpha
One
Two
Beta
Three
", + "css": "@page { size: 400pt 500pt; margin: 24pt; }\n* { margin: 0; padding: 0; }\nbody { font: 12pt Inter; line-height: 1.5; }\nbody { counter-reset: chapter; } .chapter { counter-increment: chapter; counter-reset: section; } .section { counter-increment: section; } .section::before { content: 'CNT' counter(chapter) '.' counter(section) 'END'; }", + "pages": 1, + "previewDpi": 96, + "expected": { + "labels": [ + "1.1", + "1.2", + "2.1" + ] + } + }, + { + "name": "engine-counter-nested-branches", + "family": "counter", + "html": "

Record Ada

Alpha
Beta
Gamma
Delta
Epsilon
", + "css": "@page { size: 400pt 500pt; margin: 24pt; }\n* { margin: 0; padding: 0; }\nbody { font: 12pt Inter; line-height: 1.5; }\n.scope { counter-reset: n; } .item { counter-increment: n; } .item::before { content: 'CNT' counters(n, '.') 'END'; }", + "pages": 1, + "previewDpi": 96, + "expected": { + "labels": [ + "1", + "1.1", + "1.2", + "2", + "1" + ] + } + }, + { + "name": "engine-counter-paginated-sections", + "family": "counter", + "html": "

Record Ada

Alpha
One
Two
", + "css": "@page { size: 400pt 500pt; margin: 24pt; }\n* { margin: 0; padding: 0; }\nbody { font: 12pt Inter; line-height: 1.5; }\nbody { counter-reset: chapter; } .chapter { counter-increment: chapter; counter-reset: section; } .next { break-before: page; } .section { counter-increment: section; } .section::before { content: 'CNT' counter(chapter) '.' counter(section) 'END'; }", + "pages": 2, + "previewDpi": 96, + "expected": { + "labels": [ + "1.1", + "2.1" + ] + } + }, + { + "name": "engine-overflow-contrast-hidden", + "family": "overflow", + "html": "
", + "css": "@page { size: 240px 180px; margin: 0; }\n* { margin: 0; padding: 0; box-sizing: border-box; }\nhtml, body { background: white; }\n.parent { position: absolute; left: 20px; top: 20px; width: 100px; height: 50px;\n overflow: hidden; border: 4px solid #003366; border-radius: 12px; background: #ddeeff; }\n.child { width: 60px; height: 70px; margin: 20px 10px 0; background: red; filter: contrast(1.1); }\n.parent { overflow: hidden; border-radius: 0px; } .child { filter: contrast(1.1); }", + "pages": 1, + "previewDpi": 96, + "expected": { + "probes": [ + { + "x": 50, + "y": 55, + "expected": [ + 255, + 0, + 0 + ] + }, + { + "x": 50, + "y": 85, + "expected": [ + 255, + 255, + 255 + ] + } + ], + "size": [ + 240, + 180 + ] + } + }, + { + "name": "engine-overflow-nested-clips", + "family": "overflow", + "html": "
", + "css": "@page { size: 240px 180px; margin: 0; }\n* { margin: 0; padding: 0; box-sizing: border-box; }\nhtml, body { background: white; }\n.parent { position: absolute; left: 20px; top: 20px; width: 100px; height: 50px;\n overflow: hidden; border: 4px solid #003366; border-radius: 12px; background: #ddeeff; }\n.child { width: 60px; height: 70px; margin: 20px 10px 0; background: red; filter: contrast(1.1); }\n\n .parent { width: 100px; height: 60px; border: 0; border-radius: 0; background: lime; }\n .inner { width: 50px; height: 60px; margin: 20px; overflow: hidden; background: blue; }\n .child { width: 80px; height: 80px; margin: 10px; }\n ", + "pages": 1, + "previewDpi": 96, + "expected": { + "probes": [ + { + "x": 60, + "y": 60, + "expected": [ + 255, + 0, + 0 + ] + }, + { + "x": 100, + "y": 60, + "expected": [ + 0, + 255, + 0 + ] + }, + { + "x": 60, + "y": 90, + "expected": [ + 255, + 255, + 255 + ] + } + ], + "size": [ + 240, + 180 + ] + } + }, + { + "name": "engine-overflow-ancestor-z-order", + "family": "overflow", + "html": "
", + "css": "@page { size: 240px 180px; margin: 0; }\n* { margin: 0; padding: 0; box-sizing: border-box; }\nhtml, body { background: white; }\n.parent { position: absolute; left: 20px; top: 20px; width: 100px; height: 50px;\n overflow: hidden; border: 4px solid #003366; border-radius: 12px; background: #ddeeff; }\n.child { width: 60px; height: 70px; margin: 20px 10px 0; background: red; filter: contrast(1.1); }\n\n .child { position: relative; z-index: 10; }\n .uncle { position: absolute; left: 30px; top: 50px; width: 70px;\n height: 45px; background: blue; z-index: 1; }\n ", + "pages": 1, + "previewDpi": 96, + "expected": { + "probes": [ + { + "x": 50, + "y": 55, + "expected": [ + 255, + 0, + 0 + ] + }, + { + "x": 50, + "y": 85, + "expected": [ + 0, + 0, + 255 + ] + } + ], + "size": [ + 240, + 180 + ] + } + }, + { + "name": "engine-border-space-no-horizontal-tile-raster", + "family": "border-image", + "html": "
", + "css": "@page { size: 140px 130px; margin: 0; }\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { background: white; }\n.box { position: absolute; left: 20px; top: 20px; width: 26px; height: 60px;\n background: white; border: 10px solid red; border-image-source: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAIAAAC0Ujn1AAAAQElEQVR4nO3UMQoAMAgEwXu6j/J/pk2aEyshLGx3OKUqyWTHam6hoaFndLiUT6G8a26hoaFntM3vf74naOgl+gBpduBeZXMujgAAAABJRU5ErkJggg==\");\n border-image-slice: 10 fill; border-image-width: 10px; border-image-repeat: space stretch; }\n", + "pages": 1, + "previewDpi": 96, + "expected": { + "probes": [ + { + "x": 33, + "y": 25, + "expected": [ + 255, + 255, + 255 + ] + }, + { + "x": 33, + "y": 40, + "expected": [ + 255, + 255, + 255 + ] + }, + { + "x": 25, + "y": 25, + "expected": [ + 255, + 0, + 0 + ] + }, + { + "x": 25, + "y": 40, + "expected": [ + 255, + 128, + 0 + ] + } + ], + "size": [ + 140, + 130 + ] + } + }, + { + "name": "engine-border-center-zero-top-falls-back-bottom-raster", + "family": "border-image", + "html": "
", + "css": "@page { size: 140px 130px; margin: 0; }\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { background: white; }\n.box { position: absolute; left: 20px; top: 20px; width: 60px; height: 70px;\n background: white; border: 10px solid red; border-image-source: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAIAAAC0Ujn1AAAAQElEQVR4nO3UMQoAMAgEwXu6j/J/pk2aEyshLGx3OKUqyWTHam6hoaFndLiUT6G8a26hoaFntM3vf74naOgl+gBpduBeZXMujgAAAABJRU5ErkJggg==\");\n border-image-slice: 10 fill; border-image-width: 0 10px 20px 10px; border-image-repeat: repeat stretch; }\n", + "pages": 1, + "previewDpi": 96, + "expected": { + "probes": [ + { + "x": 35, + "y": 35, + "expected": [ + 128, + 0, + 180 + ] + }, + { + "x": 45, + "y": 35, + "expected": [ + 0, + 180, + 0 + ] + }, + { + "x": 55, + "y": 35, + "expected": [ + 128, + 0, + 180 + ] + }, + { + "x": 65, + "y": 35, + "expected": [ + 0, + 180, + 0 + ] + } + ], + "size": [ + 140, + 130 + ] + } + }, + { + "name": "engine-border-space-no-horizontal-tile-svg", + "family": "border-image", + "html": "
", + "css": "@page { size: 140px 130px; margin: 0; }\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { background: white; }\n.box { position: absolute; left: 20px; top: 20px; width: 26px; height: 60px;\n background: white; border: 10px solid red; border-image-source: url(\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2230%22%20height%3D%2230%22%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff0000%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%230000ff%22%2F%3E%3Crect%20x%3D%2220%22%20y%3D%220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff0000%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%2210%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff8000%22%2F%3E%3Crect%20x%3D%2220%22%20y%3D%2210%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff8000%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%2220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff0000%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%230000ff%22%2F%3E%3Crect%20x%3D%2220%22%20y%3D%2220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff0000%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%225%22%20height%3D%2210%22%20fill%3D%22%2300b400%22%2F%3E%3Crect%20x%3D%2215%22%20y%3D%2210%22%20width%3D%225%22%20height%3D%2210%22%20fill%3D%22%238000b4%22%2F%3E%3C%2Fsvg%3E\");\n border-image-slice: 10 fill; border-image-width: 10px; border-image-repeat: space stretch; }\n", + "pages": 1, + "previewDpi": 96, + "expected": { + "probes": [ + { + "x": 33, + "y": 25, + "expected": [ + 255, + 255, + 255 + ] + }, + { + "x": 33, + "y": 40, + "expected": [ + 255, + 255, + 255 + ] + }, + { + "x": 25, + "y": 25, + "expected": [ + 255, + 0, + 0 + ] + }, + { + "x": 25, + "y": 40, + "expected": [ + 255, + 128, + 0 + ] + } + ], + "size": [ + 140, + 130 + ] + } + }, + { + "name": "engine-border-center-zero-top-falls-back-bottom-svg", + "family": "border-image", + "html": "
", + "css": "@page { size: 140px 130px; margin: 0; }\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { background: white; }\n.box { position: absolute; left: 20px; top: 20px; width: 60px; height: 70px;\n background: white; border: 10px solid red; border-image-source: url(\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2230%22%20height%3D%2230%22%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff0000%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%230000ff%22%2F%3E%3Crect%20x%3D%2220%22%20y%3D%220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff0000%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%2210%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff8000%22%2F%3E%3Crect%20x%3D%2220%22%20y%3D%2210%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff8000%22%2F%3E%3Crect%20x%3D%220%22%20y%3D%2220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff0000%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%230000ff%22%2F%3E%3Crect%20x%3D%2220%22%20y%3D%2220%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23ff0000%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%225%22%20height%3D%2210%22%20fill%3D%22%2300b400%22%2F%3E%3Crect%20x%3D%2215%22%20y%3D%2210%22%20width%3D%225%22%20height%3D%2210%22%20fill%3D%22%238000b4%22%2F%3E%3C%2Fsvg%3E\");\n border-image-slice: 10 fill; border-image-width: 0 10px 20px 10px; border-image-repeat: repeat stretch; }\n", + "pages": 1, + "previewDpi": 96, + "expected": { + "probes": [ + { + "x": 35, + "y": 35, + "expected": [ + 128, + 0, + 180 + ] + }, + { + "x": 45, + "y": 35, + "expected": [ + 0, + 180, + 0 + ] + }, + { + "x": 55, + "y": 35, + "expected": [ + 128, + 0, + 180 + ] + }, + { + "x": 65, + "y": 35, + "expected": [ + 0, + 180, + 0 + ] + } + ], + "size": [ + 140, + 130 + ] + } + } + ], + "scope": "Unchanged focused upstream fixtures and independent Chrome character/color expectations; counter labels follow retained CSS counter cases. Not general CSS parity or standards conformance." +} diff --git a/test/fixtures/inline-layout-2.5.22.json b/test/fixtures/inline-layout-2.5.22.json new file mode 100644 index 0000000..af96190 --- /dev/null +++ b/test/fixtures/inline-layout-2.5.22.json @@ -0,0 +1,70 @@ +{ + "schema": "fullbleed.reviewed_inline_layout.v1", + "engineVersion": "2.5.22", + "source": "https://github.com/fullbleed-engine/fullbleed-official/releases/tag/v2.5.22", + "reason": "The independently checked 2.5.22 invoice/report PDFs, native previews, PDFium pixels, text, assets and inputs are byte-identical to the reviewed 2.5.11 layouts. The engine SVG raster fallback is enabled. Standard 14 preview substitutes and the focused engine fixes are checked separately.", + "fonts": [ + { + "name": "Inter-Variable.ttf", + "path": "fonts/Inter-Variable.ttf", + "bytes": 876576, + "sha256": "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031" + }, + { + "name": "DMSerifDisplay-Regular.ttf", + "path": "fonts/DMSerifDisplay-Regular.ttf", + "bytes": 76580, + "sha256": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616" + }, + { + "name": "DMSerifDisplay-Italic.ttf", + "path": "fonts/DMSerifDisplay-Italic.ttf", + "bytes": 71208, + "sha256": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45" + }, + { + "name": "BebasNeue-Regular.ttf", + "path": "fonts/BebasNeue-Regular.ttf", + "bytes": 61400, + "sha256": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73" + }, + { + "name": "NotoSansMath-Regular.ttf", + "path": "fonts/NotoSansMath-Regular.ttf", + "bytes": 590904, + "sha256": "80b61fd613d3519197e64fff6f7e71fdc7f3e6526440ea4115b554ef7fd59af7" + } + ], + "fixtures": { + "invoice": { + "htmlSha256": "195725eeeb32c521e19b47dbacbb55048bebb6df8566ab135cdc41fbf33fe57a", + "cssSha256": "cda22b39ab1f7a1eeac7cd76d162b05cd3568c2430c099123ba30a2fbd58a296", + "pages": 1, + "text": "Northstar\nStudio\nD E S I G N W I T H D I R E C T I O N\nB R A N D\nD I G I T A L\nE X P E R I E N C E S\nT H A T M A T T E R .\n42 Cedar Avenue\nPortland, OR 97205\nhello@example.invalid\nnorthstar.example.invalid\nINVOICE\nGOOD WORK.\nCLEAR DETAILS.\nLET'S MAKE\nWHAT'S NEXT.\nP R E P A R E D F O R\nMaple & Finch\nAttn. Avery Morgan\nPortland, Oregon\nT H E P R O J E C T\nAutumn launch\nBrand workshop, digital\ndesign & handoff\nI N V O I C E N S - 1 0 4 2\nIssued \u00b7 01 October 2026\nDue \u00b7 31 October 2026\nCurrency \u00b7 USD\nDESCRIPTION HOURS RATE AMOUNT\nDiscovery & brand workshop\nPositioning, priorities, and a clear creative direction\n8 $125.00 $1,000.00\nDigital design & implementation\nA cohesive visual system, from layout to launch\n6 $95.00 $570.00\nReview & studio handoff\nFinal refinements, documentation, and delivery\n2 $150.00 $300.00\nA little clarity.\nA lot of possibility.\nThank you for inviting us\nto help shape your next chapter.\nSubtotal $1,870.00\nTax \u00b7 0% in this sample $0.00\nT O T A L D U E \u00b7 U S D\n$1,870.00\nP A Y M E N T D E T A I L S\nReference NS-1042 with your payment.\nPayment instructions are supplied separately.\nA N O T E F R O M T H E S T U D I O\nQuestions about a line item?\nWrite to hello@example.invalid.\nNORTHSTAR STUDIO / INVOICE NS-1042 FICTIONAL SAMPLE \u00b7 01 / 01", + "pdfSha256": "3078d155f30536f5ad721f7be3c241cd78661ae41249131fc0fd65f37f23e408", + "previewsSha256": [ + "72420749ccc3deaa8025daf7baffcdf79be716b6e8e93ed605effebed01e86c7" + ], + "pdfiumPixelSha256": [ + "d71293741974bef9abffbdf5d88b0827b48fe6523f8d9baf46f46fe1000895a5" + ] + }, + "report": { + "htmlSha256": "46b0e975d43914c8b4492218feae09ea0ad684091a9f9ee3b18f66a87202e521", + "cssSha256": "519591ae75c239ec602324f008dd99108ebeae0b9306df7215fa5f0442905473", + "pages": 3, + "text": "Common Ground\nC O M M U N I T Y T R U S T\nP E O P L E\nP L A C E S\nP O S S I B I L I T I E S\nCOMMON\nGROUND\nStronger places.\nMore room\nfor everyone.\n2026\nC O M M U N I T Y R E V I E W\nA Y E A R\nI N C O M M O N\n0 2 / O U R R E A C H\nSmall actions,\nshared outcomes.\n0 3 / O U R R E S O U R C E S\nWhere the\nsupport goes.\nCOMMON GROUND / COMMUNITY REVIEW FICTIONAL DATA \u00b7 01 / 03Common Ground 0 2 / O U R R E A C H\nA P L A C E T O B E G I N\nSmall actions.\nShared outcomes.\nAn open door, a working kitchen, a garden plot. This year, our fictional neighborhood\nprograms connected more people with practical spaces to learn, make, and belong.\n1,240\nhouseholds took part\nacross four neighborhoods\n4,860\nvolunteer hours\nshared across our programs\n12\ncommunity spaces\nkept open and active\nMore neighbors, each quarter.\nP A R T I C I P A T I N G H O U S E H O L D S\n420\n560\n790\n980\nQ1 Q2 Q3 Q4\nQuarterly participation counts; a household can participate in more than one quarter.\n\u201cBelonging starts\nwith somewhere\nto show up.\u201d\nO U R P R O G R A M P R I N C I P L E\nBuilt around daily life.\nEvening workshops, shared meals, and weekend garden\nsessions made participation easier for people with different\nschedules. The next step is a longer opening window at\neach neighborhood space.\nILLUSTRATIVE ANNUAL REVIEW / NOT AN ACTUAL ORGANIZATION 02 / 03Common Ground 0 3 / O U R R E S O U R C E S\nF O L L O W T H E S U P P O R T\nGood intentions.\nClear allocations.\nA practical view of this fictional operating budget. Every category supports the same\naim: useful places, open to more people.\nPROGRAM AREA ALLOCATION SHARE\nNeighborhood spaces $168,000.00 40%\nLearning & workshops $126,000.00 30%\nShared meals & gardens $84,000.00 20%\nAccess & transport $42,000.00 10%\nT O T A L O P E R A T I N G A L L O C A T I O N \u00b7 U S D $420,000.00\nWhat we count.\nHouseholds are deduplicated within the annual program\nregister. Volunteer hours include recorded event and\nmaintenance time. Space counts refer to year-round\nlocations, not individual events.\nQuarterly counts show participation within each period and\nmust not be added to estimate unique annual reach.\nWhat comes next.\nExtend evening access. Improve step-free entrances.\nPublish a simple monthly view of program activity and\nspending.\nA shared tomorrow\nis built in the everyday.\nCOMMON GROUND / METHODS & ALLOCATIONS FICTIONAL DATA \u00b7 03 / 03", + "pdfSha256": "139df89824e6a73c13ff62f1237bb8eb9f71e790dcbd051b1bb713ed2bc47802", + "previewsSha256": [ + "33ee3398d59f6836368cdab0f3c725c3cf12cbf2f9f3a3ac6a6b168f6c1886f5", + "c8fe5102e9219962ea068201be0a6730cb37de797649f4b5f44478433e79b018", + "a3e8acbc7ba4147c56d45b606d3974ec4aecbda1b5d7362451e5e8890491dc7d" + ], + "pdfiumPixelSha256": [ + "c7dc8db8fc08a78f74c4477629e0f68f3b34aa37b9b9eba46a977805933d3f03", + "c5e9d15bae4f208ecb0466ba3521e22c1f98c83aeab0e1112deec2fe20d8a711", + "05d2ebd36749c7421db665a5bba2ab36559e691b5bc5570ebc095d81af2585b7" + ] + } + } +} diff --git a/test/fonts/NotoSans-OFL.txt b/test/fonts/NotoSans-OFL.txt new file mode 100644 index 0000000..fdff63c --- /dev/null +++ b/test/fonts/NotoSans-OFL.txt @@ -0,0 +1,95 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/test/fonts/NotoSans-Regular.ttf b/test/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000..7557504 Binary files /dev/null and b/test/fonts/NotoSans-Regular.ttf differ diff --git a/test/test_release.py b/test/test_release.py index c953519..569fb92 100644 --- a/test/test_release.py +++ b/test/test_release.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: MIT import importlib.util +import hashlib +import json from pathlib import Path import unittest @@ -116,6 +118,48 @@ def test_queue_release_requires_installed_queue_evidence(self): with self.assertRaisesRegex(ValueError, 'Installed queue verification'): self.verify(version='0.4.0') + def engine_release(self): + self.browser_release() + self.release['tag_name'] = 'v0.4.1' + source = release_check.ENGINE_REGRESSION_SOURCE.read_bytes() + cases = json.loads(source)['cases'] + source_sha = hashlib.sha256(source).hexdigest() + self.report.update(package_version='0.4.1', engine_version='2.5.22', + engine_regressions=[dict(platform=platform, ok=True, package_version='0.4.1', engine_version='2.5.22', + source_sha256=source_sha, cases=2*len(cases), families=sorted({case['family'] for case in cases})) for platform in ['win32','linux']], + native_wasi_fixtures=[dict(name=case['name'], nativeWasiPdfEqual=True, nativeWasiPreviewsEqual=True) for case in cases]) + for browser in self.report['browser_matrix']: + browser.update(package_version='0.4.1', checks=144, engine_regression_ok=True, + engine_regression_cases=len(cases), engine_regression_source_sha256=source_sha) + for installed in self.report['installed_matrix']: + installed.update(queue_checks=14, queue_peak_workers=2) + + def test_engine_release_requires_independent_windows_and_linux_evidence(self): + self.engine_release() + self.verify(version='0.4.1') + for change in [{'ok':False}, {'cases':2}, {'source_sha256':'0'*64}, {'engine_version':'2.5.11'}, {'families':['counter']}, {'platform':'linux'}]: + with self.subTest(change=change): + self.engine_release() + self.report['engine_regressions'][0].update(change) + with self.assertRaisesRegex(ValueError, 'engine regression|Engine regression'): + self.verify(version='0.4.1') + + def test_engine_release_rejects_browser_or_native_evidence_gaps(self): + for change in [{'engine_regression_ok':False}, {'engine_regression_cases':0}, {'engine_regression_source_sha256':'0'*64}]: + with self.subTest(change=change): + self.engine_release() + self.report['browser_matrix'][0].update(change) + with self.assertRaisesRegex(ValueError, 'Browser engine regression'): + self.verify(version='0.4.1') + self.engine_release() + self.report['native_wasi_fixtures'].pop() + with self.assertRaisesRegex(ValueError, 'Native/WASI'): + self.verify(version='0.4.1') + self.engine_release() + self.report['native_wasi_fixtures'][0]['nativeWasiPreviewsEqual'] = False + with self.assertRaisesRegex(ValueError, 'Native/WASI'): + self.verify(version='0.4.1') + if __name__ == '__main__': unittest.main() diff --git a/tools/browser-requirements.txt b/tools/browser-requirements.txt index 8e8037d..45d854d 100644 --- a/tools/browser-requirements.txt +++ b/tools/browser-requirements.txt @@ -1,2 +1,4 @@ playwright==1.63.0 pypdf==6.19.0 +pypdfium2==5.14.0 +pillow==12.3.0 diff --git a/tools/build-browser.mjs b/tools/build-browser.mjs index 2fd2161..8a922f7 100644 --- a/tools/build-browser.mjs +++ b/tools/build-browser.mjs @@ -34,7 +34,7 @@ export async function buildBrowser(root, manifest) { files[name] = { source, bytes: data.length, sha256: hash(data) }; } const publicManifest = { schema: 'fullbleed.browser-assets.v1', packageVersion: manifest.packageVersion, - engineVersion: manifest.engineVersion, files }; + engineVersion: manifest.engineVersion, engineFeatures: manifest.engineFeatures, files }; await writeFile(join(output, 'asset-manifest.json'), JSON.stringify(publicManifest, null, 2) + '\n'); const outputs = {}; for (const name of ['client.js', 'worker.js', 'asset-manifest.json']) { diff --git a/tools/build.mjs b/tools/build.mjs index 90b7505..00ea571 100644 --- a/tools/build.mjs +++ b/tools/build.mjs @@ -23,6 +23,8 @@ assert.equal(metadata.packages.find(p => p.name === 'fullbleed-node-adapter').ve const core = metadata.packages.find(p => p.name === 'fullbleed'); assert.equal(core.version, pkg.fullbleed.engineVersion); assert(core.source?.startsWith('registry+'), 'Build from the published crate'); +const engineFeatures = metadata.resolve.nodes.find(node => node.id === core.id).features.sort(); +assert.deepEqual(engineFeatures, ['svg_raster'], 'Build with the native SVG fallback enabled'); await mkdir(dist, { recursive: true }); await copyFile(join(root, 'src/index.d.ts'), join(dist, 'index.d.cts')); await copyFile(join(engine, 'target/wasm32-wasip1/release/fullbleed-node-adapter.wasm'), join(dist, 'engine.wasm')); @@ -58,6 +60,7 @@ for (const name of ['dist/engine.wasm', 'dist/THIRD_PARTY_NOTICES.txt', ...previ } const manifest = { packageVersion: pkg.version, engineVersion: core.version, engineSource: `https://crates.io/crates/fullbleed/${core.version}`, + engineFeatures, target: 'wasm32-wasip1', memoryMaximumBytes: 536870912, fonts: ['Inter-Variable.ttf', 'DMSerifDisplay-Regular.ttf', 'DMSerifDisplay-Italic.ttf', 'BebasNeue-Regular.ttf'], dependencies: metadata.packages.filter(p => p.source).map(p => ({ name: p.name, version: p.version, source: p.source, license: p.license })), diff --git a/tools/check-build.mjs b/tools/check-build.mjs index b61b1ca..d6d2eed 100644 --- a/tools/check-build.mjs +++ b/tools/check-build.mjs @@ -8,6 +8,7 @@ const pkg = JSON.parse(await readFile(new URL('package.json', root), 'utf8')); const manifest = JSON.parse(await readFile(new URL('dist/build.json', root), 'utf8')); assert.equal(manifest.packageVersion, pkg.version); assert.equal(manifest.engineVersion, pkg.fullbleed.engineVersion); +assert.deepEqual(manifest.engineFeatures, ['svg_raster']); const notices = await readFile(new URL('dist/THIRD_PARTY_NOTICES.txt', root), 'utf8'); for (const family of ['Liberation', 'NotoSans', 'NotoSansMath', 'NotoSansSymbols', 'NotoSansSymbols2']) { const path = `dist/preview-font-notices/LICENSE-${family}.txt`; @@ -23,6 +24,7 @@ assert.deepEqual(await readFile(new URL('src/index.d.ts', root)), await readFile const browser = JSON.parse(await readFile(new URL('dist/browser/asset-manifest.json', root), 'utf8')); assert.equal(browser.packageVersion, pkg.version); assert.equal(browser.engineVersion, pkg.fullbleed.engineVersion); +assert.deepEqual(browser.engineFeatures, manifest.engineFeatures); for (const record of Object.values(browser.files)) { const bytes = await readFile(new URL(record.source, root)); assert.equal(bytes.length, record.bytes, record.source); diff --git a/tools/prepare-browser-check.mjs b/tools/prepare-browser-check.mjs index 01f7ff7..b3073cf 100644 --- a/tools/prepare-browser-check.mjs +++ b/tools/prepare-browser-check.mjs @@ -7,6 +7,7 @@ import { resolve, join, dirname } from 'node:path'; import { pathToFileURL } from 'node:url'; import { inlineFixtures } from '../test/inline-wrapping.mjs'; import { standardFontFixtures } from '../test/standard-fonts.mjs'; +import { engineRegressionFixtures } from '../test/engine-regressions.mjs'; const root = resolve('.'); const out = join(root, 'output/browser-verification'); @@ -36,13 +37,16 @@ const fixture = async name => ({ html: await readFile(`examples/${name}.html`, ' const inputs = { invoice: await fixture('invoice'), report: await fixture('report'), ...Object.fromEntries(inlineFixtures.map(({ name, pages, ...input }) => [name, { ...input, previewDpi: 96 }])), ...Object.fromEntries(standardFontFixtures.map(({ name, font, pages, ...input }) => [name, { ...input, previewDpi: 72 }])), + ...Object.fromEntries(engineRegressionFixtures.map(({ name, pages, ...input }) => [name, input])), custom: { html: '

Local asset test

Integral: ⨌

', css: "body {font-family:Inter,'Noto Sans Math'}", previewDpi: 72, fontFiles: ['NotoSansMath-Regular.ttf'], assetTexts: { 'brand/logo.svg': '' } }, static: { html: '

PRIVATE_BROWSER_MARKER

', css: '', previewDpi: 0 }, }; -await copyFile('test/fonts/NotoSansMath-Regular.ttf', join(site, 'fixtures/NotoSansMath-Regular.ttf')); +for (const file of new Set(Object.values(inputs).flatMap(input => input.fontFiles ?? []))) { + await copyFile('test/fonts/' + file, join(site, 'fixtures', file)); +} await writeFile(join(site, 'fixtures/inputs.json'), JSON.stringify(inputs, null, 2) + '\n'); const baselines = {}; for (const [name, fixture] of Object.entries(inputs)) { diff --git a/tools/render-engine-regressions.mjs b/tools/render-engine-regressions.mjs new file mode 100644 index 0000000..75fb8b2 --- /dev/null +++ b/tools/render-engine-regressions.mjs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { parseArgs } from 'node:util'; +import { loadEngineRegressionFixtures } from '../test/engine-regressions.mjs'; + +const root = fileURLToPath(new URL('../', import.meta.url)); +const { values } = parseArgs({ options: { 'package-root': { type: 'string' }, out: { type: 'string' } } }); +let packageRoot = values['package-root'], packed; +if (!packageRoot) { + packed = JSON.parse(await readFile(join(root, 'output/pack-verification/verification.json'), 'utf8')); + assert(packed.ok); + packageRoot = join(root, 'output/pack-verification', packed.installDirectory, 'node_modules/fullbleed'); +} +packageRoot = resolve(packageRoot); +const { renderPdf, version, engineVersion } = await import(pathToFileURL(join(packageRoot, 'src/index.js')).href); +const build = JSON.parse(await readFile(join(packageRoot, 'dist/build.json'), 'utf8')); +const hash = bytes => createHash('sha256').update(bytes).digest('hex'); +assert.equal(build.packageVersion, version); +assert.equal(build.engineVersion, engineVersion); +assert.equal(hash(await readFile(join(packageRoot, 'dist/engine.wasm'))), build.files['dist/engine.wasm'].sha256); +const out = resolve(values.out ?? join(root, 'output/engine-regressions')); +await mkdir(join(out, '..'), { recursive: true }); +await mkdir(out); +const report = { schema: 'fullbleed.javascript.engine-regressions.renders.v1', ok: false, + packageVersion: version, engineVersion, node: process.version, platform: process.platform, + wasmSha256: build.files['dist/engine.wasm'].sha256, installedTarballSha256: packed?.tarballSha256 ?? null, cases: [] }; +try { + const fixtures = await loadEngineRegressionFixtures(); + for (const isolation of ['worker', 'process']) { + for (const { name, pages, ...input } of fixtures) { + const directory = isolation + '-' + name; + const folder = join(out, directory); + await mkdir(folder); + await writeFile(join(folder, 'input.html'), input.html); + await writeFile(join(folder, 'style.css'), input.css); + const result = await renderPdf({ ...input, isolation }); + assert.equal(result.missingGlyphs, 0, name); + assert.equal(result.previews.length, result.pages, name); + assert.deepEqual((await renderPdf({ ...input, isolation })).pdf, result.pdf, name + ' repeated PDF'); + await writeFile(join(folder, 'document.pdf'), result.pdf); + for (const [index, png] of result.previews.entries()) await writeFile(join(folder, `preview-${index + 1}.png`), png); + const entry = { name, isolation, directory, pages: result.pages, expectedPages: pages, pdfSha256: hash(result.pdf), + previewsSha256: result.previews.map(hash), repeatPdfBytesIdentical: true }; + if (isolation === 'process') { + const worker = report.cases.find(row => row.name === name && row.isolation === 'worker'); + assert.equal(entry.pdfSha256, worker.pdfSha256, name + ' worker/process PDF'); + assert.deepEqual(entry.previewsSha256, worker.previewsSha256, name + ' worker/process previews'); + } + report.cases.push(entry); + } + } + report.ok = true; +} finally { + await writeFile(join(out, 'renders.json'), JSON.stringify(report, null, 2) + '\n'); +} +console.log(JSON.stringify({ ok: report.ok, packageVersion: version, engineVersion, cases: report.cases.length })); diff --git a/tools/verify-engine-regressions.py b/tools/verify-engine-regressions.py new file mode 100644 index 0000000..df0e080 --- /dev/null +++ b/tools/verify-engine-regressions.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Check JS-generated PDFs against retained counter, color and Chrome geometry expectations.""" +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +from hashlib import sha256 +from importlib.metadata import version +import json +from pathlib import Path +import re + +from PIL import Image, ImageChops, ImageFilter +from pypdf import PdfReader +import pypdfium2 as pdfium + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / 'test/fixtures/engine-2.5.22.json' +COLORS = {'initial': (188, 108, 37), 'border': (224, 64, 32), 'background': (255, 224, 128)} + + +def color_bounds(image, color): + difference = ImageChops.difference(image.convert('RGB'), Image.new('RGB', image.size, color)) + channels = [channel.point(lambda value: 255 if value < 24 else 0) for channel in difference.split()] + mask = ImageChops.multiply(ImageChops.multiply(channels[0], channels[1]), channels[2]) + if color == COLORS['background']: + mask = mask.filter(ImageFilter.MinFilter(3)) + bounds = mask.getbbox() + return list(bounds) if bounds else None + + +def inspect(pdf, dpi): + observations, images = [], [] + with pdfium.PdfDocument(pdf) as document: + for page in document: + text = page.get_textpage() + try: + chars = [[text.get_text_range(i, 1), *text.get_charbox(i)] + for i in range(text.count_chars()) if text.get_text_range(i, 1).strip()] + observations.append(dict(size=list(page.get_size()), chars=chars)) + bitmap = page.render(scale=dpi / 72) + try: + images.append(bitmap.to_pil().convert('RGB')) + finally: + bitmap.close() + finally: + text.close() + page.close() + return observations, images + + +def check_case(case, pdf, previews, destination): + checks = [] + def check(name, ok, **details): + checks.append(dict(name=name, ok=bool(ok), **details)) + reader = PdfReader(pdf) + check('page count', len(reader.pages) == case['pages'], actual=len(reader.pages), expected=case['pages']) + observations, independent = inspect(pdf, case['previewDpi']) + for index, image in enumerate(independent, 1): + image.save(destination / f'pdfium-{index}.png') + native = [] + for path in previews: + with Image.open(path) as image: + native.append(image.convert('RGB')) + try: + check('preview page count', len(native) == case['pages']) + expected = case['expected'] + if case['family'] == 'counter': + text = ''.join(page.extract_text() for page in reader.pages) + labels = re.findall(r'CNT([\d.\-]+)END', re.sub(r'\s+', '', text)) + check('emitted counter labels', labels == expected['labels'], actual=labels, expected=expected['labels']) + elif case['family'] in ('overflow', 'border-image'): + for mode, images in [('preview', native), ('pdfium', independent)]: + image = images[0] + check(mode + ' dimensions', list(image.size) == expected['size'], actual=list(image.size)) + for probe in expected['probes']: + actual = list(image.getpixel((probe['x'], probe['y']))) + check(mode + ' interior color', max(abs(a-b) for a,b in zip(actual,probe['expected'])) <= 3, + x=probe['x'], y=probe['y'], actual=actual, expected=probe['expected']) + elif case['family'] == 'first-letter': + reference = expected['chromePages'] + same_text = len(observations) == len(reference) and all( + ''.join(c[0] for c in actual['chars']) == ''.join(c[0] for c in wanted['chars']) + for actual, wanted in zip(observations, reference)) + check('Chrome text and page sequence', same_text) + check('Chrome page sizes', [p['size'] for p in observations] == [p['size'] for p in reference]) + delta = max((abs(a-b) for page, wanted in zip(observations,reference) + for actual, target in zip(page['chars'],wanted['chars']) + for a,b in zip(actual[1:],target[1:])), default=0) if same_text else None + check('Chrome character geometry', delta is not None and delta <= expected['tolerancePt'], + maximumDeltaPt=delta, tolerancePt=expected['tolerancePt']) + for mode, images in [('preview',native),('pdfium',independent)]: + for index, (image, wanted) in enumerate(zip(images,reference), 1): + for name,color in COLORS.items(): + bounds = color_bounds(image,color) + bounds = [n*72/case['previewDpi'] for n in bounds] if bounds else None + target = wanted['colorBoundsPt'][name] + delta = max(abs(a-b) for a,b in zip(bounds,target)) if bounds is not None and target is not None else 0 if bounds == target else None + check(mode+' Chrome '+name+' bounds', delta is not None and delta <= 1.5, + page=index, actualPt=bounds, expectedPt=target, maximumDeltaPt=delta) + else: + raise ValueError('Unknown fixture family: '+case['family']) + finally: + for image in [*native, *independent]: + image.close() + return dict(ok=all(item['ok'] for item in checks), checks=checks) + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('out',type=Path,nargs='?',default=ROOT/'output/engine-regressions') + parser.add_argument('--browser',action='store_true',help='Read actual downloads and the browser verification record in OUT') + parser.add_argument('--negative-control',action='store_true',help='Require public npm 0.4.0 to fail at least one case in each fix family') + args=parser.parse_args() + assert not (args.browser and args.negative_control) + source=json.loads(SOURCE.read_text(encoding='utf-8')) + font=source['font'] + assert sha256((ROOT/'test/fonts'/font['file']).read_bytes()).hexdigest()==font['sha256'] + assert sha256((ROOT/'test/fonts'/font['license']).read_bytes()).hexdigest()==font['licenseSha256'] + cases={case['name']:case for case in source['cases']} + out=args.out.resolve() + if args.browser: + origin=json.loads((out/'verification.json').read_text(encoding='utf-8')) + assert origin['ok'] + records=[dict(name=name,isolation='browser',directory=name,pdfFile=name+'.pdf',previewFiles=[name+f'-{i+1}.png' for i in range(case['pages'])]) for name,case in cases.items()] + package,engine=origin['package'],origin['engine'] + else: + origin=json.loads((out/'renders.json').read_text(encoding='utf-8')) + assert origin['ok'] + records=origin['cases'] + assert {(r['name'],r['isolation']) for r in records}=={(name,mode) for name in cases for mode in ('worker','process')} + assert len(records)==len(cases)*2 + package,engine=origin['packageVersion'],origin['engineVersion'] + if args.negative_control: + assert (package,engine)==('0.4.0','2.5.11') + report=dict(ok=False,checkedAt=datetime.now(timezone.utc).isoformat(),packageVersion=package,engineVersion=engine, + sourceSha256=sha256(SOURCE.read_bytes()).hexdigest(),installedTarballSha256=origin.get('installedTarballSha256'), + negativeControl=args.negative_control,browser=args.browser, + versions={name:version(name) for name in ('pypdf','pypdfium2','pillow')},cases=[], + scope='Only retained emitted labels, interior color probes and Chrome geometry. Browser equality follows the separate browser report; no general CSS or standards certification.') + try: + for record in records: + case=cases[record['name']] + destination=out/record['directory'] + destination.mkdir(exist_ok=True) + pdf=out/record['pdfFile'] if args.browser else destination/'document.pdf' + previews=[out/file for file in record['previewFiles']] if args.browser else [destination/f'preview-{i+1}.png' for i in range(record['pages'])] + if not args.browser: + assert sha256(pdf.read_bytes()).hexdigest()==record['pdfSha256'] + assert [sha256(p.read_bytes()).hexdigest() for p in previews]==record['previewsSha256'] + try: + result=check_case(case,pdf,previews,destination) + except Exception as error: + result=dict(ok=False,error=str(error)) + report['cases'].append(dict(name=case['name'],family=case['family'],isolation=record['isolation'],pdfSha256=sha256(pdf.read_bytes()).hexdigest(),**result)) + failed={r['family'] for r in report['cases'] if not r['ok']} + report['failedFamilies']=sorted(failed) + report['ok']=(failed=={c['family'] for c in cases.values()} and not any('error' in r for r in report['cases'])) if args.negative_control else not failed + assert report['ok'], '; '.join(r['isolation']+'/'+r['name'] for r in report['cases'] if not r['ok']) + finally: + filename='engine-regressions.json' if args.browser else 'verification.json' + (out/filename).write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8') + print(json.dumps(dict(ok=report['ok'],packageVersion=package,engineVersion=engine,cases=len(report['cases']),negativeControl=args.negative_control,failedFamilies=report['failedFamilies']))) + + +if __name__=='__main__': + main() diff --git a/tools/verify-native.mjs b/tools/verify-native.mjs index 8576190..6589fd6 100644 --- a/tools/verify-native.mjs +++ b/tools/verify-native.mjs @@ -9,6 +9,7 @@ import { renderPdf } from 'fullbleed'; import { gradientFixtures } from '../test/gradients.mjs'; import { inlineFixtures } from '../test/inline-wrapping.mjs'; import { standardFontFixtures } from '../test/standard-fonts.mjs'; +import { loadEngineRegressionFixtures } from '../test/engine-regressions.mjs'; const root = fileURLToPath(new URL('../', import.meta.url)); const engine = join(root, 'engine'); @@ -24,6 +25,7 @@ const fixtures = []; fixtures.push(...gradientFixtures); fixtures.push(...inlineFixtures); fixtures.push(...standardFontFixtures); +fixtures.push(...await loadEngineRegressionFixtures()); for (const [name, pages] of [['invoice', 1], ['report', 3]]) { fixtures.push({ name, pages, html: await readFile(join(root, 'examples', name + '.html'), 'utf8'), css: await readFile(join(root, 'examples', name + '.css'), 'utf8') }); } @@ -46,9 +48,10 @@ for (const fixture of fixtures) { } await writeFile(join(directory, 'input.html'), fixture.html); await writeFile(join(directory, 'style.css'), 'body { font-family: Inter; }\n' + fixture.css); - const native = spawnSync(executable, ['1000', '96', 'reject', ...names], { cwd: directory, encoding: 'utf8', timeout: 60000 }); + const dpi = fixture.previewDpi ?? 96; + const native = spawnSync(executable, ['1000', String(dpi), 'reject', ...names], { cwd: directory, encoding: 'utf8', timeout: 60000 }); assert.equal(native.status, 0, native.stderr); - const options = { html: fixture.html, css: fixture.css, previewDpi: 96, fonts: fixture.fonts, assets: fixture.assets }; + const options = { html: fixture.html, css: fixture.css, previewDpi: dpi, fonts: fixture.fonts, assets: fixture.assets }; const wasi = await renderPdf(options); assert.equal(wasi.pages, fixture.pages); assert.equal(wasi.missingGlyphs, 0); diff --git a/tools/verify-release.py b/tools/verify-release.py index fa9399e..628cb3f 100644 --- a/tools/verify-release.py +++ b/tools/verify-release.py @@ -14,6 +14,7 @@ import tempfile REPO = 'fullbleed-engine/fullbleed-node' +ENGINE_REGRESSION_SOURCE = Path(__file__).resolve().parents[1] / 'test/fixtures/engine-2.5.22.json' def require(condition, message): @@ -48,13 +49,34 @@ def verify_binding(version, run_id, run, release, tag_commit, report): require(len(browsers) == 3 and {item.get('browser') for item in browsers} == {'chrome', 'firefox', 'webkit'}, 'Release does not retain the required browser matrix') release_version = tuple(map(int, version.split('.'))) - required_browser_checks = 105 if release_version >= (0, 3, 2) else 66 if release_version >= (0, 3, 1) else 30 + required_browser_checks = 144 if release_version >= (0, 4, 1) else 105 if release_version >= (0, 3, 2) else 66 if release_version >= (0, 3, 1) else 30 require(all(item.get('ok') is True and item.get('package_version') == version and item.get('checks', 0) >= required_browser_checks for item in browsers), 'Browser verification is incomplete or tests another package') if release_version >= (0, 4, 0): require(all(item.get('queue_checks', 0) >= 14 and item.get('queue_peak_workers') == 2 for item in report['installed_matrix']), 'Installed queue verification is incomplete') + if release_version >= (0, 4, 1): + source = ENGINE_REGRESSION_SOURCE.read_bytes() + source_sha = hashlib.sha256(source).hexdigest() + cases = json.loads(source)['cases'] + names = {case['name'] for case in cases} + families = {case['family'] for case in cases} + regressions = report.get('engine_regressions', []) + require(len(regressions) == 2 and {item.get('platform') for item in regressions} == {'win32', 'linux'}, + 'Windows and Linux engine regression evidence is required') + require(all(item.get('ok') is True and item.get('package_version') == version + and item.get('engine_version') == report.get('engine_version') + and item.get('source_sha256') == source_sha and item.get('cases') == 2 * len(names) + and set(item.get('families', [])) == families for item in regressions), + 'Engine regression evidence is incomplete or targets another source') + require(all(item.get('engine_regression_ok') is True and item.get('engine_regression_cases') == len(names) + and item.get('engine_regression_source_sha256') == source_sha for item in browsers), + 'Browser engine regression evidence is incomplete') + native = {item.get('name'): item for item in report.get('native_wasi_fixtures', [])} + require(names.issubset(native) and all(native[name].get('nativeWasiPdfEqual') is True + and native[name].get('nativeWasiPreviewsEqual') is True for name in names), + 'Native/WASI engine regression comparison is incomplete') def verify_tarball(data, info, report):