Bhowra.Ink
1.0.7
dotnet add package Bhowra.Ink --version 1.0.7
NuGet\Install-Package Bhowra.Ink -Version 1.0.7
<PackageReference Include="Bhowra.Ink" Version="1.0.7" />
<PackageVersion Include="Bhowra.Ink" Version="1.0.7" />
<PackageReference Include="Bhowra.Ink" />
paket add Bhowra.Ink --version 1.0.7
#r "nuget: Bhowra.Ink, 1.0.7"
#:package Bhowra.Ink@1.0.7
#addin nuget:?package=Bhowra.Ink&version=1.0.7
#tool nuget:?package=Bhowra.Ink&version=1.0.7
Bhowra.Ink — HTML to PDF for .NET, C# & Azure Functions
Serverless HTML-to-PDF for .NET — without a browser. Bhowra.Ink is a zero-dependency,
pure-C# engine that converts modern HTML5 + CSS (Grid, flexbox, var()/calc(), gradients,
border-radius, box-shadow, transforms, SVG) straight to PDF, in-process — with no
Chromium, no headless browser, and no native binaries.
A fully managed alternative to headless-browser (Chromium-based) converters and AGPL-licensed PDF libraries. No AGPL. No cold start. Zero third-party dependencies.
Use it for HTML-to-PDF in C#, ASP.NET Core, Blazor, Azure Functions, and AWS Lambda — invoices, hybrid e-invoices (Factur-X / ZUGFeRD, ZATCA), payslips, statements, reports, and contracts — where launching a headless browser per request is too slow, too heavy, or simply not allowed.
Install
dotnet add package Bhowra.Ink
Targets .NET 8. One managed DLL (under 1 MB — 880 KB, fonts embedded). Works on Windows and Linux, including Azure Functions / AWS Lambda / containers.
Quickstart
using Bhowra.Ink;
string html = "<h1 style='color:teal'>Hello, PDF</h1><p>Rendered in pure .NET.</p>";
// Simplest: straight to a file
HtmlConverter.ConvertToFile(html, "out.pdf");
// Or get bytes + a result you can inspect
ConversionResult result = HtmlConverter.Convert(html, new ConverterProperties { Title = "Demo" });
Console.WriteLine($"{result.PageCount} page(s), {result.SizeBytes} bytes, {result.ConversionTimeMs} ms");
result.SaveToFile("out.pdf");
Output options: ConvertToFile, ConvertToBytes, ConvertToStream, ConvertFileToFile,
and Convert (returns a ConversionResult with bytes, page count, timing, and diagnostics).
Convert HTML to PDF in Azure Functions (serverless)
Because Bhowra.Ink runs in-process with no Chromium and no native binaries, it works on
Azure Functions, AWS Lambda, and Linux containers with no cold-start browser launch and
nothing to install on the host — just add the package and call HtmlConverter:
[Function("InvoicePdf")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
string html = await new StreamReader(req.Body).ReadToEndAsync();
byte[] pdf = HtmlConverter.ConvertToBytes(html);
var res = req.CreateResponse(System.Net.HttpStatusCode.OK);
res.Headers.Add("Content-Type", "application/pdf");
await res.Body.WriteAsync(pdf);
return res;
}
The same code runs unchanged on Windows Consumption plans, Linux, AWS Lambda, and Docker — there are no per-platform native builds to manage.
Why Bhowra.Ink? (vs headless-browser converters & AGPL/commercial PDF libraries)
| Concern | Chromium / headless-browser converters | AGPL or commercial PDF libraries | Bhowra.Ink |
|---|---|---|---|
| Cold start (serverless) | 5–15 s to launch a browser | varies | in-process, no browser launch |
| Dependencies | Chromium + native binaries | AGPL or per-seat commercial | zero — pure .NET 8 BCL |
| Deployment size | 150–400 MB | tens of MB | under 1 MB (880 KB), one DLL |
| Linux / Windows | often native-only | varies | cross-platform managed |
| Modern CSS (Grid, gradients, SVG) | yes | frequently weak | yes |
| License | — | often AGPL or costly | commercial; watermark-on-trial |
What it renders
CSS & layout
- Full CSS cascade — specificity, inheritance, shorthand expansion,
!important, inline styles - External stylesheets —
<link rel="stylesheet">and@importresolved into the cascade in document order (secure-by-default; see External stylesheets) - Selectors: type, class, id, attribute, descendant/child/adjacent/sibling,
:nth-child,:not,::before/::after(incl. counters) - Flexbox and CSS Grid (incl.
repeat(),minmax(),auto-fit/auto-fill) var(),calc(),position(relative/absolute/fixed + z-index),float, multi-column- Gradients (linear / radial / conic),
border-radius,box-shadow,filter: blur, transforms (rotate/scale/translate), opacity & alpha compositing - Tables (colspan, rowspan,
border-collapse, repeating headers), lists, nested markers
Documents & paging
- Automatic pagination,
page-break-*,@pagesize/margins - Running headers/footers (
HeaderHtml/FooterHtmlwith{{page}}/{{pages}}) - Bookmarks/outlines from headings, internal links, link annotations
- Interactive AcroForm fields and embedded file attachments
Graphics & fonts
- SVG: paths, shapes, gradients,
clipPath,<use>/<symbol>, transforms - Images: PNG (incl. alpha/soft-mask + 16-bit), JPEG (incl. CMYK + Exif orientation), GIF, BMP, WebP (lossless),
data:URIs - Base-14 fonts (no files needed) + TrueType/OpenType embedding (Identity-H), WOFF/WOFF2, subsetting,
@font-face - Right-to-left & complex scripts — Arabic (OpenType cursive joining + GPOS harakat) and Hebrew with bidirectional (mixed LTR/RTL) text, plus Devanagari and Thai — bundled Noto fonts, no setup
Output targets
- Encryption: passwords with RC4-128 or AES-256
- PDF/A-2b and PDF/A-3b (archival — all fonts embedded automatically), PDF/UA-1 + Tagged PDF (accessibility), XMP metadata
- Hybrid e-invoices — PDF/A-3 with embedded associated files (
/AFRelationship), EU Factur-X / ZUGFeRD, and a built-in QR-code generator (see E-invoicing) - Configurable diagonal watermark
E-invoicing — Factur-X / ZUGFeRD, ZATCA hybrid, and QR
Produce the hybrid invoice PDF that modern e-invoicing flows expect: a human-readable PDF/A-3 with the structured XML embedded inside it, plus a scannable QR — all in pure .NET, no extra packages.
PDF/A-3 with an embedded associated file
using Bhowra.Ink;
byte[] xml = File.ReadAllBytes("invoice.xml");
var props = new ConverterProperties { PdfA = PdfAConformance.PdfA3B }
.AttachFile("invoice.xml", xml, "application/xml", afRelationship: "Alternative");
byte[] pdf = HtmlConverter.ConvertToBytes(invoiceHtml, props);
EU Factur-X / ZUGFeRD — SetFacturX embeds the Cross-Industry Invoice XML under the mandated filename
(factur-x.xml, or xrechnung.xml for XRECHNUNG), writes the fx: XMP schema with its required PDF/A
extension declaration, and applies a profile-correct /AFRelationship:
byte[] cii = File.ReadAllBytes("factur-x.xml");
var props = new ConverterProperties { PdfA = PdfAConformance.PdfA3B }
.SetFacturX(cii, FacturXProfile.En16931);
byte[] pdf = HtmlConverter.ConvertToBytes(invoiceHtml, props);
Profiles: Minimum, BasicWL, Basic, En16931, Extended, XRechnung.
This fits enterprise EDI, ERP, and SAP S/4HANA flows that need a PDF/A-3 hybrid invoice carrier:
Bhowra.Ink builds the document layer (rendered invoice PDF + embedded CII XML + Factur-X/XRechnung
metadata). It is not an EDI network, validation service, or SAP connector; generate and validate the
business XML in your platform, then pass it to SetFacturX.
Built-in QR — a zero-dependency ISO/IEC 18004 generator (byte mode, versions 1–40, error-correction
L/M/Q/H), drawn as a crisp vector with the mandatory quiet zone — sized for payloads up to the
~700-character range (e.g. a Saudi TLV string).
For Saudi invoice QR data, ZatcaTlv builds the TLV payload: typed helpers for tags 1-5, and raw
caller-supplied bytes for Phase 2 tags 6-9 (invoice hash, signature, public key, stamp signature). It
does not generate, sign, stamp, validate, or submit the invoice XML.
using Bhowra.Ink;
using Bhowra.Ink.QrCode;
byte[] tlvBytes = ZatcaTlv.BuildPhase1Bytes(
seller: "Najd Trading Company",
vatNo: "311111111100003",
ts: DateTimeOffset.Parse("2026-06-28T10:15:00Z"),
total: "2472.50",
vat: "322.50");
var props = new ConverterProperties
{
QrCode =
{
Data = tlvBytes, // any bytes; null/empty = no QR (output unchanged)
X = 60f, // points from the page's left edge
Y = 70f, // points from the page's bottom edge
Size = 120f, // points (includes the quiet zone)
Page = 1, // 1-based; 0 = every page
EcLevel = QrEcLevel.M, // L / M / Q / H
QuietZoneModules = 4, // ISO minimum
},
};
Configuration
ConverterProperties is the single config object (every member is documented for IntelliSense):
var props = new ConverterProperties
{
Title = "Invoice 2026-001",
Author = "Acme Corp",
WatermarkText = "CONFIDENTIAL", // diagonal; always honoured, licensed or not
WatermarkOpacity = 0.15f,
WatermarkColor = "#BFBFBF",
UserPassword = "secret", // encrypts the PDF
Aes256 = true, // AES-256 instead of the default RC4-128
PdfA = PdfAConformance.PdfA2B, // archival conformance (fonts auto-embedded)
Tagged = true, // accessible structure (PDF/UA)
MissingGlyphBehavior = MissingGlyphBehavior.Diagnostic, // never silently drop glyphs
}
.SetPageA4() // or SetPageLetter() / SetPageLegal() / SetLandscape()
.SetMargins(36f); // points (1 pt = 1/72")
byte[] pdf = HtmlConverter.ConvertToBytes(html, props);
Page size/margins can also be set explicitly via PageWidth/PageHeight and
MarginTop/Right/Bottom/Left (all in points). Defaults: A4 portrait, 36 pt margins.
External stylesheets (<link rel="stylesheet"> and @import)
The engine resolves external CSS — <link rel="stylesheet"> and the @imports those sheets (and inline
<style> blocks) contain — and cascades it with embedded styles in correct document order. It is
secure-by-default: local stylesheets under BaseUri resolve out of the box; remote (http/https)
fetching is opt-in.
// Local stylesheets (the common case) — resolved relative to BaseUri, no extra setup:
var props = new ConverterProperties { BaseUri = templateDir }; // <link href="css/site.css"> just works
// Opt in to remote stylesheets (CDN), keeping every guard on:
props.ExternalCss.EnableRemote(); // or .Mode = ExternalCssMode.LocalAndRemote
props.ExternalCss (an ExternalCssOptions) carries the policy and limits:
| Setting | Default | Purpose |
|---|---|---|
Mode |
LocalOnly |
Disabled / LocalOnly / LocalAndRemote |
AllowHttp |
false |
allow plain http:// (otherwise HTTPS-only) |
AllowPrivateNetwork |
false |
the SSRF guard — blocks private/loopback/link-local IPs and the cloud metadata endpoint |
MaxBytesPerFile / MaxBytesTotal |
2 MiB / 8 MiB | size caps (DoS guard) |
MaxStylesheets / MaxImportDepth |
30 / 5 | count + @import recursion caps |
TimeoutSeconds / MaxRedirects |
10 / 5 | per-fetch timeout; each redirect is SSRF-re-validated |
FailOnError |
false |
fail-open (skip a bad sheet + record a diagnostic) vs. fail-closed |
AllowedHosts / CssFetcher |
— | optional host allow-list; optional custom fetcher (e.g. Managed Identity) |
Behaviour matches how a strict print renderer treats CSS: the print media is the target, so
media="screen" links are ignored and media="print"/all apply; @import cycles are detected
(ancestor-chain) and depth-bounded; charset follows BOM → HTTP charset → @charset → UTF-8. Skipped or
blocked stylesheets are reported in ConversionResult.ImageDiagnostics.
Images: remote & local sources (secure-by-default)
Out of the box an <img> is embedded only from a data: URI. Both remote and local file sources
are opt-in, so converting HTML you don't fully trust makes no outbound network calls and reads no local
files — no SSRF and no local-file disclosure by default.
<img src> |
Default | Enable |
|---|---|---|
data: URI |
embedded | — |
Remote http(s):// |
not fetched | props.AllowRemoteImages = true (or supply an ImageFetcher). Fetches then pass the SSRF guard — private/loopback/link-local IPs and the cloud metadata endpoint blocked, redirect-to-internal and DNS-rebind defeated, 64 MiB response cap. |
| Local file path | not read | props.AllowLocalFiles = true — only for trusted HTML. Off by default so an untrusted <img src="/etc/passwd"> can't embed an arbitrary local file. |
// Trusted template with local logo assets + CDN images:
var props = new ConverterProperties { AllowLocalFiles = true, AllowRemoteImages = true };
Skipped or blocked images are reported in ConversionResult.ImageDiagnostics.
Image → PDF
Turn one or more images into a PDF — one image per page — with ImageToPdf. It routes through the same
renderer, so it inherits the full image stack: JPEG embedded losslessly (DCTDecode passthrough),
PNG/WebP/BMP/GIF decoding, RGB/Gray/CMYK color spaces, EXIF auto-orientation, alpha (/SMask), and
image de-duplication.
// One image, fit to A4 (default), preserving aspect ratio:
byte[] pdf = ImageToPdf.ToBytes("scan.jpg");
// Several images → multi-page, page sized to each image (no margins):
ImageToPdf.ToFile(
new[] { ImageInput.FromFile("p1.png"), ImageInput.FromBytes(jpegBytes) },
"album.pdf",
new ImageToPdfOptions { PageSize = ImagePageSize.FitToImage });
// A4 with margins, centered, cropped to fill, at print resolution:
var opts = new ImageToPdfOptions
{
PageSize = ImagePageSize.A4, // or Letter / Legal / Custom / FitToImage
Fit = ImageFit.Cover, // Contain (default) / Cover / Stretch / None
MarginPt = 36,
Dpi = 300, // px→pt for FitToImage (300 = print scans)
Background = "#ffffff",
};
byte[] album = ImageToPdf.ToBytes(new[] { ImageInput.FromFile("photo.jpg") }, opts);
Inputs are files, byte[], or Stream (ImageInput.FromFile/FromBytes/FromStream). ImagePageSize.FitToImage
sizes the page to the image; with multiple differently-sized images the page is sized to the largest and the
rest are centered (one PDF uses a single page size).
Fonts & non-Latin text
props.RegisterFont("Inter", File.ReadAllBytes("Inter.ttf")); // embed a custom family
Bundled Noto fonts (Arabic, Hebrew, Thai, Devanagari) ship inside the DLL, so common non-Latin scripts render with no extra setup. Anything a registered or bundled font can't draw is reported — never silently dropped:
var result = HtmlConverter.Convert(html, props);
foreach (var d in result.FontDiagnostics)
Console.WriteLine(d); // e.g. characters with no available glyph + how to fix
Licensing & trial
Bhowra.Ink is commercial software that runs fully without a key — it never expires, never imposes a page limit, and never throws based on license state. Unlicensed output simply carries a horizontal "TRIAL VERSION" mark (plus a clickable licensing banner). Activating a valid license removes that trial mark; any caller-configured watermark is independent and unaffected. A free, time-limited evaluation key for clean output is available on request.
See LICENSE.txt for the full agreement. Licensing & sales: sales@bhowra.com.
Architecture (in brief)
HtmlConverter → HtmlParser (DOM) + CssParser/CssCascade (computed styles) →
LayoutTreeBuilder (typed layout tree) → PdfWriter (paginate, render, emit PDF bytes).
Everything — the HTML parser, CSS engine, font/image decoders, and the PDF writer — is
implemented from scratch on the .NET 8 base class library, with no third-party packages.
Known limitations
| Area | Status |
|---|---|
| PDF/A (2b / 3b) + watermark / form fields | Not combinable — watermarks, the trial mark, and form fields draw with non-embeddable standard-14 fonts, so PDF/A throws a clear error. Plain text + registered fonts embed automatically; PDF/A needs a licensed build (no trial mark). |
| Persian/Urdu extended Arabic letters | Beyond Modern Standard Arabic — not yet cursively joined |
| CJK scripts | Require a font via RegisterFont(...) or @font-face (not bundled) |
| JavaScript / SPAs / live web pages | Out of scope — renders HTML/CSS, does not execute scripts |
Remote @font-face URLs |
Not fetched (local files and data: URIs supported) |
| TIFF / JPEG-2000 images | No decoder — surfaced via result.ImageDiagnostics (never silently dropped, no black box) |
| Lossy / animated WebP | Only lossless (VP8L) WebP is decoded |
border-collapse shared-border dedup |
Borders drawn per cell (no dedup yet) |
| Cross-host font fallback | Text in base-14 and the bundled Noto/Roboto fonts (Latin, Arabic, Hebrew, Thai, Devanagari) renders identically everywhere; glyphs outside that set fall back to host system fonts, so their metrics can differ across environments (e.g. Windows Arial vs Linux DejaVu). Bundle or RegisterFont(...) a font for byte-identical output on every host. |
| Gradient as a page background | Element gradients (linear / radial / conic on a box) render as real PDF shadings; a gradient set on body/html is not yet painted as the full-page background (a solid page colour is) |
backdrop-filter |
Not applied — the blur of content behind a translucent box is a no-op (the box's own background, borders and shadows still render) |
License
Commercial license — see LICENSE.txt. Bundled Noto fonts are licensed under the
SIL Open Font License 1.1 (see OFL.txt) and remain governed by the OFL.
Links
- Product & docs: https://bhowra.com/bhowra-ink.html
- Licensing & sales: sales@bhowra.com
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
1.0.7 — ZATCA QR TLV builder and QR options grouping. New ZatcaTlv builds the Base64-encoded Tag-Length-Value payload for the Saudi ZATCA invoice QR: typed helpers for tags 1-5 (seller name as UTF-8, VAT number, ISO-8601 UTC timestamp, invoice total, VAT total) and raw caller-supplied bytes for Phase 2 tags 6-9 (invoice hash, ECDSA signature, public key, stamp signature), with a one-byte length cap and BuildPhase1/BuildPhase2 convenience helpers. It PACKS values only — it does not compute the hash or signature, generate the UBL/CII invoice XML, cryptographically stamp, or submit to the FATOORA portal; those remain the caller's responsibility (this is the presentation/document layer, not a compliance platform). QR configuration is now grouped into a QrCodeOptions object accessed via ConverterProperties.QrCode (Data/X/Y/Size/Page/EcLevel/QuietZoneModules), mirroring the ExternalCss options; the former flat ConverterProperties.QrCode* properties remain as source-compatible [Obsolete] forwarding shims over it. Internal/quality: the QR round-trip decoder that proves the encoder is correct was moved out of the shipped assembly into the test project, so no test-only code ships in the library. Output is byte-for-byte unchanged for documents that do not use the QR/e-invoicing features. 1.0.6 — Encryption hardening, table-layout fixes, and internal modularization (output byte-for-byte unchanged for unencrypted documents). Encryption: password-protected PDFs now encrypt EVERY dictionary-level string — form-field names/values and the /DA default-appearance, link /URI actions, GoTo destination names, the names tree, embedded-file names/descriptions, /Alt text and /Lang — plus the text-field appearance streams; previously several of these were emitted as plaintext, so a conforming viewer would decrypt them to garbage (broken links, unreadable field values). Radio buttons sharing a name are now grouped under one parent field with per-widget export states, so selecting one clears its name-mates; non-ASCII radio export values are UTF-8/byte-correct in PDF names. The RC4 owner password is now honoured (was silently ignored), encrypted content streams are Flate-compressed (were left uncompressed), and requesting PDF/A together with a password now fails loud (PDF/A forbids encryption) instead of quietly dropping it. Table layout: a rowspan cell beside an auto-width column no longer leaves a blank unpainted gap — row-height and column-width measurement now resolve each cell to its true column across active rowspans (matching the render pass); and word-break:break-all no longer inflates its column's minimum width and starves the sibling columns. Layout: aspect-ratio on a definite width now recomputes the derived height when a render-time max-width clamp narrows the box, and browser-style fit-to-page scaling no longer compounds across the two-pass target-counter (cross-reference) render. Image security: a crafted PNG chunk length can no longer overflow the 32-bit bounds check (long arithmetic), and IDAT decompression is capped to the size the image header declares — rejecting a decompression bomb before it can exhaust memory. Internals/quality: the large PDF writer is split into focused partial-class files, and the PNG decoder and the PDF Standard-Security cryptographic primitives (RC4 / AES-256 R6 Hash2B) are now standalone types with direct unit tests, including an automated AES-256 decrypt round-trip. 1.0.5 — Browser-parity rendering fixes and a golden-image regression harness. Four layout/text defects fixed, each locked by a test that fails on the prior code: an inline <svg> with width/height="100%" now resolves against its container (with a viewBox aspect-ratio fallback for auto height) instead of a fixed default, so it no longer overflows its box; grid-column: 1 / -1 now spans the full column set (negative grid lines resolve to the last line) instead of collapsing to a single track; an absolutely-positioned box with width:auto now shrinks to fit its content (CSS 2.1 §10.3.7) instead of stretching to the containing block; and Arabic-Indic numerals (U+0660–0669 / U+06F0–06F9) now render left-to-right as numbers instead of being reversed as if RTL, so an Arabic-Indic price such as 45.00 no longer renders as 00.54. Also adds a golden-image regression harness (rasterizes generated PDFs to fingerprints and fails the build on structural drift) and a documented capability contract for in-scope vs out-of-scope features. Security (behavior change): remote (http/https) <img> fetching is now OFF by default — opt in via ConverterProperties.AllowRemoteImages (or by supplying an ImageFetcher), matching the existing external-CSS LocalOnly policy; when enabled, image fetches go through the same SSRF guard (private/link-local/cloud-metadata addresses blocked, redirect-to-internal and DNS rebind defeated, 64 MiB response cap). Callers that relied on remote images rendering by default must now enable them. Plus code-review CSS-correctness fixes: grid-column / grid-area negative line numbers (e.g. 1 / -1) span to the last track, non-rectangular grid-template-areas are rejected per spec, a ::before/::after counter-reset no longer leaks its counter scope, and a definite-zero percentage-height parent resolves the child to 0 rather than auto. Security (behavior change): reading a LOCAL (non-data, non-http) <img> file is now OFF by default — opt in via ConverterProperties.AllowLocalFiles for TRUSTED HTML only, so converting untrusted HTML can no longer disclose an arbitrary local file (e.g. <img src="/etc/passwd">) into the PDF; callers that render local image assets must enable it. More browser-parity fixes, each locked by a test: aspect-ratio on a PERCENTAGE width now derives the box height (a responsive width:100% + aspect-ratio box is no longer zero-height); a uniform border-radius percentage resolves against the element's OWN width instead of a fixed default (a %/auto-width card is no longer over-rounded); a <select>'s <option value> export values are preserved in the AcroForm /Opt array instead of being collapsed to the display label; StyleApplier is now idempotent, so font-variant:small-caps no longer compounds its 0.8x shrink when a style is re-applied (e.g. via a legacy table bgcolor attribute); and a pathologically deep DOM is depth-bounded so it degrades gracefully instead of overflowing the stack. Hardening: Azure SAS signatures are redacted from image diagnostics and the image-download response is disposed. 1.0.4 — CSS Grid, flexbox, and browser-style fit-to-page. New unified CSS Grid engine: rows now stretch and distribute height (fr/minmax/auto tracks, align-content) the way columns do, track sizes honour every CSS unit (px/%/em/rem/…), a span grows the implicit grid, and a grid with no grid-template-columns (implicit columns, or columns from grid-template-areas) now sizes its columns from content instead of collapsing to zero width and rendering invisible. Grid item text no longer disappears under the grid container's own background (paint-order fix). New fit-to-page scaling (ConverterProperties.PageScaling, default ShrinkToFit): a design wider than the page is laid out at its natural width and the whole page is scaled to fit — with link, bookmark and destination coordinates scaled to match — instead of collapsing or paginating; PageScaling.None keeps the previous behaviour. Flexbox: a nested flex row is measured at its true (cross-axis) height rather than a block stack; justify-content space-between/around/evenly/center/end now distributes on a stretched or min-height column (e.g. a card pinning its footer to the bottom); and such a column no longer spreads its items below the page bottom when it cannot fit (falls back to top-packing, no content loss). Plus min-height/min-width and nested grid/flex measurement fixes. 1.0.3 — External stylesheets, Image-to-PDF, and rendering-correctness fixes. New external CSS resolution: <link rel="stylesheet"> and @import are fetched and folded into the cascade in document order, secure by default — local-only unless remote is explicitly enabled, with SSRF protection (private/link-local/cloud-metadata address blocking, per-redirect re-validation, connect-time IP pinning), per-file and total byte caps, a stylesheet-count cap, @import cycle and depth guards, and BOM/header/@charset-correct decoding. New Image-to-PDF API (ImageToPdf): turn one or more images (JPEG/PNG/WebP/BMP/GIF) into a PDF, one image per page, reusing the engine's image pipeline (JPEG DCTDecode passthrough, DeviceRGB/Gray/CMYK, Adobe inverted-CMYK, EXIF orientation, alpha to soft-mask, SHA-256 dedup, Flate compression) with Contain/Cover/Stretch/None fit modes and fixed or fit-to-image page sizing. Rendering correctness: line-height:normal now derives per font from the font's own hhea metrics (fixes overlapping wrapped bold lines); nested non-table block content and long paragraphs now paginate across pages instead of overflowing off-page; a box's background and borders are sliced across page breaks (box-decoration-break: slice); position:fixed/absolute elements anchor to the CSS page area so running headers/footers land correctly on every page; percentage heights resolve against the containing block's height; text placed directly inside a flex or grid box inherits the container's font-size, weight and colour; undefined utility class names no longer synthesize styles in author-styled documents. 1.0.2 — Hybrid invoices, QR, and expanded search tags. New PDF/A-3b conformance (PdfAConformance.PdfA3B) embeds arbitrary files (e.g. an invoice XML) as associated files with a per-file /AFRelationship. New EU Factur-X / ZUGFeRD support (SetFacturX): the structured CII XML is embedded under the mandated filename with the fx: XMP schema and its required PDF/A extension-schema declaration, and a profile-correct /AFRelationship. This is also the PDF/A-3 hybrid that Saudi ZATCA permits as the human-readable buyer copy (the signed XML, cryptographic stamp and FATOORA submission remain the caller's responsibility — this is the document layer, not a compliance platform). New zero-dependency QR generator (QrCodeData): byte mode, versions 1–40, all four EC levels, drawn as a crisp vector Form XObject with the mandatory quiet zone — sized for the ~500–700-char ZATCA TLV payload. Also expanded NuGet search tags. 1.0.1 — PDF/A-2b now embeds all fonts automatically, including the default font-less path (a small OFL font is bundled), so archival output is conformant out of the box; watermarks and form fields under PDF/A now fail fast with a clear error instead of emitting a non-conformant file. Zero-dependency, pure-.NET HTML-to-PDF: modern CSS (flexbox, grid, gradients, transforms), SVG, PDF/A-2b and PDF/UA-1, RC4-128/AES-256 encryption, and right-to-left scripts (Arabic with GSUB cursive joining + GPOS harakat, and Hebrew).