diff --git a/.gitattributes b/.gitattributes index 5c7b689..127d3c3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,17 @@ examples/aspnet/*.md text eol=lf examples/aspnet/LICENSE text eol=lf examples/aspnet/Assets/*.html text eol=lf examples/aspnet/Assets/*.css text eol=lf +examples/dotnet-accessibility/*.cs text eol=lf +examples/dotnet-accessibility/*.csproj text eol=lf +examples/dotnet-accessibility/*.json text eol=lf +examples/dotnet-accessibility/*.md text eol=lf +examples/dotnet-accessibility/LICENSE text eol=lf +examples/dotnet-accessibility/Assets/*.html text eol=lf +examples/dotnet-accessibility/Assets/*.css text eol=lf +examples/dotnet-accessibility/Assets/*.svg text eol=lf +examples/dotnet-accessibility/Assets/*-OFL.txt -text whitespace=-blank-at-eol +docs/assets/dotnet-accessibility/** -text whitespace=cr-at-eol +docs/assets/dotnet-accessibility/*.pdf binary docs/assets/playground/LICENSES.txt text eol=lf whitespace=-blank-at-eol docs/assets/pdf-regression/*.json text eol=lf # Evidence downloads and their manifests are verified by byte hashes. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6edc18d..e8dc7bd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -100,6 +100,38 @@ jobs: output/aspnet-verification/*.log output/aspnet-verification/preview/* retention-days: 14 + tagged-notice: + name: C# tagged notice (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + - run: python -m pip install pypdf==6.19.0 + - name: Verify the downloaded ZIP and isolated published tagged notice + run: python tools/verify_dotnet_accessibility.py + - uses: actions/upload-artifact@v4 + if: always() + with: + name: dotnet-accessibility-verification-${{ matrix.os }} + path: | + output/dotnet-accessibility-verification/verification.json + output/dotnet-accessibility-verification/*.txt + output/dotnet-accessibility-verification/ua1/ + output/dotnet-accessibility-verification/ua2/ + retention-days: 14 build: runs-on: ubuntu-latest steps: @@ -305,7 +337,7 @@ jobs: path: site deploy: if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - needs: [build, aspnet, chinese, markdown] + needs: [build, aspnet, chinese, markdown, tagged-notice] runs-on: ubuntu-latest permissions: pages: write diff --git a/.gitignore b/.gitignore index 1239467..3fccb14 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ examples/dotnet/obj/ examples/aspnet/bin/ examples/aspnet/obj/ examples/aspnet/output/ +examples/dotnet-accessibility/bin/ +examples/dotnet-accessibility/obj/ +examples/dotnet-accessibility/output/ docs/assets/browser-demo/ docs/assets/react-demo/ diff --git a/README.md b/README.md index 2968aa0..ebe2b84 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,19 @@ pages, checks their PDF text and page counts, and exercises the documented proje commands. CI retains the PDFs, previews, and structured report. Older showcase, playground, and tutorial downloads keep their own verified release pins. +The tagged C# notice lives in `examples/dotnet-accessibility`. When updating it, +restore its lockfile from public NuGet, render both `ua1` and `ua2`, and review +the final PDFs and native preview before replacing `docs/assets/dotnet-accessibility`. +Run `python tools/build_dotnet_accessibility_starter.py` to rebuild the stable +ZIP and source manifest, then `python tools/verify_dotnet_accessibility.py`. +The verifier needs the .NET 10 SDK, Java 21 or later, and `pypdf==6.19.0`; it +downloads a checksum-pinned veraPDF 1.30.2 validator. These are maintainer checks, +not dependencies of the console application. Pass `--refresh-evidence` when +updating the downloadable reports; it writes `verification.json`, +`verapdf-ua1.json` and `verapdf-ua2.json` only after rendering and validation pass. +CI verifies the actual ZIP in isolated published applications on Windows and +Linux and checks that the retained reports still describe those specimens. + When changing the canonical domain, update `site_url`, `docs/robots.txt`, and this README, configure GitHub Pages, and verify DNS, HTTPS, sitemap URLs, and redirects. Keep the GitHub Pages URL until the custom domain is working. Fullbleed is MIT licensed. Do not publish unsupported performance comparisons, standards conformance claims, or stale licensing statements. Retain old page URLs through the redirect map when consolidating reference material. diff --git a/docs/accessibility/overview.md b/docs/accessibility/overview.md index 7964f59..8292389 100644 --- a/docs/accessibility/overview.md +++ b/docs/accessibility/overview.md @@ -5,6 +5,10 @@ description: Author semantic documents, generate tagged PDF output, and retain a Fullbleed provides semantic Python components, tagged PDF output profiles, inspection, and accessibility verification tools. Start with clear headings, logical reading order, meaningful table headers, document language, and appropriate alternative text. +For .NET, use the [editable C# tagged-notice starter](../guides/csharp-tagged-pdf.md). +It includes styled HTML/CSS, explicit fonts, a scoped table and a captioned +illustration, with PDF/UA-1 and PDF/UA-2 output and retained verification. + ## Try the checked example [Open the service-notice PDF](../assets/examples/accessible.pdf) · [View HTML](../assets/examples/accessible.html) · [View CSS](../assets/examples/accessible.css) diff --git a/docs/assets/dotnet-accessibility/notice-ua1.pdf b/docs/assets/dotnet-accessibility/notice-ua1.pdf new file mode 100644 index 0000000..2ff882d Binary files /dev/null and b/docs/assets/dotnet-accessibility/notice-ua1.pdf differ diff --git a/docs/assets/dotnet-accessibility/notice-ua2.pdf b/docs/assets/dotnet-accessibility/notice-ua2.pdf new file mode 100644 index 0000000..18c7d52 Binary files /dev/null and b/docs/assets/dotnet-accessibility/notice-ua2.pdf differ diff --git a/docs/assets/dotnet-accessibility/notice.png b/docs/assets/dotnet-accessibility/notice.png new file mode 100644 index 0000000..542e461 Binary files /dev/null and b/docs/assets/dotnet-accessibility/notice.png differ diff --git a/docs/assets/dotnet-accessibility/project.zip b/docs/assets/dotnet-accessibility/project.zip new file mode 100644 index 0000000..b5f842b Binary files /dev/null and b/docs/assets/dotnet-accessibility/project.zip differ diff --git a/docs/assets/dotnet-accessibility/source.json b/docs/assets/dotnet-accessibility/source.json new file mode 100644 index 0000000..ddbd4c8 --- /dev/null +++ b/docs/assets/dotnet-accessibility/source.json @@ -0,0 +1,79 @@ +{ + "source": "https://github.com/fullbleed-engine/docs/tree/main/examples/dotnet-accessibility", + "framework": "net10.0", + "package": "FullBleed.DotNet", + "packageVersion": "0.1.7", + "projectZipSha256": "bff5801de233ea3bca593a0656decd739373ec51ab5e9c1f2526d0d67eae8f02", + "files": [ + { + "path": "AccessibleNotice.csproj", + "bytes": 563, + "sha256": "7d9a7a9d1851786c8db87013078cfffb49b1f55928f88dd9d3d8b9a4d9508e3b" + }, + { + "path": "Assets/DMSerifDisplay-OFL.txt", + "bytes": 4605, + "sha256": "a3e5cdd67d4571dd0a24fcc968de0efde7ae97ef752daf0906e4767619dd7231" + }, + { + "path": "Assets/DMSerifDisplay-Regular.ttf", + "bytes": 76580, + "sha256": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616" + }, + { + "path": "Assets/Inter-OFL.txt", + "bytes": 4377, + "sha256": "5b9321a4298cfeb6b34354164a1c3afc3db114569984c502b9b35d988fd58c57" + }, + { + "path": "Assets/Inter-Variable.ttf", + "bytes": 876576, + "sha256": "29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031" + }, + { + "path": "Assets/notice.css", + "bytes": 1504, + "sha256": "3994f4ca6c1f8539bed60fda207980a9646ad3c5a111f647da8b742cb7158495" + }, + { + "path": "Assets/notice.html", + "bytes": 1957, + "sha256": "e4426b77b847f02394c66a97a756878e12d1b9ec27affb4716c36f4b9c8ef7b9" + }, + { + "path": "Assets/route.svg", + "bytes": 596, + "sha256": "09c4e6146359bc1c68cec600e2ef649ac6f4caf9762366cd19e9d7ad7b1fbea7" + }, + { + "path": "LICENSE", + "bytes": 1079, + "sha256": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9" + }, + { + "path": "Program.cs", + "bytes": 2955, + "sha256": "d3e996ccfcdb81f9808f3a4fa208dae570af53a6775a00297eb68d5f7fc944e3" + }, + { + "path": "README.md", + "bytes": 1958, + "sha256": "e175125529180af2457b6ba97111c62e32bfd7a48c401d822dfbaacbfbbd9f8c" + }, + { + "path": "global.json", + "bytes": 111, + "sha256": "20c93ca7812d0a9825eef71217156f95ab2f4fe5aadfeb22ebc1c24b630018d3" + }, + { + "path": "packages.lock.json", + "bytes": 306, + "sha256": "612f1930c91b980f19cd58755526d4a78f67117163d761e4ff683817e1eef4af" + } + ], + "outputs": { + "notice-ua1.pdf": "2aab47bbb10a7ff165c62c3d663e1638dc0abd37982e7bfa9501cdc7cdb091d8", + "notice-ua2.pdf": "20df3d1684cfc9dea142d1bf36cfb20790c9f33931979e420a4fe61c0f217fe1", + "notice.png": "445e9b5c417399eb0ada4de06ea10fad89126f2fe8d937c64f40dd57e2c8c87b" + } +} diff --git a/docs/assets/dotnet-accessibility/verapdf-ua1.json b/docs/assets/dotnet-accessibility/verapdf-ua1.json new file mode 100644 index 0000000..b67fdef --- /dev/null +++ b/docs/assets/dotnet-accessibility/verapdf-ua1.json @@ -0,0 +1,63 @@ +{"report":{"buildInformation":{"releaseDetails":[{ + "id" : "core", + "version" : "1.30.2", + "buildDate" : 1780501380000 +},{ + "id" : "validation-model", + "version" : "1.30.2", + "buildDate" : 1780501560000 +},{ + "id" : "apps", + "version" : "1.30.2", + "buildDate" : 1780502100000 +}]},"jobs":[{"itemDetails":{ + "name" : "C:\\dev\\workbench\\fullbleed-official\\target\\growth\\dotnet-accessibility-20261007\\docs-final\\starter-verification\\ua1\\notice.pdf", + "size" : 115082 +},"validationResult":[ { + "details" : { + "passedRules" : 106, + "failedRules" : 0, + "passedChecks" : 3348, + "failedChecks" : 0, + "ruleSummaries" : [ ] + }, + "jobEndStatus" : "normal", + "profileName" : "PDF/UA-1 validation profile", + "statement" : "PDF file is compliant with Validation Profile requirements.", + "compliant" : true +} ],"processingTime":{ + "start" : 1791379588643, + "finish" : 1791379590061, + "difference" : 1418, + "duration" : "00:00:01.418" +}}],"batchSummary":{ + "duration" : { + "start" : 1791379588399, + "finish" : 1791379590135, + "difference" : 1736, + "duration" : "00:00:01.736" + }, + "totalJobs" : 1, + "outOfMemory" : 0, + "veraExceptions" : 0, + "failedEncryptedJobs" : 0, + "validationSummary" : { + "nonCompliantPdfaCount" : 0, + "failedJobCount" : 0, + "compliantPdfaCount" : 1, + "totalJobCount" : 1, + "successfulJobCount" : 1 + }, + "failedParsingJobs" : 0, + "repairSummary" : { + "failedJobCount" : 0, + "totalJobCount" : 0, + "successfulJobCount" : 0 + }, + "featuresSummary" : { + "failedJobCount" : 0, + "totalJobCount" : 0, + "successfulJobCount" : 0 + }, + "multiJob" : false +}}} \ No newline at end of file diff --git a/docs/assets/dotnet-accessibility/verapdf-ua2.json b/docs/assets/dotnet-accessibility/verapdf-ua2.json new file mode 100644 index 0000000..7222906 --- /dev/null +++ b/docs/assets/dotnet-accessibility/verapdf-ua2.json @@ -0,0 +1,63 @@ +{"report":{"buildInformation":{"releaseDetails":[{ + "id" : "core", + "version" : "1.30.2", + "buildDate" : 1780501380000 +},{ + "id" : "validation-model", + "version" : "1.30.2", + "buildDate" : 1780501560000 +},{ + "id" : "apps", + "version" : "1.30.2", + "buildDate" : 1780502100000 +}]},"jobs":[{"itemDetails":{ + "name" : "C:\\dev\\workbench\\fullbleed-official\\target\\growth\\dotnet-accessibility-20261007\\docs-final\\starter-verification\\ua2\\notice.pdf", + "size" : 115301 +},"validationResult":[ { + "details" : { + "passedRules" : 1727, + "failedRules" : 0, + "passedChecks" : 5068, + "failedChecks" : 0, + "ruleSummaries" : [ ] + }, + "jobEndStatus" : "normal", + "profileName" : "PDF/UA-2 + Tagged PDF validation profile", + "statement" : "PDF file is compliant with Validation Profile requirements.", + "compliant" : true +} ],"processingTime":{ + "start" : 1791379595808, + "finish" : 1791379597275, + "difference" : 1467, + "duration" : "00:00:01.467" +}}],"batchSummary":{ + "duration" : { + "start" : 1791379595574, + "finish" : 1791379597365, + "difference" : 1791, + "duration" : "00:00:01.791" + }, + "totalJobs" : 1, + "outOfMemory" : 0, + "veraExceptions" : 0, + "multiJob" : false, + "featuresSummary" : { + "totalJobCount" : 0, + "failedJobCount" : 0, + "successfulJobCount" : 0 + }, + "repairSummary" : { + "totalJobCount" : 0, + "failedJobCount" : 0, + "successfulJobCount" : 0 + }, + "validationSummary" : { + "totalJobCount" : 1, + "compliantPdfaCount" : 1, + "failedJobCount" : 0, + "nonCompliantPdfaCount" : 0, + "successfulJobCount" : 1 + }, + "failedParsingJobs" : 0, + "failedEncryptedJobs" : 0 +}}} \ No newline at end of file diff --git a/docs/assets/dotnet-accessibility/verification.json b/docs/assets/dotnet-accessibility/verification.json new file mode 100644 index 0000000..1b07978 --- /dev/null +++ b/docs/assets/dotnet-accessibility/verification.json @@ -0,0 +1,63 @@ +{ + "ok": true, + "checkedAt": "2026-10-07T13:26:37.436229+00:00", + "sdk": "10.0.401", + "package": "FullBleed.DotNet", + "packageVersion": "0.1.7", + "engineVersion": "2.5.13", + "packageSha256": "4334469bb70fe5896c479f9bb68025cd0fefafa280429dd367055f1818ecd1ee", + "projectZipSha256": "bff5801de233ea3bca593a0656decd739373ec51ab5e9c1f2526d0d67eae8f02", + "freshNugetCache": true, + "nativeLibraryOverride": false, + "isolatedPublishedApp": true, + "commands": [ + "sdk", + "restore", + "publish", + "ua1", + "ua1-repeat", + "ua2", + "ua2-repeat" + ], + "profiles": [ + { + "profile": "ua1", + "pdfSha256": "2aab47bbb10a7ff165c62c3d663e1638dc0abd37982e7bfa9501cdc7cdb091d8", + "previewSha256": "445e9b5c417399eb0ada4de06ea10fad89126f2fe8d937c64f40dd57e2c8c87b", + "textSha256": "b6416f0d3b3bec1137e6d3b4bac864e1c3a3b1289283bcbd43d4cf908e8aa9da", + "repeatPdfBytesIdentical": true, + "veraPDF": { + "version": "1.30.2", + "profile": "ua1", + "failedChecks": 0, + "installerUrl": "https://software.verapdf.org/rel/1.30/verapdf-greenfield-1.30.2-installer.zip", + "installerSha256": "6cc6341cb1af644044054b81f00a6590a7918abb18f762243de115258bcad838" + } + }, + { + "profile": "ua2", + "pdfSha256": "20df3d1684cfc9dea142d1bf36cfb20790c9f33931979e420a4fe61c0f217fe1", + "previewSha256": "445e9b5c417399eb0ada4de06ea10fad89126f2fe8d937c64f40dd57e2c8c87b", + "textSha256": "b6416f0d3b3bec1137e6d3b4bac864e1c3a3b1289283bcbd43d4cf908e8aa9da", + "repeatPdfBytesIdentical": true, + "veraPDF": { + "version": "1.30.2", + "profile": "ua2", + "failedChecks": 0, + "installerUrl": "https://software.verapdf.org/rel/1.30/verapdf-greenfield-1.30.2-installer.zip", + "installerSha256": "6cc6341cb1af644044054b81f00a6590a7918abb18f762243de115258bcad838" + } + } + ], + "runtimeIdentity": { + "runtime": "10.0.12", + "framework": ".NETCoreApp,Version=v10.0", + "rid": "win-x64", + "nativeLibrarySha256": "7252e7add0c8639992bff4d6d01842a0584b68d57db0432e456054138eec41a9" + }, + "scope": "Downloaded starter, retained final PDFs and machine-verifiable PDF/UA checks. No blanket accessibility or assistive-technology claim.", + "validatorReports": { + "verapdf-ua1.json": "a7a8007e02693e46ab9d10214a90b0b58d7a4d79469008f341805c84f7e17723", + "verapdf-ua2.json": "2d56867945fb3394152ee45be9edae80bb2f3c159748d5e6dcacce2c980cd59c" + } +} diff --git a/docs/examples.md b/docs/examples.md index 853253f..6c97039 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -41,6 +41,8 @@ Start with the [invoice guide](guides/invoices.md), or [serve an invoice from FastAPI, Flask, or Django](guides/web-frameworks.md). The Python starter also includes a [tested Docker image](guides/python-docker.md). For C# web applications, try the [ASP.NET Core download starter](guides/aspnet-pdf.md). +For a tagged document, use the [editable C# library notice](guides/csharp-tagged-pdf.md) +with PDF/UA profiles, embedded fonts and retained validation reports. diff --git a/docs/getting-started/dotnet.md b/docs/getting-started/dotnet.md index daf3f58..86b6e0b 100644 --- a/docs/getting-started/dotnet.md +++ b/docs/getting-started/dotnet.md @@ -152,6 +152,8 @@ document and retain the relevant verification evidence before making an accessibility, archival, or print-standard claim. For a web application, download the [ASP.NET Core PDF starter](../guides/aspnet-pdf.md). +For tagged output, use the [C# library-notice starter](../guides/csharp-tagged-pdf.md) +with PDF/UA profiles and independent validation. It includes a download page, styled invoice, private responses, and checks against the published application artifact. diff --git a/docs/guides/csharp-tagged-pdf.md b/docs/guides/csharp-tagged-pdf.md new file mode 100644 index 0000000..b7fa185 --- /dev/null +++ b/docs/guides/csharp-tagged-pdf.md @@ -0,0 +1,122 @@ +--- +title: Create tagged PDFs in C# from HTML and CSS +description: Download a .NET 10 starter for PDF/UA-1 and PDF/UA-2 output with editable HTML/CSS, embedded fonts, semantic tables, alternative text, and retained verification evidence. +--- +# Create tagged PDFs in C\# + +Generate a designed library notice with the `FullBleed.DotNet` NuGet package. +The starter includes editable HTML and CSS, two embedded fonts, a route +illustration and a choice of PDF/UA-1 or PDF/UA-2 output. C# calls the native +Rust engine in your application process. + +[Download the C# starter](../assets/dotnet-accessibility/project.zip){ .md-button .md-button--primary } +[Open the PDF/UA-1 specimen](../assets/dotnet-accessibility/notice-ua1.pdf){ .md-button } +[Open the PDF/UA-2 specimen](../assets/dotnet-accessibility/notice-ua2.pdf){ .md-button } + +[![Riverton Library notice with a large green title, a shaded visit summary, a session table and a straight route to the Learning Room.](../assets/dotnet-accessibility/notice.png)](../assets/dotnet-accessibility/notice-ua1.pdf) + +The event and location are fictional. Use the design and source as a starting +point, then review your own document's semantics and rendered pages. + +## Run the editable example + +Install the .NET 10 SDK. Extract the download, open `fullbleed-tagged-notice`, +and run: + +```sh +dotnet restore --locked-mode +dotnet run -c Release --no-restore -- --profile ua1 --out output/ua1 +dotnet run -c Release --no-restore -- --profile ua2 --out output/ua2 +``` + +Each output directory contains `notice.pdf`, PNG previews rendered from that +PDF, and `render.json` with a SHA-256 digest, inspection and rendering +diagnostics. The application fails on missing glyphs. Templates, fonts and +the SVG are local assets; the document makes no network requests. + +The lockfile pins FullBleed.DotNet 0.1.7, containing engine 2.5.13. The +[C# quickstart](../getting-started/dotnet.md) covers ordinary PDF generation; +the [ASP.NET guide](aspnet-pdf.md) covers HTTP downloads. + +## Configure the output profile + +The relevant options in `Program.cs` are: + +```csharp +using var engine = new FullBleedEngine(new FullBleedEngineOptions +{ + PdfProfile = PdfProfile.PdfUa1, + DocumentLanguage = "en-US", + DocumentTitle = "Your next chapter: Riverton Library workshop", + Assets = [ + FullBleedAsset.FromPath(interPath, FullBleedAssetKind.Font), + FullBleedAsset.FromPath(displayFontPath, FullBleedAssetKind.Font), + FullBleedAsset.FromPath(routePath, FullBleedAssetKind.Svg, "route.svg"), + ], +}); +var result = engine.RenderPdfWithDiagnostics(html, css); +``` + +The complete source resolves these paths from the application's copied +`Assets` directory. Choose `PdfProfile.PdfUa2` for the second profile. Selecting +a profile configures output; it does not establish that your final document +satisfies every accessibility requirement. + +## Preserve the source meaning + +The notice uses one `h1`, ordered `h2` sections, a list, a table with `scope` +on its row and column headers, and a figure with an authored description and +caption. Its source order follows its visual reading sequence. + +```html +
+ A straight, step-free corridor connects the main entrance on the left to the Learning Room on the right. +
Step-free route: main entrance (left) → corridor → Learning Room (right).
+
+``` + +Change the illustration, description and caption together. Keep real headings +and table headers when adjusting the design. Set the language and title for +your actual document, and choose embeddable fonts covering its characters. +The bundled fonts include their OFL notices. + +`example.org/workshops` is printed sample text. This example does not create +an interactive PDF link for that address. + +## Validate the final PDF + +The supplied specimens pass veraPDF 1.30.2's machine checks for their selected +profiles. The [verification record](../assets/dotnet-accessibility/verification.json) +records a fresh public NuGet restore, the published application's runtime and +native-library identity, repeatable PDF bytes and specimen digests. The +[source manifest](../assets/dotnet-accessibility/source.json) identifies every +file in the download. Full validator reports are retained for +[PDF/UA-1](../assets/dotnet-accessibility/verapdf-ua1.json) and +[PDF/UA-2](../assets/dotnet-accessibility/verapdf-ua2.json). + +Inspect the final pages and `render.json`, then run an independent validator +such as [veraPDF](https://docs.verapdf.org/cli/validation/): + +```sh +verapdf --format json -f ua1 output/ua1/notice.pdf +verapdf --format json -f ua2 output/ua2/notice.pdf +``` + +Keep the validator version, exact PDF, source assets, report and digest +together. Repeat these checks after changing your content or design. Review +reading order, heading levels, table relationships, alternative text, +contrast and usefulness with the intended readers and assistive technology. +Machine checks alone do not establish full PDF/UA or WCAG acceptance. + +## Publish the console application + +```sh +dotnet publish -c Release --no-restore -o output/publish +dotnet output/publish/AccessibleNotice.dll --profile ua1 --out output/published +``` + +Keep the complete publish directory, including native libraries, templates, +fonts and notices. The package provides Windows x64, Linux x64 (glibc), Intel +macOS and Apple Silicon macOS binaries. The application needs the matching +.NET runtime; it does not need Python or a browser renderer. diff --git a/examples/dotnet-accessibility/AccessibleNotice.csproj b/examples/dotnet-accessibility/AccessibleNotice.csproj new file mode 100644 index 0000000..dfb077a --- /dev/null +++ b/examples/dotnet-accessibility/AccessibleNotice.csproj @@ -0,0 +1,14 @@ + + + Exe + net10.0 + LatestPatch + enable + enable + true + + + + + + diff --git a/examples/dotnet-accessibility/Assets/DMSerifDisplay-OFL.txt b/examples/dotnet-accessibility/Assets/DMSerifDisplay-OFL.txt new file mode 100644 index 0000000..be384f0 --- /dev/null +++ b/examples/dotnet-accessibility/Assets/DMSerifDisplay-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. Copyright 2019 Google LLC. + +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: http://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/examples/dotnet-accessibility/Assets/DMSerifDisplay-Regular.ttf b/examples/dotnet-accessibility/Assets/DMSerifDisplay-Regular.ttf new file mode 100644 index 0000000..bc5ed0c Binary files /dev/null and b/examples/dotnet-accessibility/Assets/DMSerifDisplay-Regular.ttf differ diff --git a/examples/dotnet-accessibility/Assets/Inter-OFL.txt b/examples/dotnet-accessibility/Assets/Inter-OFL.txt new file mode 100644 index 0000000..21f6aff --- /dev/null +++ b/examples/dotnet-accessibility/Assets/Inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +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/examples/dotnet-accessibility/Assets/Inter-Variable.ttf b/examples/dotnet-accessibility/Assets/Inter-Variable.ttf new file mode 100644 index 0000000..047c92f Binary files /dev/null and b/examples/dotnet-accessibility/Assets/Inter-Variable.ttf differ diff --git a/examples/dotnet-accessibility/Assets/notice.css b/examples/dotnet-accessibility/Assets/notice.css new file mode 100644 index 0000000..7347e4f --- /dev/null +++ b/examples/dotnet-accessibility/Assets/notice.css @@ -0,0 +1,23 @@ +@page { size: A4; margin: 17mm 20mm; } +body { font-family: Inter, sans-serif; font-size: 10.5pt; line-height: 1.45; color: #183c38; } +p { margin: 0 0 8pt; } +.masthead { border-top: 4pt solid #183c38; padding-top: 9pt; font-size: 9pt; font-weight: 700; letter-spacing: 0.5pt; } +.series { margin-left: 34pt; font-size: 8pt; letter-spacing: 0.6pt; } +h1 { font-family: "DM Serif Display", serif; font-size: 42pt; font-weight: 400; line-height: 1.06; margin: 15pt 0 8pt; } +.intro { font-size: 13pt; line-height: 1.4; max-width: 440pt; margin-bottom: 16pt; } +h2 { font-size: 12pt; font-weight: 700; margin: 14pt 0 7pt; break-after: avoid; } +.visit { background: #eef2e5; border-left: 4pt solid #183c38; padding: 11pt 14pt 5pt; } +.visit h2 { margin-top: 0; } +table { width: 100%; border-collapse: collapse; font-size: 9.5pt; } +th, td { text-align: left; padding: 7pt 6pt; border-bottom: 0.7pt solid #a7b4a7; vertical-align: top; } +thead th { background: #183c38; color: #fff; font-weight: 700; } +tbody th { width: 52pt; font-weight: 700; } +tr { break-inside: avoid; } +figure { margin: 0; } +figure img { width: 100%; height: 65pt; } +figcaption { font-size: 9pt; margin-top: 4pt; } +ul { padding-left: 15pt; margin: 0 0 8pt; } +li { padding-left: 2pt; margin: 3pt 0; } +a { color: #183c38; text-decoration: underline; font-weight: 700; } +.more { margin-top: 12pt; padding-top: 9pt; border-top: 1pt solid #183c38; font-size: 9pt; } +.sample-note { color: #455c53; font-size: 8pt; line-height: 1.35; margin-top: 5pt; } diff --git a/examples/dotnet-accessibility/Assets/notice.html b/examples/dotnet-accessibility/Assets/notice.html new file mode 100644 index 0000000..ecd8280 --- /dev/null +++ b/examples/dotnet-accessibility/Assets/notice.html @@ -0,0 +1,38 @@ + + +Your next chapter: Riverton Library workshop + +
+

RIVERTON LIBRARY LEARN TOGETHER / 01

+

Your next chapter.

+

A friendly introduction to finding books, learning online, and making the most of your library.

+
+

Your visit

+

Saturday, 17 October 2026 · 10:00–11:30
Learning Room, ground floor · 24 Example Lane

+

Free to attend. No library card or previous experience needed.

+
+

Session schedule

+ + + + + + + +
TimeWhat we will doDuration
10:00Welcome and a short library tour20 minutes
10:20Find a book and try the online catalogue40 minutes
11:00Explore learning resources and ask questions30 minutes
+

Finding the room

+
+ A straight, step-free corridor connects the main entrance on the left to the Learning Room on the right. +
Step-free route: main entrance (left) → corridor → Learning Room (right).
+
+

Before you arrive

+ +

Information and access requests: example.org/workshops.

+

Fictional event and location. This sample demonstrates PDF authoring; it is not an event invitation.

+
+ + diff --git a/examples/dotnet-accessibility/Assets/route.svg b/examples/dotnet-accessibility/Assets/route.svg new file mode 100644 index 0000000..66f111c --- /dev/null +++ b/examples/dotnet-accessibility/Assets/route.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/dotnet-accessibility/LICENSE b/examples/dotnet-accessibility/LICENSE new file mode 100644 index 0000000..4c99bbd --- /dev/null +++ b/examples/dotnet-accessibility/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Fullbleed Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/dotnet-accessibility/Program.cs b/examples/dotnet-accessibility/Program.cs new file mode 100644 index 0000000..10eee67 --- /dev/null +++ b/examples/dotnet-accessibility/Program.cs @@ -0,0 +1,63 @@ +using System.Security.Cryptography; +using System.Text.Json; +using FullBleed.DotNet; + +var profileName = "ua1"; +var output = "output"; +for (var i = 0; i < args.Length; i++) +{ + if (args[i] is "--help" or "-h") + { + Console.WriteLine("dotnet run -- [--profile ua1|ua2] [--out directory]"); + return; + } + if (args[i] is not ("--profile" or "--out") || i + 1 == args.Length) + throw new ArgumentException("Use --profile ua1|ua2 or --out directory. See --help."); + var option = args[i++]; + if (option == "--profile") profileName = args[i]; + else output = args[i]; +} +var profile = profileName switch +{ + "ua1" => PdfProfile.PdfUa1, + "ua2" => PdfProfile.PdfUa2, + _ => throw new ArgumentException("The profile must be ua1 or ua2."), +}; +var assets = Path.Combine(AppContext.BaseDirectory, "Assets"); +var html = File.ReadAllText(Path.Combine(assets, "notice.html")); +var css = File.ReadAllText(Path.Combine(assets, "notice.css")); +using var engine = new FullBleedEngine(new FullBleedEngineOptions +{ + PdfProfile = profile, + DocumentLanguage = "en-US", + DocumentTitle = "Your next chapter: Riverton Library workshop", + Assets = [ + FullBleedAsset.FromPath(Path.Combine(assets, "Inter-Variable.ttf"), FullBleedAssetKind.Font), + FullBleedAsset.FromPath(Path.Combine(assets, "DMSerifDisplay-Regular.ttf"), FullBleedAssetKind.Font), + FullBleedAsset.FromPath(Path.Combine(assets, "route.svg"), FullBleedAssetKind.Svg, "route.svg"), + ], +}); + +var result = engine.RenderPdfWithDiagnostics(html, css); +if (result.Diagnostics.MissingGlyphs.Count != 0) + throw new InvalidOperationException("The supplied fonts do not cover every character. Inspect MissingGlyphs before distributing the PDF."); +Directory.CreateDirectory(output); +var pdfPath = Path.GetFullPath(Path.Combine(output, "notice.pdf")); +File.WriteAllBytes(pdfPath, result.Pdf); +var inspection = FullBleedEngine.InspectPdf(pdfPath); +var previews = engine.RenderFinalizedPdfImagePagesToDirectory(pdfPath, Path.Combine(output, "preview"), dpi: 120, stem: "notice"); +var report = new +{ + profile = profileName, + runtime = Environment.Version.ToString(), + framework = AppContext.TargetFrameworkName, + runtimeRid = System.Runtime.InteropServices.RuntimeInformation.RuntimeIdentifier, + engine = FullBleedEngine.GetNativeFeatures(), + pdfSha256 = Convert.ToHexString(SHA256.HashData(result.Pdf)).ToLowerInvariant(), + inspection, + diagnostics = result.Diagnostics, + previews, + scope = "Rendering and structural inspection. Run an independent PDF/UA validator and review content and reading order before delivery.", +}; +File.WriteAllText(Path.Combine(output, "render.json"), JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }) + "\n"); +Console.WriteLine($"Created {pdfPath}: {inspection.PageCount} page(s), {profileName}. Previews and render.json are in {Path.GetFullPath(output)}."); diff --git a/examples/dotnet-accessibility/README.md b/examples/dotnet-accessibility/README.md new file mode 100644 index 0000000..858cce5 --- /dev/null +++ b/examples/dotnet-accessibility/README.md @@ -0,0 +1,39 @@ +# Tagged library notice in C# + +A .NET 10 console starter that renders a fictional library workshop notice with +FullBleed.DotNet. Edit the bundled HTML, CSS, route illustration and fonts to +make your own document. Keep the font license notices with redistributed fonts. + +```sh +dotnet restore --locked-mode +dotnet run -c Release --no-restore -- --profile ua1 --out output/ua1 +dotnet run -c Release --no-restore -- --profile ua2 --out output/ua2 +``` + +Each output directory contains `notice.pdf`, native previews of the finalized +PDF, and `render.json` with its hash, inspection and rendering diagnostics. +The application fails if its supplied fonts leave missing glyphs. Output is +generated inside the .NET process using the package's native Rust library. + +`PdfUa1` and `PdfUa2` select the output profile. A profile selection and the +built-in structural inspection do not establish accessibility for your content. +Run an independent PDF/UA validator against the exact final PDF and retain its +report. Review reading order, heading levels, table headers, contrast, image +descriptions and the usefulness of the content itself before delivery. + +The illustration's alternative text describes the same straight corridor as +the visible route. If you change the illustration, update both its description +and caption. `example.org/workshops` is printed sample text, not an interactive +PDF link. All event details and access provisions in this example are fictional. + +To deploy the console application, keep its complete publish output: + +```sh +dotnet publish -c Release --no-restore -o output/publish +dotnet output/publish/AccessibleNotice.dll --profile ua1 --out output/published +``` + +The publish directory includes native libraries, templates, fonts and notices. +Use the matching .NET 10 runtime. The package provides Windows x64, Linux x64 +(glibc), Intel macOS and Apple Silicon macOS binaries. Review the rendered +pages after changing content, dimensions or assets. diff --git a/examples/dotnet-accessibility/global.json b/examples/dotnet-accessibility/global.json new file mode 100644 index 0000000..d46d21e --- /dev/null +++ b/examples/dotnet-accessibility/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/examples/dotnet-accessibility/packages.lock.json b/examples/dotnet-accessibility/packages.lock.json new file mode 100644 index 0000000..5dfdc9e --- /dev/null +++ b/examples/dotnet-accessibility/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "FullBleed.DotNet": { + "type": "Direct", + "requested": "[0.1.7, )", + "resolved": "0.1.7", + "contentHash": "+RYeFZEIPGphSJVdxLOiixv/ZesP6ZxFBYU6u+c60SPQjyCGyhIfbTg5Zs+8K8GKwM5i+B+iNiIifJnhzyCgqg==" + } + } + } +} diff --git a/mkdocs.yml b/mkdocs.yml index ada3526..f688879 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -95,6 +95,7 @@ nav: - ASP.NET Core PDF downloads: guides/aspnet-pdf.md - Variable-data documents: guides/bank-statements.md - Tagged PDF and accessibility: accessibility/overview.md + - Tagged PDFs in C#: guides/csharp-tagged-pdf.md - Print output and PDF/VT: guides/print-output.md - Coding agents and MCP: guides/ai-agents.md - Reference: diff --git a/tools/build_dotnet_accessibility_starter.py b/tools/build_dotnet_accessibility_starter.py new file mode 100644 index 0000000..d664de2 --- /dev/null +++ b/tools/build_dotnet_accessibility_starter.py @@ -0,0 +1,53 @@ +"""Build the editable C# tagged-notice download with stable ZIP bytes.""" +from hashlib import sha256 +import io +import json +from pathlib import Path +import xml.etree.ElementTree as ET +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +PROJECT = ROOT / 'examples/dotnet-accessibility' +ASSETS = ROOT / 'docs/assets/dotnet-accessibility' + + +def digest(data): + return sha256(data).hexdigest() + + +def project_files(): + names = ['AccessibleNotice.csproj', 'Program.cs', 'global.json', 'packages.lock.json', 'README.md', 'LICENSE'] + names += [p.relative_to(PROJECT).as_posix() for p in (PROJECT / 'Assets').rglob('*') if p.is_file()] + return {name: (PROJECT / name).read_bytes() for name in sorted(names)} + + +def archive_bytes(files): + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for name, data in files.items(): + entry = zipfile.ZipInfo('fullbleed-tagged-notice/' + name, date_time=(1980, 1, 1, 0, 0, 0)) + entry.compress_type = zipfile.ZIP_DEFLATED + entry.external_attr = 0o100644 << 16 + archive.writestr(entry, data) + return buffer.getvalue() + + +def build(): + files = project_files() + version = ET.parse(PROJECT / 'AccessibleNotice.csproj').find('.//PackageReference').attrib['Version'] + locked = json.loads((PROJECT / 'packages.lock.json').read_text())['dependencies']['net10.0']['FullBleed.DotNet'] + assert locked['resolved'] == version, 'Restore the final public package before making the download' + archive = archive_bytes(files) + ASSETS.mkdir(parents=True, exist_ok=True) + (ASSETS / 'project.zip').write_bytes(archive) + manifest = {'source': 'https://github.com/fullbleed-engine/docs/tree/main/examples/dotnet-accessibility', + 'framework': 'net10.0', 'package': 'FullBleed.DotNet', 'packageVersion': version, + 'projectZipSha256': digest(archive), + 'files': [{'path': name, 'bytes': len(data), 'sha256': digest(data)} for name, data in files.items()], + 'outputs': {name: digest((ASSETS / name).read_bytes()) for name in ['notice-ua1.pdf', 'notice-ua2.pdf', 'notice.png']}} + (ASSETS / 'source.json').write_text(json.dumps(manifest, indent=2) + '\n', encoding='utf-8', newline='\n') + print(json.dumps({'files': len(files), 'zipBytes': len(archive), 'sha256': digest(archive)})) + + +if __name__ == '__main__': + build() diff --git a/tools/pdfua_validation.py b/tools/pdfua_validation.py new file mode 100644 index 0000000..ab95294 --- /dev/null +++ b/tools/pdfua_validation.py @@ -0,0 +1,54 @@ +"""Pinned, maintainer-only veraPDF checks for downloadable PDF specimens.""" +from hashlib import sha256 +import io +import json +from pathlib import Path +import subprocess +from urllib.request import urlopen +import zipfile + +URL = 'https://software.verapdf.org/rel/1.30/verapdf-greenfield-1.30.2-installer.zip' +INSTALLER_SHA256 = '6cc6341cb1af644044054b81f00a6590a7918abb18f762243de115258bcad838' + + +def validator(cache): + cache.mkdir(parents=True, exist_ok=True) + installer = cache / 'verapdf-1.30.2.zip' + if not installer.exists(): + with urlopen(URL, timeout=120) as response: + data = response.read() + assert sha256(data).hexdigest() == INSTALLER_SHA256 + installer.write_bytes(data) + assert sha256(installer.read_bytes()).hexdigest() == INSTALLER_SHA256 + classes = cache / 'classes' + if not (classes / 'org/verapdf/apps/GreenfieldCliWrapper.class').is_file(): + with zipfile.ZipFile(installer) as archive: + jars = [name for name in archive.namelist() if name.endswith('.jar')] + assert len(jars) == 1 + jar_bytes = archive.read(jars[0]) + with zipfile.ZipFile(io.BytesIO(jar_bytes)) as jar: + packs = [name for name in jar.namelist() if name.endswith('pack-veraPDF CLI')] + assert len(packs) == 1 + pack_bytes = jar.read(packs[0]) + with zipfile.ZipFile(io.BytesIO(pack_bytes)) as pack: + for info in pack.infolist(): + assert (classes / info.filename).resolve().is_relative_to(classes.resolve()) + pack.extractall(classes) + return classes + + +def check(pdf, profile, classpath, out): + pdf = Path(pdf).resolve() + classpath = Path(classpath).resolve() + out.mkdir(parents=True, exist_ok=True) + version = subprocess.check_output(['java', '-cp', str(classpath), 'org.verapdf.apps.GreenfieldCliWrapper', '--version'], text=True) + assert version.startswith('veraPDF 1.30.2\n'), version + with (out / 'verapdf.json').open('w', encoding='utf-8') as stdout, (out / 'verapdf.stderr.txt').open('w', encoding='utf-8') as stderr: + result = subprocess.run(['java', '-cp', str(classpath), 'org.verapdf.apps.GreenfieldCliWrapper', + '--format', 'json', '--maxfailuresdisplayed', '-1', '-f', profile, pdf.name], + cwd=pdf.parent, stdout=stdout, stderr=stderr, timeout=120) + report = json.loads((out / 'verapdf.json').read_text(encoding='utf-8'))['report']['jobs'][0]['validationResult'][0] + assert result.returncode == 0 and report['jobEndStatus'] == 'normal' + assert report['compliant'] and report['details']['failedChecks'] == 0 + return {'version': '1.30.2', 'profile': profile, 'failedChecks': 0, + 'installerUrl': URL, 'installerSha256': INSTALLER_SHA256} diff --git a/tools/verify_dotnet_accessibility.py b/tools/verify_dotnet_accessibility.py new file mode 100644 index 0000000..da2166a --- /dev/null +++ b/tools/verify_dotnet_accessibility.py @@ -0,0 +1,140 @@ +"""Restore the real tagged-notice ZIP and verify its isolated published .NET app.""" +import argparse +from datetime import datetime, timezone +import io +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import zipfile + +from pypdf import PdfReader +from build_dotnet_accessibility_starter import ROOT, ASSETS, project_files, digest +from pdfua_validation import validator, check + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--dotnet', default=shutil.which('dotnet')) + parser.add_argument('--out', type=Path, default=ROOT / 'output/dotnet-accessibility-verification') + parser.add_argument('--verapdf-cp', type=Path) + parser.add_argument('--refresh-evidence', action='store_true', + help='Replace the downloadable reports after all checks pass') + args = parser.parse_args() + assert args.dotnet, 'Install the .NET 10 SDK or pass --dotnet' + dotnet = str(Path(args.dotnet).resolve()) + out = args.out.resolve(); out.mkdir(parents=True, exist_ok=True) + workspace = Path(tempfile.mkdtemp(prefix='tagged notice with spaces ', dir=out)).resolve() + project = workspace / 'fullbleed-tagged-notice' + manifest = json.loads((ASSETS / 'source.json').read_text(encoding='utf-8')) + assert manifest['framework'] == 'net10.0' and manifest['packageVersion'] == '0.1.7' + sources = project_files() + content = (ASSETS / 'project.zip').read_bytes() + assert digest(content) == manifest['projectZipSha256'] + with zipfile.ZipFile(io.BytesIO(content)) as archive: + expected = {'fullbleed-tagged-notice/' + name: data for name, data in sources.items()} + assert len(archive.namelist()) == len(expected) and set(archive.namelist()) == set(expected) + for name, data in expected.items(): + assert archive.read(name) == data + path = workspace / name + assert path.resolve().is_relative_to(workspace) + path.parent.mkdir(parents=True, exist_ok=True); path.write_bytes(data) + assert len(manifest['files']) == len(sources) + for entry in manifest['files']: + data = sources[entry['path']] + assert digest(data) == entry['sha256'] and len(data) == entry['bytes'] + env = dict(os.environ, DOTNET_ROOT=str(Path(dotnet).parent), DOTNET_ROLL_FORWARD='LatestPatch', + NUGET_PACKAGES=str(workspace / 'nuget-cache'), NUGET_HTTP_CACHE_PATH=str(workspace / 'nuget-http-cache'), + DOTNET_CLI_TELEMETRY_OPTOUT='1') + env.pop('FULLBLEED_NATIVE_LIBRARY', None) + commands = [] + def run(label, arguments, cwd=project): + result = subprocess.run([dotnet, *map(str, arguments)], cwd=cwd, env=env, + capture_output=True, text=True, encoding='utf-8', timeout=240) + (out / (label + '.stdout.txt')).write_text(result.stdout, encoding='utf-8') + (out / (label + '.stderr.txt')).write_text(result.stderr, encoding='utf-8') + assert result.returncode == 0, f'{label} failed; see {out}' + commands.append(label) + return result.stdout + sdk = run('sdk', ['--version']).strip(); assert sdk.startswith('10.0.') + run('restore', ['restore', '--locked-mode', '--source', 'https://api.nuget.org/v3/index.json']) + published = workspace / 'isolated-publish' + run('publish', ['publish', '-c', 'Release', '--no-restore', '-o', published]) + package_dir = workspace / 'nuget-cache/fullbleed.dotnet/0.1.7' + provenance = json.loads((package_dir / 'native-provenance.json').read_text()) + engine = next(p for p in provenance['dependencies'] if p['name'] == 'fullbleed') + assert engine['version'] == '2.5.13' + classpath = args.verapdf_cp or validator(out / 'validator-cache') + records = [] + native_identity = None + for profile in ['ua1', 'ua2']: + folder = out / profile + run(profile, [published / 'AccessibleNotice.dll', '--profile', profile, '--out', folder], cwd=workspace) + run(profile + '-repeat', [published / 'AccessibleNotice.dll', '--profile', profile, '--out', out / (profile + '-repeat')], cwd=workspace) + pdf = folder / 'notice.pdf' + assert pdf.read_bytes() == (out / (profile + '-repeat') / 'notice.pdf').read_bytes() + assert pdf.read_bytes() == (ASSETS / f'notice-{profile}.pdf').read_bytes(), 'Review changed sample PDF' + previews = list((folder / 'preview').glob('*.png')); assert len(previews) == 1 + assert previews[0].read_bytes() == (ASSETS / 'notice.png').read_bytes(), 'Review changed sample preview' + report = json.loads((folder / 'render.json').read_text()) + assert report['profile'] == profile and report['pdfSha256'] == digest(pdf.read_bytes()) + assert report['runtime'].startswith('10.0.') and report['framework'] == '.NETCoreApp,Version=v10.0' + assert report['engine']['BindingVersion'] == '0.1.7' + native_name = {'win-x64': 'fullbleed_dotnet_native.dll', 'linux-x64': 'libfullbleed_dotnet_native.so', + 'osx-x64': 'libfullbleed_dotnet_native.dylib', 'osx-arm64': 'libfullbleed_dotnet_native.dylib'}[report['runtimeRid']] + relative_native = Path('runtimes') / report['runtimeRid'] / 'native' / native_name + assert (published / relative_native).read_bytes() == (package_dir / relative_native).read_bytes() + identity = {'runtime': report['runtime'], 'framework': report['framework'], 'rid': report['runtimeRid'], + 'nativeLibrarySha256': digest((published / relative_native).read_bytes())} + assert native_identity is None or native_identity == identity + native_identity = identity + assert not report['diagnostics']['MissingGlyphs'] + reader = PdfReader(pdf, strict=True) + assert len(reader.pages) == 1 + catalog = reader.trailer['/Root'] + assert catalog['/Lang'] == 'en-US' and catalog['/MarkInfo']['/Marked'] + assert catalog['/StructTreeRoot'].get('/K') is not None + text = ' '.join(reader.pages[0].extract_text().split()) + for phrase in ['Your next chapter.', 'Your visit', 'Session schedule', 'Finding the room', + 'Before you arrive', 'example.org/workshops', 'Fictional event and location.']: + assert text.count(phrase) == 1, phrase + (folder / 'text.txt').write_text(text + '\n', encoding='utf-8') + validation = check(pdf, profile, classpath, folder) + records.append({'profile': profile, 'pdfSha256': digest(pdf.read_bytes()), + 'previewSha256': digest(previews[0].read_bytes()), 'textSha256': digest(text.encode()), + 'repeatPdfBytesIdentical': True, 'veraPDF': validation}) + assert records[0]['previewSha256'] == records[1]['previewSha256'] + assert records[0]['textSha256'] == records[1]['textSha256'] + for name, expected_hash in manifest['outputs'].items(): + assert digest((ASSETS / name).read_bytes()) == expected_hash + result = {'ok': True, 'checkedAt': datetime.now(timezone.utc).isoformat(), 'sdk': sdk, + 'package': 'FullBleed.DotNet', 'packageVersion': '0.1.7', 'engineVersion': engine['version'], + 'packageSha256': digest((package_dir / 'fullbleed.dotnet.0.1.7.nupkg').read_bytes()), + 'projectZipSha256': manifest['projectZipSha256'], 'freshNugetCache': True, + 'nativeLibraryOverride': False, 'isolatedPublishedApp': True, 'commands': commands, 'profiles': records, + 'runtimeIdentity': native_identity, + 'scope': 'Downloaded starter, retained final PDFs and machine-verifiable PDF/UA checks. No blanket accessibility or assistive-technology claim.'} + if args.refresh_evidence: + result['validatorReports'] = {} + for profile in ['ua1', 'ua2']: + report_bytes = (out / profile / 'verapdf.json').read_bytes() + name = f'verapdf-{profile}.json' + (ASSETS / name).write_bytes(report_bytes) + result['validatorReports'][name] = digest(report_bytes) + (ASSETS / 'verification.json').write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8') + retained = json.loads((ASSETS / 'verification.json').read_text(encoding='utf-8')) + assert retained['ok'] + for key in ['packageVersion', 'engineVersion', 'packageSha256', 'projectZipSha256', 'profiles']: + assert retained[key] == result[key], f'Refresh the published verification record: {key}' + assert set(retained['validatorReports']) == {'verapdf-ua1.json', 'verapdf-ua2.json'} + for name, expected_hash in retained['validatorReports'].items(): + assert digest((ASSETS / name).read_bytes()) == expected_hash + result['publishedEvidenceVerified'] = True + (out / 'verification.json').write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8') + print(json.dumps({'ok': True, 'profiles': 2, 'packageVersion': '0.1.7', 'engineVersion': engine['version']})) + + +if __name__ == '__main__': + main()