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
Record Ada
Record Ada
Integral: ⨌
', 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):