<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Inkoxide releases</title>
  <link href="https://inkoxide.dev/changelog"/>
  <link rel="self" href="https://inkoxide.dev/changelog.xml"/>
  <id>https://inkoxide.dev/changelog</id>
  <author><name>Inkoxide</name></author>
  <updated>2026-10-16T00:00:00Z</updated>
  <entry>
    <title>0.9.1 - 2026-10-16</title>
    <link href="https://inkoxide.dev/changelog#091---2026-10-16"/>
    <id>https://inkoxide.dev/changelog#091---2026-10-16</id>
    <updated>2026-10-16T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Removed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render&lt;/code&gt; no longer ships MIGRATING.md, and the benchmarks measure Inkoxide alone.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.9.0 - 2026-10-16</title>
    <link href="https://inkoxide.dev/changelog#090---2026-10-16"/>
    <id>https://inkoxide.dev/changelog#090---2026-10-16</id>
    <updated>2026-10-16T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Rendering in the browser. &lt;code&gt;@inkoxide/render/browser&lt;/code&gt; renders the same templates with the
engine as WebAssembly, in a Web Worker, to the same PDF bytes as Node (but for the last digits
of SVG marker angles): &lt;code&gt;renderToBuffer&lt;/code&gt; and &lt;code&gt;renderToBlob&lt;/code&gt;, whose &lt;code&gt;signal&lt;/code&gt; or &lt;code&gt;timeoutMs&lt;/code&gt;
stops a render, &lt;code&gt;generate&lt;/code&gt;, &lt;code&gt;clearCaches&lt;/code&gt;, and the React helpers &lt;code&gt;usePdf&lt;/code&gt;, &lt;code&gt;&amp;#x3C;PdfDownloadLink&gt;&lt;/code&gt;
and &lt;code&gt;&amp;#x3C;PdfViewer&gt;&lt;/code&gt;. It needs no
cross-origin isolation, and a Content Security Policy needs only &lt;code&gt;'wasm-unsafe-eval'&lt;/code&gt;. The
engine is about 1.2 MB to download with brotli; a bundled font downloads when a document first
draws with it (Noto Sans, the fallback, only for a character Inter lacks), and a language's
hyphenation patterns when a document first hyphenates text in it. See GUIDE.md, "In the
browser".&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/wasm&lt;/code&gt;, the engine as WebAssembly, with the functions of &lt;code&gt;inkoxide&lt;/code&gt;, and &lt;code&gt;init&lt;/code&gt;,
&lt;code&gt;clearCaches&lt;/code&gt; and &lt;code&gt;loadHyphenation&lt;/code&gt;; its &lt;code&gt;render&lt;/code&gt; fetches the hyphenation patterns and the
&lt;code&gt;deferredFonts&lt;/code&gt; a document needs. It is released with the other packages, at their version.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/tailwind&lt;/code&gt; has a browser entry, chosen through the &lt;code&gt;browser&lt;/code&gt; export condition, with
Tailwind's theme bundled.&lt;/li&gt;
&lt;li&gt;Rust: &lt;code&gt;inkoxide::clear_caches()&lt;/code&gt;; the cargo features &lt;code&gt;msgpack&lt;/code&gt;, for
&lt;code&gt;InputFormat::MessagePack&lt;/code&gt;, and &lt;code&gt;embedded-hyphenation&lt;/code&gt;, for the built-in hyphenation
patterns, both on by default; without the latter, &lt;code&gt;HyphenationStore::register_builtin&lt;/code&gt; gives
a language's patterns (&lt;code&gt;builtin_hyphenation_file&lt;/code&gt; names its file). &lt;code&gt;RenderOptions::deferred_fonts&lt;/code&gt;
lists declared fonts that may be missing until a render needs one. A render that needs what
it was not given fails with &lt;code&gt;InkoxideError::NotLoaded&lt;/code&gt; (&lt;code&gt;NOT_LOADED&lt;/code&gt;), naming it.&lt;/li&gt;
&lt;li&gt;A text node with &lt;code&gt;hyphenate&lt;/code&gt; on and no &lt;code&gt;lang&lt;/code&gt; (nor &lt;code&gt;meta.lang&lt;/code&gt;) logs a warning, and the
&lt;code&gt;warnings&lt;/code&gt; of an &lt;code&gt;@inkoxide/render&lt;/code&gt; result say so, since such text is not hyphenated.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/react&lt;/code&gt; validates trees in development with code generated from the schema, which
compiles nothing at run time: it no longer depends on &lt;code&gt;ajv&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The engine matches words against the built-in hyphenation patterns itself, with hypher's
patterns and algorithm, so that they can also come at run time; it hyphenates as before.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The READMEs, GUIDE.md, MIGRATING.md, docs/API.md, docs/SCHEMA.md, docs/CLIENT_INTEGRATION.md,
the schema README and descriptions, and the &lt;code&gt;--png-dir&lt;/code&gt; help of the CLI no longer contradict
the code. They now say that a word wider than its line breaks between any two characters, and
that &lt;code&gt;[word]&lt;/code&gt; from a hyphenation callback keeps a word from being hyphenated, not from breaking.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The work budget of a &lt;code&gt;COLR&lt;/code&gt; colour glyph also counts the paint tables its paint graph reaches,
each as often as it is reached, before the glyph is painted: past 10000 the glyph draws
without colour, whether or not those paints draw anything. So does a glyph whose paint graph
has a cycle or nests more than 64 deep, which the &lt;code&gt;COLR&lt;/code&gt; specification does not allow.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>@inkoxide/schema 2.5.1 - 2026-10-16</title>
    <link href="https://inkoxide.dev/changelog#schema-251---2026-10-16"/>
    <id>https://inkoxide.dev/changelog#schema-251---2026-10-16</id>
    <updated>2026-10-16T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Documentation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Descriptions:&lt;/strong&gt; &lt;code&gt;opacity&lt;/code&gt; applies to each element, not to a group, and &lt;code&gt;0&lt;/code&gt; leaves out the
links, form fields and notes inside the node; the &lt;code&gt;background&lt;/code&gt; of a page is opaque, whatever
the alpha of its colour; a header or footer that shows page numbers is laid out again on every
page; &lt;code&gt;hyphenate&lt;/code&gt; on a node with no &lt;code&gt;lang&lt;/code&gt; and no &lt;code&gt;meta.lang&lt;/code&gt; hyphenates nothing, and the
engine logs a warning for it; a word wider than its line breaks between any two characters;
&lt;code&gt;fontFeatures&lt;/code&gt; takes at most 32 features of 0 to 65535 per run; a forced page break is
refused inside an &lt;em&gt;absolutely&lt;/em&gt; positioned node, not inside any positioned node.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SCHEMA.md&lt;/strong&gt; shows &lt;code&gt;minProperties&lt;/code&gt; and &lt;code&gt;maxProperties&lt;/code&gt; (4096 &lt;code&gt;emojiImages&lt;/code&gt;), and its error
table lists every case of &lt;code&gt;DUPLICATE_ID&lt;/code&gt; and &lt;code&gt;PDFA_CONFLICT&lt;/code&gt;, and says that &lt;code&gt;IMAGE_TOO_LARGE&lt;/code&gt;
is for raster images only.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.8.0 - 2026-10-15</title>
    <link href="https://inkoxide.dev/changelog#080---2026-10-15"/>
    <id>https://inkoxide.dev/changelog#080---2026-10-15</id>
    <updated>2026-10-15T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;An SVG without &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt; or &lt;code&gt;viewBox&lt;/code&gt; is laid out at the size of what it draws without
its text, the size it is drawn at, instead of a box sized by its text that stretched the drawing
and clipped the text.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alignItems&lt;/code&gt; and &lt;code&gt;alignSelf: 'baseline'&lt;/code&gt; align the first baselines of the items' text, as in CSS,
instead of their bottom edges.&lt;/li&gt;
&lt;li&gt;A table nested in a row split across pages continues below the repeated header rows of the table
around it, and repeats its own header rows below them; its first row on each new page was hidden
under the outer header.&lt;/li&gt;
&lt;li&gt;Fixed elements in the body are painted in paint order, by &lt;code&gt;zIndex&lt;/code&gt; then document order, as
without &lt;code&gt;fixed&lt;/code&gt;; they were painted over everything else in the flow. A fixed element inside an
element that is not painted on a page (&lt;code&gt;display: none&lt;/code&gt;, &lt;code&gt;opacity: 0&lt;/code&gt;, &lt;code&gt;pages&lt;/code&gt;) is not painted
there either.&lt;/li&gt;
&lt;li&gt;Encryption &lt;code&gt;permissions&lt;/code&gt; with an owner password equal to the user password are &lt;code&gt;INVALID_VALUE&lt;/code&gt; at
&lt;code&gt;options.encryption.permissions&lt;/code&gt;, since the user password would lift them.&lt;/li&gt;
&lt;li&gt;A second checked radio button in a group is &lt;code&gt;INVALID_VALUE&lt;/code&gt;, and radio buttons of one value share
an on state in encrypted files too, so a radio repeated on every page turns on and off as one, as
in a plain file.&lt;/li&gt;
&lt;li&gt;A text, select or list field whose value or options have a character its font lacks is
&lt;code&gt;INVALID_VALUE&lt;/code&gt;, instead of the character being left out silently. Field text is drawn with its
own font only, without fallback fonts, shaping or right-to-left ordering, and the docs now say
so.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render&lt;/code&gt; and &lt;code&gt;inkoxide&lt;/code&gt; reject a render option of the wrong type (&lt;code&gt;threads: '4'&lt;/code&gt;,
&lt;code&gt;pdfA: 5&lt;/code&gt;, a permission that is not a boolean) with &lt;code&gt;ENGINE_REJECTED&lt;/code&gt; / &lt;code&gt;INVALID_ARGUMENT&lt;/code&gt; at its
path instead of &lt;code&gt;INTERNAL&lt;/code&gt;; &lt;code&gt;localImages&lt;/code&gt; without &lt;code&gt;root&lt;/code&gt; is a &lt;code&gt;RenderError&lt;/code&gt;; and option errors no
longer name &lt;code&gt;&amp;#x3C;Document&gt;&lt;/code&gt;. Options that are not an object now fail.&lt;/li&gt;
&lt;li&gt;The guide says what happens after a font fails to load (it is dropped, and later renders fall
back from it), and that only faces of the same weight and style clash on their family name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w-screen&lt;/code&gt;, &lt;code&gt;h-screen&lt;/code&gt;, &lt;code&gt;min-h-screen&lt;/code&gt; and the other &lt;code&gt;*-screen&lt;/code&gt; sizing classes are listed and
tested as supported.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lineHeight&lt;/code&gt; takes lengths and percentages (&lt;code&gt;'14pt'&lt;/code&gt;), &lt;code&gt;letterSpacing&lt;/code&gt; in &lt;code&gt;em&lt;/code&gt; reads the same in
a style and a class, and props an element would ignore (&lt;code&gt;&amp;#x3C;Page wrap={false}&gt;&lt;/code&gt;, &lt;code&gt;href&lt;/code&gt; on a &lt;code&gt;Cell&lt;/code&gt;
or &lt;code&gt;Header&lt;/code&gt;, &lt;code&gt;&amp;#x3C;Document pdfVersion&gt;&lt;/code&gt;, &lt;code&gt;hyphenationPenalty&lt;/code&gt;, &lt;code&gt;className&lt;/code&gt; on an SVG shape) now
throw a &lt;code&gt;TemplateError&lt;/code&gt;; errors inside a &lt;code&gt;render&lt;/code&gt; prop keep the components around it in their
stack.&lt;/li&gt;
&lt;li&gt;An emoji drawn from &lt;code&gt;emojiImages&lt;/code&gt; in a run with &lt;code&gt;verticalAlign: 'super'&lt;/code&gt; or &lt;code&gt;'sub'&lt;/code&gt; is raised or
lowered with the run's text; it stayed on the line's baseline.&lt;/li&gt;
&lt;li&gt;The limit of 4096 colour glyphs per render counts only glyphs drawn in colour, not every glyph of
a colour font. Glyphs of a bitmap-only colour font that are not drawn in colour (past the limits,
or without colour, such as its space) still copy and search as their text instead of
disappearing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COLR&lt;/code&gt; layers in the foreground colour take the run's &lt;code&gt;color&lt;/code&gt;, as in browsers, instead of always
drawing black. In the text of SVG images they still draw black.&lt;/li&gt;
&lt;li&gt;A page with a side under 3 points fails a PDF/A render with &lt;code&gt;PDFA_CONFLICT&lt;/code&gt; at the page's &lt;code&gt;size&lt;/code&gt;,
at every level; such a file was not PDF/A.&lt;/li&gt;
&lt;li&gt;The engine and the JSON Schema agree on more values: the schema rejects negative sizes, gaps,
padding and grid tracks as the engine does, integers may be written as &lt;code&gt;400.0&lt;/code&gt;, &lt;code&gt;options: []&lt;/code&gt; is
&lt;code&gt;INVALID_VALUE&lt;/code&gt; on every field, and percentages and &lt;code&gt;fr&lt;/code&gt; sizes are bounded as written
(&lt;code&gt;"10000.0001%"&lt;/code&gt; is &lt;code&gt;OUT_OF_RANGE&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.7.1 - 2026-10-15</title>
    <link href="https://inkoxide.dev/changelog#071---2026-10-15"/>
    <id>https://inkoxide.dev/changelog#071---2026-10-15</id>
    <updated>2026-10-15T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A host entry in &lt;code&gt;remoteImages.allow&lt;/code&gt; (&lt;code&gt;cdn.example.com&lt;/code&gt;, &lt;code&gt;*.example.com&lt;/code&gt;) allows only port 443,
unless it names a port (&lt;code&gt;cdn.example.com:8443&lt;/code&gt;). Host entries used to allow any port; URL
entries already matched it.&lt;/li&gt;
&lt;li&gt;Encrypted PDFs give each font the widths of all its glyphs (&lt;code&gt;/DW&lt;/code&gt; and &lt;code&gt;/W&lt;/code&gt;), not only those of
the glyphs used, so the widths no longer depend on the text. This adds up to about 5 bytes a
glyph per font (some 15 KB for a Latin font); unencrypted output is unchanged.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A colour glyph drawn as an image, in page text and in SVG text, extracts as the text of its
own cluster, not the text it was first shaped with.&lt;/li&gt;
&lt;li&gt;Images are told apart by their bytes, not only by their hash: two different images whose
hashes are equal are drawn as two images, and each is checked for transparency on its own
under PDF/A-1b. Font subsets kept across renders are keyed by the font's SHA-256.&lt;/li&gt;
&lt;li&gt;A render waiting for an emoji image another render is already fetching stops at its own
&lt;code&gt;remoteImages.timeoutMs&lt;/code&gt;, and the download goes on for the other renders.&lt;/li&gt;
&lt;li&gt;SECURITY.md and the API docs say which form field state stays readable in encrypted files:
checkbox states, the position of the chosen radio button, and the positions of a list box's
selected and first visible options. Field values and options are encrypted.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The tables of a document are limited to 1000000 grid slots in all (rows times columns,
however the cells span them); more is &lt;code&gt;OUT_OF_RANGE&lt;/code&gt; at the table's &lt;code&gt;rows&lt;/code&gt;. Checking a
table's rows takes time in proportion to them.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fallbackFonts&lt;/code&gt; holds at most 256 ids; a longer list is &lt;code&gt;OUT_OF_RANGE&lt;/code&gt;. A font listed more
than once is tried once, where it is first listed.&lt;/li&gt;
&lt;li&gt;A form field &lt;code&gt;name&lt;/code&gt; that breaks the name rules is reported once as &lt;code&gt;INVALID_VALUE&lt;/code&gt; and is not
checked further against other fields' names and groups.&lt;/li&gt;
&lt;li&gt;SVG text counts the properties each span inherits toward the SVG limits, as shapes do, and
spans that inherit one &lt;code&gt;font-family&lt;/code&gt; share it; an SVG over a limit fails with
&lt;code&gt;INVALID_IMAGE&lt;/code&gt;. The image cache counts the text read from an SVG in the size of its entry.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;textLength&lt;/code&gt; in SVG text takes time in proportion to the characters inside it, however many
spans set one.&lt;/li&gt;
&lt;li&gt;A wavy text decoration draws its waves only where the page shows them, and at most 16 half
waves per glyph: a wider piece of text gets longer waves.&lt;/li&gt;
&lt;li&gt;Headers, footers and fixed nodes count the alt text of images in text, the text of colour
glyphs and link targets in the estimate of what they repeat on every page.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.7.0 - 2026-10-15</title>
    <link href="https://inkoxide.dev/changelog#070---2026-10-15"/>
    <id>https://inkoxide.dev/changelog#070---2026-10-15</id>
    <updated>2026-10-15T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Floats: &lt;code&gt;float: 'left' | 'right'&lt;/code&gt; and &lt;code&gt;clear: 'left' | 'right' | 'both'&lt;/code&gt; (Tailwind
&lt;code&gt;float-left&lt;/code&gt;, &lt;code&gt;float-right&lt;/code&gt;, &lt;code&gt;clear-*&lt;/code&gt;) on the children of a column &lt;code&gt;View&lt;/code&gt;, a &lt;code&gt;Page&lt;/code&gt; or a
&lt;code&gt;Cell&lt;/code&gt;. Text after a float wraps beside it, line by line; a box after it is narrowed to the
room beside it, or moved below it; the parent grows to hold its floats; and a float moves to
the next page with the lines beside it. A float taller than a page is sliced. &lt;code&gt;debug&lt;/code&gt; on a
paragraph beside a float shows its narrower line boxes.&lt;/li&gt;
&lt;li&gt;Encryption: user and owner passwords and permissions (printing at low or high resolution,
modifying, copying, annotating, filling forms, content accessibility, document assembly),
with AES-256 (PDF 2.0, revision 6). &lt;code&gt;RenderOptions::encryption&lt;/code&gt; with &lt;code&gt;Encryption&lt;/code&gt;,
&lt;code&gt;Permissions&lt;/code&gt; and &lt;code&gt;Printing&lt;/code&gt; in Rust, &lt;code&gt;encryption: { userPassword, ownerPassword, permissions }&lt;/code&gt; in the Node binding and in &lt;code&gt;@inkoxide/render&lt;/code&gt;, and &lt;code&gt;&amp;#x3C;Document userPassword ownerPassword permissions&gt;&lt;/code&gt;, which the option replaces. Passwords are printable ASCII up to
127 bytes, never part of the document tree, and never shown in errors or &lt;code&gt;Debug&lt;/code&gt; output; in
Node, a password that is not a string is &lt;code&gt;INVALID_ARGUMENT&lt;/code&gt; at its path before the engine
sees it.
Output stays deterministic: the keys are derived from the document and the passwords.
Radio buttons name their states by position in an encrypted file, with their values in
&lt;code&gt;/Opt&lt;/code&gt;, so the values are encrypted too, and the subset tags of font names are keyed by the
file's keys. Encryption with PDF/A is &lt;code&gt;INVALID_VALUE&lt;/code&gt; at &lt;code&gt;options.encryption&lt;/code&gt; in Rust and
&lt;code&gt;INVALID_ARGUMENT&lt;/code&gt; in Node. SECURITY.md lists what encryption does not protect.&lt;/li&gt;
&lt;li&gt;PDF/A-1b and PDF/A-3b, next to PDF/A-2b: &lt;code&gt;RenderOptions::pdf_a_level&lt;/code&gt; with &lt;code&gt;PdfALevel&lt;/code&gt; in
Rust (&lt;code&gt;with_pdf_a_level&lt;/code&gt;; &lt;code&gt;PdfALevel::ALL&lt;/code&gt; lists the levels), &lt;code&gt;pdfA: '1b' | '2b' | '3b'&lt;/code&gt; in
the Node binding and in &lt;code&gt;@inkoxide/render&lt;/code&gt; (&lt;code&gt;true&lt;/code&gt; is still PDF/A-2b), &lt;code&gt;--pdf-a=LEVEL&lt;/code&gt; in the
CLI (&lt;code&gt;--pdf-a&lt;/code&gt; alone is still PDF/A-2b, also before the document), and
&lt;code&gt;&amp;#x3C;Document conformance&gt;&lt;/code&gt; (&lt;code&gt;'PDF/A-1b'&lt;/code&gt;, &lt;code&gt;'PDF/A-2b'&lt;/code&gt;, &lt;code&gt;'PDF/A-3b'&lt;/code&gt;, or the part alone), which
the &lt;code&gt;pdfA&lt;/code&gt; option overrides. The a and u levels, which need tagged PDF, are a
&lt;code&gt;TEMPLATE_ERROR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;PDF/A-1b has no transparency: a render at 1b fails with the new code &lt;code&gt;PDFA_CONFLICT&lt;/code&gt; at the
path of the first colour with alpha below 1, &lt;code&gt;opacity&lt;/code&gt; between 0 and 1, &lt;code&gt;debug&lt;/code&gt;, image (or
background image) with an alpha channel, or SVG drawn with opacity, a mask or a blend mode
(&lt;code&gt;ENGINE_REJECTED&lt;/code&gt; with the component in &lt;code&gt;@inkoxide/render&lt;/code&gt;). &lt;code&gt;validate&lt;/code&gt; does not know the
level and does not report it. A PDF/A-1b file also keeps within the limits of PDF/A-1
(numbers, strings, arrays, dictionaries, graphics state nesting) or fails with &lt;code&gt;PDF_ERROR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;textWrap: 'pretty'&lt;/code&gt; (Tailwind &lt;code&gt;text-pretty&lt;/code&gt;) breaks a paragraph's lines over the whole
paragraph for even lines (Knuth and Plass's total fit), with fewer hyphens; justified
&lt;code&gt;pretty&lt;/code&gt; lines may also shrink their spaces by up to a third. It is inherited. &lt;code&gt;'wrap'&lt;/code&gt;, the
default, keeps first fit and the same output; &lt;code&gt;'balance'&lt;/code&gt; is reserved. &lt;code&gt;text-wrap&lt;/code&gt; maps to
&lt;code&gt;wrap&lt;/code&gt; and &lt;code&gt;text-nowrap&lt;/code&gt; to &lt;code&gt;whiteSpace: 'nowrap'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Superscripts and subscripts: &lt;code&gt;verticalAlign: 'super'&lt;/code&gt; or &lt;code&gt;'sub'&lt;/code&gt; on a &lt;code&gt;Text&lt;/code&gt; (the Tailwind
classes &lt;code&gt;align-super&lt;/code&gt; and &lt;code&gt;align-sub&lt;/code&gt;) raises or lowers its baseline by the font's own
superscript and subscript offsets, and the line grows to fit it. &lt;code&gt;verticalAlign&lt;/code&gt; stays &lt;code&gt;top&lt;/code&gt;,
&lt;code&gt;middle&lt;/code&gt; or &lt;code&gt;bottom&lt;/code&gt; on a &lt;code&gt;Cell&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;OpenType features: &lt;code&gt;fontFeatureSettings&lt;/code&gt; as a CSS string (&lt;code&gt;"'tnum' 1, 'liga' off"&lt;/code&gt;), a list
of tags or an object of tags to values or switches, and &lt;code&gt;fontVariantNumeric&lt;/code&gt;
(&lt;code&gt;'tabular-nums slashed-zero'&lt;/code&gt;); in Tailwind, &lt;code&gt;font-features-[…]&lt;/code&gt;, &lt;code&gt;tabular-nums&lt;/code&gt;,
&lt;code&gt;oldstyle-nums&lt;/code&gt;, &lt;code&gt;slashed-zero&lt;/code&gt;, &lt;code&gt;ordinal&lt;/code&gt;, &lt;code&gt;diagonal-fractions&lt;/code&gt; and the other numeric
classes. Both are inherited. A run takes at most 32 features, with values up to 65535, and a
document at most 64 different sets of them (&lt;code&gt;OUT_OF_RANGE&lt;/code&gt;). &lt;code&gt;@inkoxide/style&lt;/code&gt; exports
&lt;code&gt;parseFontFeatureSettings&lt;/code&gt;, &lt;code&gt;parseFontVariantNumeric&lt;/code&gt; and &lt;code&gt;RUN_ALIGNS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;debug&lt;/code&gt; on a &lt;code&gt;Text&lt;/code&gt; outlines the paragraph line by line: each line box, its baseline, and the
glyphs of every run on the line, so nested &lt;code&gt;Text&lt;/code&gt; and &lt;code&gt;Link&lt;/code&gt; elements show where their words
landed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;Marker&gt;&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;&amp;#x3C;Svg&gt;&lt;/code&gt;: a shape drawn at the start, middle or end vertices of a path, line
or poly shape, referenced with the new &lt;code&gt;markerStart&lt;/code&gt;, &lt;code&gt;markerMid&lt;/code&gt; and &lt;code&gt;markerEnd&lt;/code&gt; props. It
takes &lt;code&gt;viewBox&lt;/code&gt;, &lt;code&gt;refX&lt;/code&gt;, &lt;code&gt;refY&lt;/code&gt;, &lt;code&gt;markerWidth&lt;/code&gt;, &lt;code&gt;markerHeight&lt;/code&gt;, &lt;code&gt;orient&lt;/code&gt; and &lt;code&gt;markerUnits&lt;/code&gt;.
&lt;code&gt;Marker&lt;/code&gt; and &lt;code&gt;MarkerProps&lt;/code&gt; are exported.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fr&lt;/code&gt;, the radius of the focal circle, on &lt;code&gt;RadialGradient&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;#x3C;Canvas&gt;&lt;/code&gt; painter clips and paints gradients: &lt;code&gt;clip(rule?)&lt;/code&gt; clips what follows to the
current path until &lt;code&gt;restore()&lt;/code&gt;, and &lt;code&gt;linearGradient(x1, y1, x2, y2)&lt;/code&gt; and
&lt;code&gt;radialGradient(x1, y1, r1, x2, y2, r2)&lt;/code&gt; return a &lt;code&gt;CanvasGradient&lt;/code&gt; (exported as a type) that
takes colour stops with &lt;code&gt;stop(offset, color, opacity?)&lt;/code&gt; and paints in place of a colour.
&lt;code&gt;miterLimit(n)&lt;/code&gt; sets how far mitred joins reach.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text in the &lt;code&gt;&amp;#x3C;Canvas&gt;&lt;/code&gt; painter&lt;/strong&gt;: &lt;code&gt;font(name, size?)&lt;/code&gt; (a registered font id, a family name,
or one of pdfkit's standard names such as &lt;code&gt;Helvetica-Bold&lt;/code&gt;, which picks the canvas's family in
that weight and style), &lt;code&gt;fontSize(n)&lt;/code&gt; (12 by default) and &lt;code&gt;text(str, x, y, options?)&lt;/code&gt;, which
draws one line with the fill colour or gradient, its top at &lt;code&gt;y&lt;/code&gt; by default. The options are
&lt;code&gt;width&lt;/code&gt; with &lt;code&gt;align&lt;/code&gt; (&lt;code&gt;justify&lt;/code&gt;, as in pdfkit, is &lt;code&gt;left&lt;/code&gt;: a line does not wrap), &lt;code&gt;baseline&lt;/code&gt;,
&lt;code&gt;characterSpacing&lt;/code&gt;, &lt;code&gt;wordSpacing&lt;/code&gt;, &lt;code&gt;underline&lt;/code&gt;, &lt;code&gt;strike&lt;/code&gt;, &lt;code&gt;fill&lt;/code&gt; (whether to fill the text,
&lt;code&gt;true&lt;/code&gt; by default) and &lt;code&gt;stroke&lt;/code&gt;; &lt;code&gt;CanvasTextOptions&lt;/code&gt; is exported. A line break in the text is
an error.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text in SVG images&lt;/strong&gt;: &lt;code&gt;&amp;#x3C;text&gt;&lt;/code&gt;, its &lt;code&gt;&amp;#x3C;tspan&gt;&lt;/code&gt;s and &lt;code&gt;&amp;#x3C;a&gt;&lt;/code&gt;s are drawn as PDF text with the
document's fonts, so it can be selected, searched and copied, in SVG files and in SVGs nested
in them. Each span's font is the declared font that matches its &lt;code&gt;font-family&lt;/code&gt;, &lt;code&gt;font-weight&lt;/code&gt;
and &lt;code&gt;font-style&lt;/code&gt; (as CSS matches them), with the document's fallback fonts for characters it
lacks. Positions (&lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;dx&lt;/code&gt;, &lt;code&gt;dy&lt;/code&gt;, &lt;code&gt;rotate&lt;/code&gt;), &lt;code&gt;text-anchor&lt;/code&gt;, &lt;code&gt;dominant-baseline&lt;/code&gt;,
&lt;code&gt;alignment-baseline&lt;/code&gt;, &lt;code&gt;baseline-shift&lt;/code&gt;, &lt;code&gt;letter-spacing&lt;/code&gt;, &lt;code&gt;word-spacing&lt;/code&gt;, &lt;code&gt;text-decoration&lt;/code&gt;,
&lt;code&gt;textLength&lt;/code&gt;, &lt;code&gt;lengthAdjust&lt;/code&gt;, &lt;code&gt;xml:space&lt;/code&gt;, style sheets, and fills and strokes with gradients
and patterns work, also in clip paths, masks, patterns, markers and &lt;code&gt;use&lt;/code&gt; copies, whatever an
author style sheet says about the paths of the SVG. Colour emoji fonts draw in colour there,
and emoji the document maps to images are those images, as in other text. Text on a path,
vertical text and text that an XML entity expands to fail with &lt;code&gt;INVALID_IMAGE&lt;/code&gt;, as all SVG
text did. An SVG without a size takes it from what it draws without its text, so the same
with any fonts. An SVG with text in a
document that declares no fonts fails validation with &lt;code&gt;INVALID_IMAGE&lt;/code&gt;, and an SVG may hold up
to 1,000,000 characters of text, counting every copy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;Text&gt;&lt;/code&gt; and &lt;code&gt;&amp;#x3C;Tspan&gt;&lt;/code&gt; in &lt;code&gt;&amp;#x3C;Svg&gt;&lt;/code&gt;&lt;/strong&gt;: a &lt;code&gt;Text&lt;/code&gt; inside an &lt;code&gt;&amp;#x3C;Svg&gt;&lt;/code&gt; (or its &lt;code&gt;G&lt;/code&gt;, &lt;code&gt;Defs&lt;/code&gt;,
&lt;code&gt;ClipPath&lt;/code&gt; or &lt;code&gt;Marker&lt;/code&gt;) is SVG text, with &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;dx&lt;/code&gt;, &lt;code&gt;dy&lt;/code&gt;, &lt;code&gt;rotate&lt;/code&gt;, &lt;code&gt;textAnchor&lt;/code&gt;,
&lt;code&gt;dominantBaseline&lt;/code&gt;, &lt;code&gt;letterSpacing&lt;/code&gt;, &lt;code&gt;wordSpacing&lt;/code&gt;, &lt;code&gt;textDecoration&lt;/code&gt;, &lt;code&gt;textLength&lt;/code&gt;,
&lt;code&gt;lengthAdjust&lt;/code&gt;, &lt;code&gt;direction&lt;/code&gt; and the SVG paint props; a &lt;code&gt;Tspan&lt;/code&gt; inside it is a span, which also
takes &lt;code&gt;baselineShift&lt;/code&gt; and &lt;code&gt;alignmentBaseline&lt;/code&gt;. Its font comes from &lt;code&gt;fontFamily&lt;/code&gt;, &lt;code&gt;fontWeight&lt;/code&gt; and &lt;code&gt;fontStyle&lt;/code&gt; on it or
around it, and from the text style around the drawing, matched against the registered fonts
like other text, and declared in the document. &lt;code&gt;Tspan&lt;/code&gt;, &lt;code&gt;SvgTextProps&lt;/code&gt; and &lt;code&gt;TspanProps&lt;/code&gt; are
exported; &lt;code&gt;SvgPresentation&lt;/code&gt; gains the font props, &lt;code&gt;textAnchor&lt;/code&gt;, &lt;code&gt;dominantBaseline&lt;/code&gt; and
&lt;code&gt;visibility&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Background images: &lt;code&gt;&amp;#x3C;ImageBackground src fit&gt;&lt;/code&gt;, a &lt;code&gt;View&lt;/code&gt; with an image behind its children,
and &lt;code&gt;backgroundImage&lt;/code&gt; on a box node of the document (&lt;code&gt;src&lt;/code&gt; or &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;fit&lt;/code&gt;, &lt;code&gt;objectPosition&lt;/code&gt;).
The image fills the padding box, over the background colour and under the borders, clipped to
it and rounded by &lt;code&gt;borderRadius&lt;/code&gt;; a box split across pages is sliced with its image, like its
background colour. &lt;code&gt;src&lt;/code&gt; takes every form &lt;code&gt;&amp;#x3C;Image src&gt;&lt;/code&gt; takes, and &lt;code&gt;fit&lt;/code&gt;, &lt;code&gt;objectFit&lt;/code&gt;,
&lt;code&gt;objectPosition&lt;/code&gt; and the &lt;code&gt;object-*&lt;/code&gt; classes place it as on an &lt;code&gt;Image&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Colour emoji from fonts. Glyphs in a font's &lt;code&gt;COLR&lt;/code&gt; table (versions 0 and 1) and PNG bitmaps in
&lt;code&gt;CBDT&lt;/code&gt; or &lt;code&gt;sbix&lt;/code&gt; are drawn in colour, as images that copy and search as their text
(&lt;code&gt;ActualText&lt;/code&gt;). Register an emoji font, such as Noto Color Emoji, and list it in
&lt;code&gt;fallbackFonts&lt;/code&gt;: an emoji goes to the first colour fallback font that has it. A font with
colour bitmaps and no outlines now registers; it is never embedded and cannot be a form
field's font. A colour glyph that takes over 10 000 drawing steps, or past the first 4096 of a
render or the first 32 MB of colour glyph images, draws without colour. Under PDF/A-1b, which
forbids transparency, a colour glyph that needs it (a bitmap with an alpha channel, a layer
with an opacity) fails the render with &lt;code&gt;PDFA_CONFLICT&lt;/code&gt; at its run, as an emoji image does at
its &lt;code&gt;emojiImages&lt;/code&gt; key, or at the &lt;code&gt;src&lt;/code&gt; of an SVG whose text has it.&lt;/li&gt;
&lt;li&gt;Emoji from images: &lt;code&gt;Font.registerEmojiSource({ url, format, withVariationSelectors })&lt;/code&gt; or
&lt;code&gt;{ builder }&lt;/code&gt; draws the emoji of every &lt;code&gt;Text&lt;/code&gt; from an image set, through the document's new
&lt;code&gt;emojiImages&lt;/code&gt; (emoji sequence → image id). The images load like &lt;code&gt;&amp;#x3C;Image src&gt;&lt;/code&gt; (URLs allowed by
&lt;code&gt;remoteImages&lt;/code&gt;, kept between renders); an emoji image is 1em high with its aspect ratio and
copies as the emoji. An emoji whose image cannot be loaded, or is not an image the engine can
draw, is drawn by the fonts with a warning, and a ZWJ sequence without an image uses those of
its parts. &lt;code&gt;EmojiSource&lt;/code&gt; is exported.&lt;/li&gt;
&lt;li&gt;Images in text: an &lt;code&gt;&amp;#x3C;Image&gt;&lt;/code&gt; inside a &lt;code&gt;&amp;#x3C;Text&gt;&lt;/code&gt; (and &lt;code&gt;image&lt;/code&gt; on a document's text run) sits in
the line, as wide and high as asked in points or em (1em high by default, keeping its aspect
ratio), placed by &lt;code&gt;verticalAlign&lt;/code&gt; (&lt;code&gt;baseline&lt;/code&gt;, &lt;code&gt;middle&lt;/code&gt;, &lt;code&gt;text-top&lt;/code&gt;, &lt;code&gt;text-bottom&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;,
&lt;code&gt;bottom&lt;/code&gt;, &lt;code&gt;super&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt; or points; &lt;code&gt;align-text-top&lt;/code&gt; and &lt;code&gt;align-text-bottom&lt;/code&gt; in Tailwind), with
&lt;code&gt;alt&lt;/code&gt; as the text it copies as. Lines break around it like around a character.&lt;/li&gt;
&lt;li&gt;Groups of form fields: a dot in a field &lt;code&gt;name&lt;/code&gt; puts the field in a group, as in PDF's own
field names. &lt;code&gt;address.street&lt;/code&gt; and &lt;code&gt;address.city&lt;/code&gt; are the fields &lt;code&gt;street&lt;/code&gt; and &lt;code&gt;city&lt;/code&gt; of the
group &lt;code&gt;address&lt;/code&gt;; a name has at most 32 parts, so groups nest up to 31 deep. The PDF holds
them as a field tree, which viewers show and export by their full names.&lt;/li&gt;
&lt;li&gt;List boxes: a form field of &lt;code&gt;kind: "list"&lt;/code&gt; shows several of its &lt;code&gt;options&lt;/code&gt; at once (four rows
without a height) and scrolls to the first selected one. With &lt;code&gt;multiSelect&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt; is an
array and several options can be selected.&lt;/li&gt;
&lt;li&gt;Formats on text fields: &lt;code&gt;format&lt;/code&gt; shows a number (decimals, separators, negative style,
currency), a percent, a date or a time (with a pattern such as &lt;code&gt;dd/mm/yyyy&lt;/code&gt;), a postal code, a
phone number or a social security number, and viewers format what the user types with the
standard form scripts. PDF/A output keeps the formatted text and value but leaves the scripts
out, as PDF/A requires.&lt;/li&gt;
&lt;li&gt;Text notes: a &lt;code&gt;note&lt;/code&gt; node is a sticky-note icon that viewers open to show its text, with an
author, a colour and whether it starts open. It takes no space; the icon sits at the top-left
corner of its box, keeps its size at any zoom and stays upright under a transform. It has an
appearance stream, so it prints and passes PDF/A-2b. A note can also be part of a line of text
(&lt;code&gt;note&lt;/code&gt; on a text run): it takes no room there, and its icon hangs from the top of the line
where it is. Line breaking keeps it with the word before it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;FieldSet name&gt;&lt;/code&gt; in &lt;code&gt;@inkoxide/render&lt;/code&gt;: a &lt;code&gt;View&lt;/code&gt;, with its props and &lt;code&gt;render&lt;/code&gt;, whose name goes
in front of the names of the fields inside it, at any depth (&lt;code&gt;address.street&lt;/code&gt;); sets nest.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;List&gt;&lt;/code&gt; in &lt;code&gt;@inkoxide/render&lt;/code&gt;, a list box, with &lt;code&gt;multiSelect&lt;/code&gt; and an array &lt;code&gt;value&lt;/code&gt;; &lt;code&gt;Field&lt;/code&gt;
takes &lt;code&gt;kind="list"&lt;/code&gt;. &lt;code&gt;Select&lt;/code&gt; and &lt;code&gt;List&lt;/code&gt; take their choices as &lt;code&gt;options&lt;/code&gt; or &lt;code&gt;select&lt;/code&gt;, and
&lt;code&gt;TextInput&lt;/code&gt; is another name for &lt;code&gt;TextField&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;format&lt;/code&gt; on &lt;code&gt;TextField&lt;/code&gt; in &lt;code&gt;@inkoxide/render&lt;/code&gt;, with the readable keys of the schema, or &lt;code&gt;param&lt;/code&gt;,
&lt;code&gt;nDec&lt;/code&gt;, &lt;code&gt;sepComma&lt;/code&gt;, &lt;code&gt;negStyle&lt;/code&gt; and &lt;code&gt;currencyPrepend&lt;/code&gt;; the type &lt;code&gt;TextFormat&lt;/code&gt; is exported. A
number or percent format with any of those keys defaults to no decimals and no thousands
separator; on a &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;param&lt;/code&gt; is one of four styles, &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;3&lt;/code&gt;; &lt;code&gt;zip4&lt;/code&gt; is &lt;code&gt;zipPlus4&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;Note&gt;&lt;/code&gt; in &lt;code&gt;@inkoxide/render&lt;/code&gt;: a text note, with its text as children, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, and
its &lt;code&gt;backgroundColor&lt;/code&gt; as its colour. Inside a &lt;code&gt;&amp;#x3C;Text&gt;&lt;/code&gt;, it is part of the line.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Emoji sequences (flags, keycaps, skin tones, tag sequences and ZWJ sequences such as &lt;code&gt;👩‍💻&lt;/code&gt;) stay
in one font, so the font's ligature forms: their selectors, joiners and modifiers could go to
another font than their base, which drew them as separate glyphs.&lt;/li&gt;
&lt;li&gt;Strokes with mitred joins in a &lt;code&gt;&amp;#x3C;Canvas&gt;&lt;/code&gt; have a miter limit of 10, PDF's default, as in
pdfkit; they had SVG's 4, so joins sharper than about 29 degrees (down to about 11) are now
mitred instead of bevelled. &lt;code&gt;miterLimit(4)&lt;/code&gt; gives the old joins.&lt;/li&gt;
&lt;li&gt;In SVG images, &lt;code&gt;em&lt;/code&gt; and &lt;code&gt;ex&lt;/code&gt; lengths and &lt;code&gt;font-size&lt;/code&gt; keywords are relative to a font size of
16 where nothing sets one, as in browsers; it was 12.&lt;/li&gt;
&lt;li&gt;Font family names match ignoring ASCII case, as in CSS: &lt;code&gt;fontFamily: 'inter'&lt;/code&gt; picks Inter
instead of falling back with a warning. &lt;code&gt;Font.register&lt;/code&gt; refuses a family that differs from
a registered or bundled one only in case, for the same weight and style.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The text of SVG images is bounded as the rest of the SVG is: what replaces it for the SVG
conversion counts toward the SVG limits as it is drawn, before it is written, and validation
checks it as a render does, so an SVG over a limit fails validation with &lt;code&gt;INVALID_IMAGE&lt;/code&gt;.
The text nodes of an SVG text count as elements. Reading, laying out and drawing SVG text
takes time in proportion to it.&lt;/li&gt;
&lt;li&gt;Field options and values are limited to 1000 options of at most 1000 characters each, and a
&lt;code&gt;value&lt;/code&gt; of at most 8000 characters. Longer ones are &lt;code&gt;INVALID_VALUE&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>@inkoxide/schema 2.5.0 - 2026-10-15</title>
    <link href="https://inkoxide.dev/changelog#schema-250---2026-10-15"/>
    <id>https://inkoxide.dev/changelog#schema-250---2026-10-15</id>
    <updated>2026-10-15T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A character that the font of a text, select or list field has no glyph for, in the shown
value or in an option, is &lt;code&gt;INVALID_VALUE&lt;/code&gt;. Fields are drawn with their own font only, so the
character was left out without a word; such a document was valid before.&lt;/li&gt;
&lt;li&gt;A second radio button with &lt;code&gt;checked&lt;/code&gt; in the same group is &lt;code&gt;INVALID_VALUE&lt;/code&gt;; such a group was
valid before, with both buttons drawn on but only the first one's value as the group's.
Buttons of a group with the same value are now on together, as viewers show them, encrypted
or not.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;emojiImages&lt;/code&gt; image in a run with &lt;code&gt;verticalAlign&lt;/code&gt; &lt;code&gt;super&lt;/code&gt; or &lt;code&gt;sub&lt;/code&gt; is raised or lowered
with the run's glyphs, its bottom 0.1em below the run's moved baseline. It stayed on the
line's baseline.&lt;/li&gt;
&lt;li&gt;A page with a side under 3 points renders as PDF/A no more: the render fails with
&lt;code&gt;PDFA_CONFLICT&lt;/code&gt; at the page's &lt;code&gt;size&lt;/code&gt;, at every level. Such a file was not PDF/A. The &lt;code&gt;Size&lt;/code&gt;
description says so.&lt;/li&gt;
&lt;li&gt;The schema and the engine agree on more values:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;minWidth&lt;/code&gt;, &lt;code&gt;minHeight&lt;/code&gt;, &lt;code&gt;maxWidth&lt;/code&gt;, &lt;code&gt;maxHeight&lt;/code&gt;, &lt;code&gt;flexBasis&lt;/code&gt;, &lt;code&gt;gap&lt;/code&gt;,
&lt;code&gt;rowGap&lt;/code&gt;, &lt;code&gt;columnGap&lt;/code&gt; and &lt;code&gt;padding&lt;/code&gt;, percentage grid tracks and &lt;code&gt;fitContent&lt;/code&gt; are not
negative in the schema either (new &lt;code&gt;NonNegativePercent&lt;/code&gt;, &lt;code&gt;NonNegativeLength&lt;/code&gt; and
&lt;code&gt;NonNegativeLengthPercent&lt;/code&gt;). The engine already rejected negative ones with &lt;code&gt;OUT_OF_RANGE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An integer written with a zero fraction (&lt;code&gt;"weight": 400.0&lt;/code&gt;, &lt;code&gt;"version": 1.0&lt;/code&gt;,
&lt;code&gt;"colSpan": 1.0&lt;/code&gt;) is read as the integer, as JSON Schema reads it. It was &lt;code&gt;INVALID_TYPE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"options": []&lt;/code&gt; is &lt;code&gt;INVALID_VALUE&lt;/code&gt; on every field kind, as the schema's &lt;code&gt;minItems&lt;/code&gt; says. On
a text or checkbox field the engine accepted it.&lt;/li&gt;
&lt;li&gt;A percentage or &lt;code&gt;fr&lt;/code&gt; size past its bound by digits a 32-bit float drops (&lt;code&gt;"10000.0001%"&lt;/code&gt;,
&lt;code&gt;"1000000.01fr"&lt;/code&gt;) is &lt;code&gt;OUT_OF_RANGE&lt;/code&gt;, as the schema's pattern says, and a negative
percentage too small for a float stays negative. Other numbers are read as 32-bit floats,
which &lt;code&gt;docs/SCHEMA.md&lt;/code&gt; now says.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TableCell.style&lt;/code&gt; lists &lt;code&gt;float&lt;/code&gt; and &lt;code&gt;clear&lt;/code&gt; among the properties a cell does not take, and
&lt;code&gt;float&lt;/code&gt;, &lt;code&gt;Break.before&lt;/code&gt; and &lt;code&gt;PageBreakNode&lt;/code&gt; say that a float takes no forced page break,
on it or inside it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>@inkoxide/schema 2.4.1 - 2026-10-15</title>
    <link href="https://inkoxide.dev/changelog#schema-241---2026-10-15"/>
    <id>https://inkoxide.dev/changelog#schema-241---2026-10-15</id>
    <updated>2026-10-15T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fallbackFonts&lt;/code&gt; holds at most 256 ids; a longer list is &lt;code&gt;OUT_OF_RANGE&lt;/code&gt;. It was unbounded, so
a document with a longer one was valid before. An id listed again is skipped: each font is
tried once, where it is first listed.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>@inkoxide/schema 2.4.0 - 2026-10-15</title>
    <link href="https://inkoxide.dev/changelog#schema-240---2026-10-15"/>
    <id>https://inkoxide.dev/changelog#schema-240---2026-10-15</id>
    <updated>2026-10-15T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Style.float&lt;/code&gt; (&lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt;) and &lt;code&gt;Style.clear&lt;/code&gt; (&lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, &lt;code&gt;both&lt;/code&gt;,
&lt;code&gt;none&lt;/code&gt;): floats in column boxes, the page and cells, with text wrapping beside them and boxes
narrowed beside them. Floats go in column boxes only, not on cells, not absolutely positioned
and without forced breaks; at most 1000 per box; a box with floats takes no &lt;code&gt;justifyContent&lt;/code&gt;
or &lt;code&gt;alignContent&lt;/code&gt;, and its children no &lt;code&gt;flexGrow&lt;/code&gt; or &lt;code&gt;flexBasis&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Style.textWrap&lt;/code&gt; on text nodes: &lt;code&gt;wrap&lt;/code&gt; (the default, first fit, as before) or &lt;code&gt;pretty&lt;/code&gt;, which
sets each stretch between forced breaks as a whole (Knuth and Plass's total fit) and lets the
spaces of justified lines shrink by up to a third. Bounded per paragraph (20 000 break
opportunities, 512 open breakpoints, 2 097 152 lines tried), past which it breaks first fit.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TextRun.verticalAlign&lt;/code&gt;: &lt;code&gt;baseline&lt;/code&gt; (the default), &lt;code&gt;super&lt;/code&gt; or &lt;code&gt;sub&lt;/code&gt;. A superscript's
baseline is raised and a subscript's lowered by the offsets the font gives (OS/2), for text
of the run's size, and the line grows to fit them. The run keeps its own &lt;code&gt;size&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TextRun.fontFeatures&lt;/code&gt;: OpenType features by tag, as CSS &lt;code&gt;font-feature-settings&lt;/code&gt;
(&lt;code&gt;{"tnum": 1, "liga": 0}&lt;/code&gt;): 0 turns a feature off, 1 on, and a larger value picks an
alternate. At most 32 per run, values up to 65535, and 64 different sets in a document.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BoxNode.backgroundImage&lt;/code&gt; (&lt;code&gt;$defs/BackgroundImage&lt;/code&gt;): an image painted into the box's padding
box, over its &lt;code&gt;backgroundColor&lt;/code&gt; and under its borders and children, clipped to the padding box
rounded by &lt;code&gt;borderRadius&lt;/code&gt;. It takes &lt;code&gt;src&lt;/code&gt; or &lt;code&gt;data&lt;/code&gt; (exactly one, as on an &lt;code&gt;image&lt;/code&gt; node), &lt;code&gt;fit&lt;/code&gt;
(default &lt;code&gt;fill&lt;/code&gt;) and &lt;code&gt;objectPosition&lt;/code&gt;, with the same bounds as on an &lt;code&gt;image&lt;/code&gt; node. Optional;
documents without it render as before.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TextRun.image&lt;/code&gt; (&lt;code&gt;$defs/InlineImage&lt;/code&gt;): an image in the line in place of the run's text, a run
having exactly one of &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;placeholder&lt;/code&gt; and &lt;code&gt;image&lt;/code&gt;. It takes &lt;code&gt;src&lt;/code&gt; or &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;width&lt;/code&gt; and
&lt;code&gt;height&lt;/code&gt; in (0, 14400] points (one from the other by the image's aspect ratio, and 1em high,
the run's &lt;code&gt;size&lt;/code&gt;, with neither), &lt;code&gt;fit&lt;/code&gt;, &lt;code&gt;objectPosition&lt;/code&gt;, and &lt;code&gt;alt&lt;/code&gt; (at most 1024 characters),
the text copying and searching give for it. The run's &lt;code&gt;font&lt;/code&gt; and &lt;code&gt;size&lt;/code&gt; are the line's strut.
It breaks lines like one character.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Document.emojiImages&lt;/code&gt;: emoji sequences drawn as images, sequence → image id (at most 4096,
each key one emoji sequence of at most 64 bytes in UTF-8; U+FE0F is ignored). Where text has the
sequence with emoji presentation, the image takes its place, 1em high with its aspect ratio,
its bottom 0.1em below the baseline, and copies as the sequence. A ZWJ sequence without an
image tries its parts; the rest is drawn by the fonts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TextRun.verticalAlign&lt;/code&gt; places an image run with &lt;code&gt;middle&lt;/code&gt;, &lt;code&gt;textTop&lt;/code&gt;, &lt;code&gt;textBottom&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;,
&lt;code&gt;bottom&lt;/code&gt;, or a number: points its bottom sits above the baseline (within ±14400). On a text
run they are &lt;code&gt;INVALID_VALUE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Field.name&lt;/code&gt; may contain dots, which make groups of fields: &lt;code&gt;address.street&lt;/code&gt; is the field
&lt;code&gt;street&lt;/code&gt; in the group &lt;code&gt;address&lt;/code&gt;. A name has at most 32 parts of 1 to 128 characters, and at
most 512 characters in all (it was one part of at most 128). A name that is both a field and a
group (&lt;code&gt;a&lt;/code&gt; and &lt;code&gt;a.b&lt;/code&gt;) is &lt;code&gt;DUPLICATE_ID&lt;/code&gt;. Documents without dots render as before.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Field.kind: "list"&lt;/code&gt;, a list box: &lt;code&gt;options&lt;/code&gt; (required) and &lt;code&gt;font&lt;/code&gt; (required) as for &lt;code&gt;select&lt;/code&gt;,
and &lt;code&gt;value&lt;/code&gt;, the selected option. &lt;code&gt;Field.multiSelect&lt;/code&gt; (list only) lets several options be
selected; &lt;code&gt;value&lt;/code&gt; is then an array of options, each at most once (at most 1000). An array
&lt;code&gt;value&lt;/code&gt; on any other field is &lt;code&gt;INVALID_VALUE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Field.format&lt;/code&gt; (&lt;code&gt;$defs/FieldFormat&lt;/code&gt;), on single-line text fields that are not passwords:
&lt;code&gt;type&lt;/code&gt; (&lt;code&gt;number&lt;/code&gt;, &lt;code&gt;percent&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;zip&lt;/code&gt;, &lt;code&gt;zipPlus4&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;ssn&lt;/code&gt;) with
&lt;code&gt;decimals&lt;/code&gt; (0 to 10), &lt;code&gt;separators&lt;/code&gt;, &lt;code&gt;negative&lt;/code&gt;, &lt;code&gt;currency&lt;/code&gt; (at most 8 characters, no control
characters, quotes or backslashes) and &lt;code&gt;currencyBefore&lt;/code&gt; for numbers, and &lt;code&gt;pattern&lt;/code&gt; (required,
at most 64 characters; a time's shows only &lt;code&gt;H&lt;/code&gt;, &lt;code&gt;h&lt;/code&gt;, &lt;code&gt;M&lt;/code&gt;, &lt;code&gt;s&lt;/code&gt; and &lt;code&gt;t&lt;/code&gt;) for dates and times.
&lt;code&gt;value&lt;/code&gt; must fit the format; a time is &lt;code&gt;HH:MM[:SS]&lt;/code&gt;, or a date with a time.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;note&lt;/code&gt; node (&lt;code&gt;NoteNode&lt;/code&gt;): a text annotation, &lt;code&gt;{"type":"note","note":{"contents"}}&lt;/code&gt; with
&lt;code&gt;author&lt;/code&gt;, &lt;code&gt;color&lt;/code&gt; (default yellow) and &lt;code&gt;open&lt;/code&gt;. &lt;code&gt;contents&lt;/code&gt; is 1 to 8000 characters and &lt;code&gt;author&lt;/code&gt;
at most 256. It takes &lt;code&gt;style&lt;/code&gt;, &lt;code&gt;break&lt;/code&gt;, &lt;code&gt;pages&lt;/code&gt;, &lt;code&gt;debug&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt;, but no &lt;code&gt;link&lt;/code&gt; or
&lt;code&gt;bookmark&lt;/code&gt;, and no space in the layout. Its icon is 20 pt square at any zoom and stays upright
under a transform.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TextRun.note&lt;/code&gt; (&lt;code&gt;$defs/Note&lt;/code&gt;): a note in the line, in place of the run's text, a run having
exactly one of &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;placeholder&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt; and &lt;code&gt;note&lt;/code&gt;. It takes no room; its icon hangs from
the top of the line where the run is, on the page where the line starts. The run's &lt;code&gt;font&lt;/code&gt; and
&lt;code&gt;size&lt;/code&gt; are the line's strut, and line breaking keeps it with the character before it, or after
a space, the one after it.&lt;/li&gt;
&lt;li&gt;SVG images may contain text (&lt;code&gt;&amp;#x3C;text&gt;&lt;/code&gt;, &lt;code&gt;&amp;#x3C;tspan&gt;&lt;/code&gt;). It is laid out with the document's fonts:
&lt;code&gt;FontDecl.family&lt;/code&gt;, &lt;code&gt;weight&lt;/code&gt; and &lt;code&gt;style&lt;/code&gt;, which were descriptive until now, are what its
&lt;code&gt;font-family&lt;/code&gt;, &lt;code&gt;font-weight&lt;/code&gt; and &lt;code&gt;font-style&lt;/code&gt; select a declared font by. A document whose SVG
images have text must declare a font; one that declares none fails with &lt;code&gt;INVALID_IMAGE&lt;/code&gt;, and
so does text on a path, vertical text and text that an XML entity expands to. The text counts
toward the limits of its SVG as it is drawn (its text nodes as elements). Documents with SVG
images without text render as before.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;debug&lt;/code&gt; on a text node also outlines its lines: each line box, its baseline, and the glyphs of
each run on the line.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Field.options&lt;/code&gt; holds at most 1000 options of at most 1000 characters each, and &lt;code&gt;Field.value&lt;/code&gt;
at most 8000 characters; longer ones are &lt;code&gt;INVALID_VALUE&lt;/code&gt;. Both were unbounded, so a document
with longer ones was valid before.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.6.5 - 2026-10-13</title>
    <link href="https://inkoxide.dev/changelog#065---2026-10-13"/>
    <id>https://inkoxide.dev/changelog#065---2026-10-13</id>
    <updated>2026-10-13T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Text is held to a limit on the glyphs shaping makes beyond one per byte of text: about
4 million over a render. Over it, the render fails with &lt;code&gt;FONT_ERROR&lt;/code&gt; naming the font. Text
shapes to about one glyph per byte or fewer, but a font whose layout tables substitute one
glyph with thousands made a few kilobytes of text take gigabytes of memory. Those glyphs
also count toward the limit on what is painted on every page, which counted characters only.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.6.4 - 2026-10-13</title>
    <link href="https://inkoxide.dev/changelog#064---2026-10-13"/>
    <id>https://inkoxide.dev/changelog#064---2026-10-13</id>
    <updated>2026-10-13T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A form field taller than a page is one widget, on the page it starts on: it was a widget on
every page it was sliced across, each embedding the field's editing glyphs again, so a single
field could make tens of thousands of widgets and take seconds to render. The glyphs for
editing a text or select field are now added once per font. Found by the &lt;code&gt;render_tree&lt;/code&gt; fuzz
target.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.6.3 - 2026-10-13</title>
    <link href="https://inkoxide.dev/changelog#063---2026-10-13"/>
    <id>https://inkoxide.dev/changelog#063---2026-10-13</id>
    <updated>2026-10-13T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A CFF font with a number in its Top, Font or Private DICT that overflows a 32-bit float (or
beyond 10³⁰) is rejected with &lt;code&gt;FONT_ERROR&lt;/code&gt; when it is registered. Registering it panicked in
the font subsetter: a panic in Rust, an &lt;code&gt;INTERNAL&lt;/code&gt; error in Node. Found by the &lt;code&gt;register_font&lt;/code&gt;
fuzz target.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.6.2 - 2026-10-13</title>
    <link href="https://inkoxide.dev/changelog#062---2026-10-13"/>
    <id>https://inkoxide.dev/changelog#062---2026-10-13</id>
    <updated>2026-10-13T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The limit on what is painted on every page is about 64 MB instead of 256 MB: a document just
under 256 MB made a PDF that size and took seconds to render.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.6.1 - 2026-10-13</title>
    <link href="https://inkoxide.dev/changelog#061---2026-10-13"/>
    <id>https://inkoxide.dev/changelog#061---2026-10-13</id>
    <updated>2026-10-13T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;What is painted on every page (headers, footers, overlays, fixed elements and repeated table
header rows) is held to a limit of about 256 MB over the whole document, estimated before
anything is painted; a document over it fails with &lt;code&gt;LAYOUT_ERROR&lt;/code&gt;. Repeated on every one of
thousands of pages, a few kilobytes of content (a fixed element taller than its page, links
on every line) made a PDF of several gigabytes. Elements shown on some pages only (&lt;code&gt;pages&lt;/code&gt;)
count for those pages. Found by the &lt;code&gt;render_tree&lt;/code&gt; fuzz target.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.6.0 - 2026-10-13</title>
    <link href="https://inkoxide.dev/changelog#060---2026-10-13"/>
    <id>https://inkoxide.dev/changelog#060---2026-10-13</id>
    <updated>2026-10-13T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Document metadata &lt;code&gt;producer&lt;/code&gt;, &lt;code&gt;pageMode&lt;/code&gt; and &lt;code&gt;pageLayout&lt;/code&gt; (&lt;code&gt;meta&lt;/code&gt; in the document, props of
&lt;code&gt;&amp;#x3C;Document&gt;&lt;/code&gt;): the PDF producer (default &lt;code&gt;Inkoxide &amp;#x3C;version&gt;&lt;/code&gt;), what the viewer shows when it
opens the PDF (no panel, the bookmarks, thumbnails or full screen; by default the bookmarks
when there are any, as before), and how it lays out the pages (one or two pages, or one or two
scrolling columns).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inkoxide&lt;/code&gt; (Rust): &lt;code&gt;validate_without_images&lt;/code&gt;, which validates a document without looking up
its image ids, as the Node &lt;code&gt;validate&lt;/code&gt; does when it is given no &lt;code&gt;images&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A URL entry in &lt;code&gt;remoteImages.allow&lt;/code&gt; is matched by origin and path instead of as a string
prefix: it allows URLs with the same scheme, host and port, and, when it has a path, that path
and the paths under it (&lt;code&gt;https://cdn.example.com/img&lt;/code&gt; allows &lt;code&gt;/img/logo.png&lt;/code&gt;, not
&lt;code&gt;/imgs/logo.png&lt;/code&gt;). Entries such as &lt;code&gt;https://cdn.example.com/&lt;/code&gt; work as before.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;inkoxide&lt;/code&gt; CLI's &lt;code&gt;validate&lt;/code&gt; refuses &lt;code&gt;--full-fonts&lt;/code&gt; and &lt;code&gt;--hyphenation&lt;/code&gt;, which it accepted
and ignored; &lt;code&gt;render&lt;/code&gt; and &lt;code&gt;bench&lt;/code&gt; take them as before.&lt;/li&gt;
&lt;li&gt;The published &lt;code&gt;@inkoxide/tailwind&lt;/code&gt; no longer declares &lt;code&gt;"sideEffects": false&lt;/code&gt;: importing it
loads &lt;code&gt;@tailwindcss/node&lt;/code&gt;, which registers Node module resolution hooks.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@inkoxide/tailwind&lt;/code&gt; README documents &lt;code&gt;DEFAULT_THEME_CSS&lt;/code&gt;, every &lt;code&gt;initTailwind&lt;/code&gt; option,
&lt;code&gt;setTailwindStrict&lt;/code&gt;, &lt;code&gt;tw.warm&lt;/code&gt; and &lt;code&gt;tw.cacheSize&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Lengths in &lt;code&gt;style&lt;/code&gt; and in classes take the same syntax, read by one reader in
&lt;code&gt;@inkoxide/style&lt;/code&gt;. &lt;code&gt;style&lt;/code&gt; now takes &lt;code&gt;calc()&lt;/code&gt; (&lt;code&gt;'calc(10mm + 1em)'&lt;/code&gt;), units in any case
(&lt;code&gt;'5PX'&lt;/code&gt;) and exponents (&lt;code&gt;'1e1pt'&lt;/code&gt;), and &lt;code&gt;em&lt;/code&gt;, &lt;code&gt;vw&lt;/code&gt; and &lt;code&gt;vh&lt;/code&gt; in a &lt;code&gt;transform&lt;/code&gt; or
&lt;code&gt;transformOrigin&lt;/code&gt; string. Classes now take &lt;code&gt;em&lt;/code&gt;, &lt;code&gt;vw&lt;/code&gt; and &lt;code&gt;vh&lt;/code&gt; (&lt;code&gt;w-[2em]&lt;/code&gt;, &lt;code&gt;h-[10vh]&lt;/code&gt;; &lt;code&gt;tw()&lt;/code&gt;
takes the font and page sizes as a second argument) and a space before the unit
(&lt;code&gt;w-[5_pt]&lt;/code&gt;). A number too large for a float (&lt;code&gt;'1e999pt'&lt;/code&gt;) is an error in both, as is a bare
&lt;code&gt;infinity&lt;/code&gt; (&lt;code&gt;w-[infinity]&lt;/code&gt;), which CSS takes only inside &lt;code&gt;calc()&lt;/code&gt;, and &lt;code&gt;style&lt;/code&gt; length errors
name the property (&lt;code&gt;width: 3ex (unsupported unit "ex")&lt;/code&gt;). &lt;code&gt;textDecoration&lt;/code&gt; and &lt;code&gt;objectFit&lt;/code&gt; in
&lt;code&gt;style&lt;/code&gt; are read as their classes are, and &lt;code&gt;text-decoration-line&lt;/code&gt; in a class takes both lines
(&lt;code&gt;underline line-through&lt;/code&gt;), as &lt;code&gt;style&lt;/code&gt; does.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Deprecated&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/style&lt;/code&gt;: &lt;code&gt;Reader&lt;/code&gt;, &lt;code&gt;ENUMS&lt;/code&gt;, &lt;code&gt;EnumKey&lt;/code&gt;, &lt;code&gt;evalDim&lt;/code&gt;, &lt;code&gt;Dim&lt;/code&gt;, &lt;code&gt;substituteVars&lt;/code&gt; and
&lt;code&gt;parseFontFamily&lt;/code&gt;, which only &lt;code&gt;@inkoxide/tailwind&lt;/code&gt; uses. They moved to
&lt;code&gt;@inkoxide/style/internal&lt;/code&gt;, which is for Inkoxide's own packages and not covered by semver,
and will be removed from the main entry.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TwStyle&lt;/code&gt; and &lt;code&gt;TwKey&lt;/code&gt; (from &lt;code&gt;@inkoxide/style&lt;/code&gt; and &lt;code&gt;@inkoxide/tailwind&lt;/code&gt;) are renamed
&lt;code&gt;FlatStyle&lt;/code&gt; and &lt;code&gt;FlatStyleKey&lt;/code&gt;: &lt;code&gt;style&lt;/code&gt; props build this style too, not only classes. The old
names stay as aliases for now.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/tailwind&lt;/code&gt;: &lt;code&gt;TailwindCompiler&lt;/code&gt;, which wraps Tailwind's unstable design-system API
and which &lt;code&gt;tw()&lt;/code&gt; cannot use (set a theme with &lt;code&gt;initTailwind({ css })&lt;/code&gt;), and &lt;code&gt;SUPPORTED&lt;/code&gt;,
&lt;code&gt;UNSUPPORTED&lt;/code&gt; and &lt;code&gt;SupportedFamily&lt;/code&gt;, the data of the Tailwind support table. They will be
removed from the package's exports.&lt;/li&gt;
&lt;li&gt;The default Tailwind theme's colours &lt;code&gt;ink&lt;/code&gt;, &lt;code&gt;ink-muted&lt;/code&gt;, &lt;code&gt;brand&lt;/code&gt;, &lt;code&gt;field&lt;/code&gt;, &lt;code&gt;field-line&lt;/code&gt;,
&lt;code&gt;rule&lt;/code&gt; and &lt;code&gt;head&lt;/code&gt; (&lt;code&gt;bg-brand&lt;/code&gt;, &lt;code&gt;text-ink&lt;/code&gt;, …), made for the engine's test documents, will be
removed from it. Set them in your own theme (&lt;code&gt;initTailwind({ css })&lt;/code&gt;) to keep using them.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/react&lt;/code&gt;: &lt;code&gt;FontRegistry.loadDirectory&lt;/code&gt; and the &lt;code&gt;file&lt;/code&gt; field of &lt;code&gt;FontFace&lt;/code&gt; that it
reads. Read the font files yourself and pass their bytes to &lt;code&gt;FontRegistry.register&lt;/code&gt;;
&lt;code&gt;@inkoxide/render&lt;/code&gt; now loads its bundled fonts that way.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/react&lt;/code&gt;: &lt;code&gt;FontRegistry.families()&lt;/code&gt;, which nothing uses.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The examples in the &lt;code&gt;@inkoxide/render&lt;/code&gt; README run when copied into a new project and started
with &lt;code&gt;npx tsx&lt;/code&gt;, as the README says: like the quick start, they name the JSX runtime and end in
a plain &lt;code&gt;renderToFile&lt;/code&gt; call, without a top-level await. The files in &lt;code&gt;examples/&lt;/code&gt; render their
PDF on every Node.js 22, not only on 22.18 and later, also when started through a symlinked
path such as &lt;code&gt;/tmp&lt;/code&gt; on macOS.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate&lt;/code&gt; decodes the images a document uses, so an image that does not decode is
&lt;code&gt;INVALID_IMAGE&lt;/code&gt; at its path. Until now only the header was read: a corrupt or truncated PNG
passed validation and failed the render with &lt;code&gt;IMAGE_ERROR&lt;/code&gt; and no path. A JPEG is still
embedded without decoding, but its structure is checked: a JPEG cut short, or coded in a way
PDF readers cannot show (lossless, hierarchical or arithmetic), is &lt;code&gt;INVALID_IMAGE&lt;/code&gt;. The decode
is cached, so the render that follows does not decode again.&lt;/li&gt;
&lt;li&gt;Node: &lt;code&gt;validate&lt;/code&gt; without &lt;code&gt;images&lt;/code&gt; found nothing past 100 image references. It counted the
&lt;code&gt;UNKNOWN_IMAGE&lt;/code&gt; errors it then dropped against the 100-error cap; it now skips the image
lookups instead.&lt;/li&gt;
&lt;li&gt;Node: &lt;code&gt;threads&lt;/code&gt; and &lt;code&gt;compressionLevel&lt;/code&gt; that are not integers in range (&lt;code&gt;threads&lt;/code&gt; 1–256,
&lt;code&gt;compressionLevel&lt;/code&gt; 0–9) are &lt;code&gt;INVALID_ARGUMENT&lt;/code&gt;. They were converted: &lt;code&gt;threads: -1&lt;/code&gt; started a
256-thread pool, and &lt;code&gt;1.5&lt;/code&gt;, &lt;code&gt;NaN&lt;/code&gt; or &lt;code&gt;2 ** 32 + 3&lt;/code&gt; were truncated or wrapped. An invalid
&lt;code&gt;creationDate&lt;/code&gt; is &lt;code&gt;INVALID_ARGUMENT&lt;/code&gt; too, like every other bad option, not the document code
&lt;code&gt;INVALID_VALUE&lt;/code&gt;. &lt;code&gt;@inkoxide/render&lt;/code&gt; reports a bad &lt;code&gt;threads&lt;/code&gt; or &lt;code&gt;creationDate&lt;/code&gt; as
&lt;code&gt;ENGINE_REJECTED&lt;/code&gt; (400) at the option's path (&lt;code&gt;options.threads&lt;/code&gt;); &lt;code&gt;threads: 0&lt;/code&gt; was an
&lt;code&gt;INTERNAL&lt;/code&gt; error (500).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render&lt;/code&gt;: &lt;code&gt;imageTimeoutMs&lt;/code&gt;, &lt;code&gt;remoteImages.timeoutMs&lt;/code&gt; and the &lt;code&gt;maxBytes&lt;/code&gt; of
&lt;code&gt;remoteImages&lt;/code&gt; and &lt;code&gt;localImages&lt;/code&gt; must be numbers from 0 (&lt;code&gt;Infinity&lt;/code&gt; included); another value
fails the render with &lt;code&gt;ENGINE_REJECTED&lt;/code&gt; at its path before any image loads. A NaN or negative
timeout failed every image at once, and a NaN &lt;code&gt;maxBytes&lt;/code&gt; lifted the limit. A fractional
timeout is rounded up to a whole millisecond: &lt;code&gt;remoteImages.timeoutMs: 1.5&lt;/code&gt; failed every
remote image with a &lt;code&gt;RangeError&lt;/code&gt; message.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render&lt;/code&gt;: a remote image answered with a 3xx status that is not a redirect (&lt;code&gt;304&lt;/code&gt;)
fails with &lt;code&gt;HTTP 304&lt;/code&gt;, not &lt;code&gt;HTTP 304, and redirects are not followed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Node: &lt;code&gt;InkoxideError&lt;/code&gt; no longer has an undocumented &lt;code&gt;detail&lt;/code&gt; property (the same text as
&lt;code&gt;message&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Rust: the &lt;code&gt;RenderOptions::threads&lt;/code&gt; docs state that a pool has at most 256 threads and that
&lt;code&gt;Some(0)&lt;/code&gt; renders on the calling thread, as &lt;code&gt;Some(1)&lt;/code&gt; does.&lt;/li&gt;
&lt;li&gt;A transform function with two keys or none (&lt;code&gt;{ translate: [1, 2], rotate: 5 }&lt;/code&gt;, &lt;code&gt;{}&lt;/code&gt;), and an
array with more items than its fixed length (a five-item colour, a three-item
&lt;code&gt;transformOrigin&lt;/code&gt;), are &lt;code&gt;INVALID_VALUE&lt;/code&gt; at their path. They were &lt;code&gt;PARSE_ERROR&lt;/code&gt; ("not
well-formed JSON") with no path.&lt;/li&gt;
&lt;li&gt;JSON and MessagePack report a bad &lt;code&gt;version&lt;/code&gt; alike: a number other than 1 (&lt;code&gt;2&lt;/code&gt;, &lt;code&gt;2.5&lt;/code&gt;) is
&lt;code&gt;UNSUPPORTED_VERSION&lt;/code&gt;, and a &lt;code&gt;version&lt;/code&gt; that is not a number (&lt;code&gt;"1"&lt;/code&gt;, &lt;code&gt;true&lt;/code&gt;) is
&lt;code&gt;INVALID_TYPE&lt;/code&gt;. JSON reported &lt;code&gt;"1"&lt;/code&gt; as an unsupported version, and MessagePack reported &lt;code&gt;2.5&lt;/code&gt;
as a wrong type.&lt;/li&gt;
&lt;li&gt;Validation follows the schema more closely (see the
&lt;a href="https://inkoxide.dev/changelog#schema"&gt;schema changelog&lt;/a&gt;): an integer past the engine's integer type is
&lt;code&gt;OUT_OF_RANGE&lt;/code&gt;, not &lt;code&gt;INVALID_VALUE&lt;/code&gt;; &lt;code&gt;".5%"&lt;/code&gt;, &lt;code&gt;"5.%"&lt;/code&gt;, &lt;code&gt;".5fr"&lt;/code&gt; and &lt;code&gt;"-0fr"&lt;/code&gt; are rejected, as
the schema rejects them; a &lt;code&gt;"0fr"&lt;/code&gt; table column is &lt;code&gt;OUT_OF_RANGE&lt;/code&gt;; and an empty or too long
&lt;code&gt;link&lt;/code&gt; or bookmark &lt;code&gt;title&lt;/code&gt; is &lt;code&gt;INVALID_VALUE&lt;/code&gt;, like an id of the wrong length.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/schema&lt;/code&gt; types: a transform function or a grid track object takes the keys of one
form only, so &lt;code&gt;{ translate: [1, 2], rotate: 5 }&lt;/code&gt; no longer type-checks.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;inkoxide&lt;/code&gt; CLI printed a validation error's code twice (&lt;code&gt;UNKNOWN_FONT UNKNOWN_FONT at …&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;PageBreak&lt;/code&gt;, &lt;code&gt;break="page-before"&lt;/code&gt; or &lt;code&gt;break="page-after"&lt;/code&gt; inside a &lt;code&gt;View&lt;/code&gt; that fits on the
page starts a new page; it was ignored. It also breaks inside &lt;code&gt;break="avoid"&lt;/code&gt;
(&lt;code&gt;wrap={false}&lt;/code&gt;), as in CSS, and a break before a &lt;code&gt;View&lt;/code&gt;'s first child takes the &lt;code&gt;View&lt;/code&gt; to the
new page instead of leaving its top padding behind. Inside a &lt;code&gt;flex-row&lt;/code&gt;, grid, wrapping or
reversed &lt;code&gt;View&lt;/code&gt;, and inside an absolutely positioned, rotated or transformed element, where it
did nothing, it is now an error, as is &lt;code&gt;break="page-before"&lt;/code&gt; or &lt;code&gt;break="page-after"&lt;/code&gt; on an
absolutely positioned element.&lt;/li&gt;
&lt;li&gt;A page break at the end of the content, or one with nothing above it on its page (at the top
of the first page, below a top margin), no longer adds a blank page.&lt;/li&gt;
&lt;li&gt;An absolutely positioned element of the page body written after a page break is placed from
the top of the body, like one written first; it moved with the break and could be cut across
two pages.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;flex-row&lt;/code&gt;, grid or wrapping &lt;code&gt;View&lt;/code&gt; taller than a page splits between the lines of its
children, each column continuing on the next page, and a &lt;code&gt;break="avoid"&lt;/code&gt; element taller than
a page splits like any other; both were cut through a line of text at the page edge.&lt;/li&gt;
&lt;li&gt;Fixed elements in the body are painted in document order and by &lt;code&gt;zIndex&lt;/code&gt;, as without &lt;code&gt;fixed&lt;/code&gt;;
nested ones were painted in the wrong order.&lt;/li&gt;
&lt;li&gt;A table cell's &lt;code&gt;height&lt;/code&gt; is a minimum, as in CSS: the row grows to fit the cell's content.
Until now it fixed the row's height, and taller content ran over the next row.&lt;/li&gt;
&lt;li&gt;Styles a table cell ignored are validation errors (&lt;code&gt;STYLE_NOT_APPLICABLE&lt;/code&gt;), like &lt;code&gt;transform&lt;/code&gt;
and &lt;code&gt;zIndex&lt;/code&gt; already were: &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;minWidth&lt;/code&gt;, &lt;code&gt;maxWidth&lt;/code&gt;, &lt;code&gt;maxHeight&lt;/code&gt;, &lt;code&gt;margin&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt;,
&lt;code&gt;inset&lt;/code&gt;, &lt;code&gt;alignSelf&lt;/code&gt;, &lt;code&gt;justifySelf&lt;/code&gt;, &lt;code&gt;flexGrow&lt;/code&gt;, &lt;code&gt;flexShrink&lt;/code&gt;, &lt;code&gt;flexBasis&lt;/code&gt;, &lt;code&gt;gridRow&lt;/code&gt;,
&lt;code&gt;gridColumn&lt;/code&gt;, &lt;code&gt;gridArea&lt;/code&gt;, &lt;code&gt;overflow&lt;/code&gt;, &lt;code&gt;opacity&lt;/code&gt; and &lt;code&gt;rotate&lt;/code&gt;. They passed validation and had no
effect, or, for &lt;code&gt;minWidth&lt;/code&gt; and &lt;code&gt;maxWidth&lt;/code&gt;, changed the width of the cell's text but not of its
column, so the text ran into the next cell or wrapped in a narrow strip.&lt;/li&gt;
&lt;li&gt;Collapsed table borders (the default &lt;code&gt;borderCollapse&lt;/code&gt;) keep their &lt;code&gt;borderStyle&lt;/code&gt;: dashed and
dotted cell borders were drawn solid. Where two borders of equal width meet, solid wins over
dashed and dashed over dotted, as in CSS.&lt;/li&gt;
&lt;li&gt;A box whose first child has &lt;code&gt;minPresenceAhead&lt;/code&gt; or &lt;code&gt;break.before: "page"&lt;/code&gt; (or is a
&lt;code&gt;PageBreak&lt;/code&gt;) moves to the next page with that child, as CSS carries a break up to the parent.
It left its empty top (padding and background) at the foot of the page before.&lt;/li&gt;
&lt;li&gt;A table whose first row is taller than a page starts on the current page and splits that row,
as it already did for later rows. It moved whole to a new page, leaving the rest of the page
empty.&lt;/li&gt;
&lt;li&gt;Percentage heights of a &lt;code&gt;Page&lt;/code&gt;'s children (&lt;code&gt;height: '50%'&lt;/code&gt;, &lt;code&gt;minHeight&lt;/code&gt;, &lt;code&gt;maxHeight&lt;/code&gt;) are of
the page content height. They behaved as &lt;code&gt;auto&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Text collapses only spaces, tabs and line breaks, as in CSS. Thin, em, figure, ideographic and
other space characters keep their own width, and the non-breaking ones no longer allow a line
break.&lt;/li&gt;
&lt;li&gt;Tabs in &lt;code&gt;pre&lt;/code&gt; and &lt;code&gt;pre-wrap&lt;/code&gt; text advance to the next tab stop, as CSS &lt;code&gt;tab-size: 8&lt;/code&gt;: stops are
8 spaces of the font apart from the start of the line, so tab-separated columns line up in any
font. Each tab was four spaces.&lt;/li&gt;
&lt;li&gt;A box sized to the min-content width of hyphenated text leaves room for the hyphen; the
hyphen was drawn outside it.&lt;/li&gt;
&lt;li&gt;The docs say how &lt;code&gt;minPresenceAhead&lt;/code&gt;, percentage gaps and relative &lt;code&gt;top&lt;/code&gt;/&lt;code&gt;left&lt;/code&gt; offsets work.&lt;/li&gt;
&lt;li&gt;A creation date with an offset under an hour west of UTC (&lt;code&gt;-00:30&lt;/code&gt;) is written with its sign
in the PDF information dictionary; it was written east of UTC, an hour off the XMP date that
PDF/A requires it to match.&lt;/li&gt;
&lt;li&gt;Registering a font collection (&lt;code&gt;.ttc&lt;/code&gt;) fails with &lt;code&gt;FONT_ERROR&lt;/code&gt;. It was accepted, only its first
face could be used, and with &lt;code&gt;subsetFonts: false&lt;/code&gt; the whole collection was embedded where the
PDF needs a single TrueType program.&lt;/li&gt;
&lt;li&gt;An SVG image whose width or height is under 0.00001 px, or whose &lt;code&gt;viewBox&lt;/code&gt; width or height
is, fails with &lt;code&gt;INVALID_IMAGE&lt;/code&gt; at its path (a nested SVG image is left out). Scaling it up
made the PDF writer output &lt;code&gt;inf&lt;/code&gt;, or numbers too large for PDF readers, in the image's matrix.&lt;/li&gt;
&lt;li&gt;Huge but finite numbers in an SVG image (&lt;code&gt;transform="scale(1e30)"&lt;/code&gt;, a gradient, mask,
pattern or shape at 1e30) no longer make an invalid PDF. The PDF writer wrote them as digits
only, integers far past the 32 bits PDF readers parse, which they treat as errors or null.
Every number the writer emits, in page content, SVG drawings, annotations and dictionaries,
is now at most ±10¹¹.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;borderRadius&lt;/code&gt; rounds an image's corners without &lt;code&gt;overflow: hidden&lt;/code&gt;: the image is clipped to
the rounded content box, as in CSS.&lt;/li&gt;
&lt;li&gt;A select field embeds the glyphs of the common Latin range and of every option, as a text
field does, so a viewer can show the option picked; it embedded only those of its value.&lt;/li&gt;
&lt;li&gt;The mark of a checkbox or radio button takes its size from &lt;code&gt;size&lt;/code&gt;, centred in the widget; it
grew with the widget box.&lt;/li&gt;
&lt;li&gt;A checkbox or radio button without a width or height keeps its &lt;code&gt;size&lt;/code&gt; + 2 pt square where its
container would stretch it (as in a column, by default); it stretched across the container.&lt;/li&gt;
&lt;li&gt;A radio button whose value is &lt;code&gt;Off&lt;/code&gt;, or longer than 127 bytes, is no longer written with a
broken appearance state: such a group names its states by position and lists its values in
&lt;code&gt;/Opt&lt;/code&gt;. A checked &lt;code&gt;Off&lt;/code&gt; button showed as unselected.&lt;/li&gt;
&lt;li&gt;Links and form fields inside a box with &lt;code&gt;transform&lt;/code&gt; or &lt;code&gt;rotate&lt;/code&gt; are clickable where the box is
drawn: their areas follow the transform, and a field's appearance turns and scales with it.
They stayed at the untransformed position.&lt;/li&gt;
&lt;li&gt;An SVG with a &lt;code&gt;viewBox&lt;/code&gt; drawn with &lt;code&gt;fit: fill&lt;/code&gt; (the default) into a box of another shape keeps
its proportions, as in CSS: the box becomes its viewport, and its &lt;code&gt;preserveAspectRatio&lt;/code&gt;
(centred and whole by default) places the drawing. It was stretched to the box, so an &lt;code&gt;&amp;#x3C;Svg&gt;&lt;/code&gt;
given &lt;code&gt;width: '100%'&lt;/code&gt; came out squashed.&lt;/li&gt;
&lt;li&gt;An SVG element with a &lt;code&gt;filter&lt;/code&gt; (a drop shadow, a blur) is drawn without the filter; it was left
out with its children, so a logo with a shadow showed as a blank box.&lt;/li&gt;
&lt;li&gt;SVG gradients with &lt;code&gt;spreadMethod="repeat"&lt;/code&gt; or &lt;code&gt;"reflect"&lt;/code&gt; repeat or mirror their stops over
what they paint; they were drawn as &lt;code&gt;pad&lt;/code&gt;, one ramp and then a flat colour.&lt;/li&gt;
&lt;li&gt;GIF and WebP images embedded in an SVG as &lt;code&gt;data:&lt;/code&gt; URLs are drawn; they were left out.&lt;/li&gt;
&lt;li&gt;An SVG fails with &lt;code&gt;INVALID_IMAGE&lt;/code&gt; when an SVG image nested in it has text, as it does with text
of its own, and when an image embedded in it is over 50 megapixels; both were left out
silently. The guide lists what SVG drawing does not support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grid placement:&lt;/strong&gt; a &lt;code&gt;gridColumn&lt;/code&gt; or &lt;code&gt;gridRow&lt;/code&gt; with &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt; and &lt;code&gt;span&lt;/code&gt; runs from
&lt;code&gt;start&lt;/code&gt; to &lt;code&gt;end&lt;/code&gt;, as in CSS; the engine used &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;span&lt;/code&gt; and ignored &lt;code&gt;end&lt;/code&gt;. In Tailwind,
a span after the slash is kept (&lt;code&gt;col-[2/span_3]&lt;/code&gt; was one column at line 2),
&lt;code&gt;col-span-3 col-start-1 col-end-3&lt;/code&gt; is 1 / 3, not three columns, and a span at the end is
ignored after one at the start (&lt;code&gt;col-start-[span_2] col-end-[span_3]&lt;/code&gt; is two columns).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind themes:&lt;/strong&gt; &lt;code&gt;initTailwind({ css })&lt;/code&gt; run alongside &lt;code&gt;init()&lt;/code&gt; or another &lt;code&gt;initTailwind()&lt;/code&gt;
keeps its theme; whichever load finished last used to win, so the default theme could replace
it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;flex&lt;/code&gt;:&lt;/strong&gt; a lone length is the basis, as in CSS: &lt;code&gt;flex: '30%'&lt;/code&gt; and &lt;code&gt;flex-[30%]&lt;/code&gt; are
&lt;code&gt;1 1 30%&lt;/code&gt;. The style prop rejected it and the class read it as a grow of 0.3. Grow and shrink
with a unit are an unsupported value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading values:&lt;/strong&gt; an unsupported Tailwind value always names the CSS property
(&lt;code&gt;leading-[abc]&lt;/code&gt;, &lt;code&gt;rotate-[abc]&lt;/code&gt;, &lt;code&gt;p-[1pt_2pt_3pt_4pt_5pt]&lt;/code&gt;, &lt;code&gt;m-[var(--nope)]&lt;/code&gt;), and
&lt;code&gt;parseTransform&lt;/code&gt; throws &lt;code&gt;UnsupportedValueError&lt;/code&gt; for a bad angle (&lt;code&gt;rotate(1grad)&lt;/code&gt;).
&lt;code&gt;rotate-none&lt;/code&gt; works, and lengths above 10000pt are no longer cut to 10000pt (&lt;code&gt;h-[12000pt]&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind mapping:&lt;/strong&gt; a class or style value outside what the engine takes fails where it is
read, naming the class: &lt;code&gt;line-clamp-0&lt;/code&gt;, &lt;code&gt;line-clamp-[2.5]&lt;/code&gt;, &lt;code&gt;opacity-[1.5]&lt;/code&gt;, &lt;code&gt;col-span-0&lt;/code&gt;,
&lt;code&gt;aspect-[0]&lt;/code&gt;, a skew of ±90deg or more, and a percentage or negative grow or shrink
(&lt;code&gt;grow-[30%]&lt;/code&gt;, &lt;code&gt;shrink-[-1]&lt;/code&gt;), which CSS rejects. &lt;code&gt;grid-flow-dense&lt;/code&gt; works. Switching strict
mode on checks class strings cached while it was off. Only selector problems that style
children suggest &lt;code&gt;gap&lt;/code&gt;; &lt;code&gt;container&lt;/code&gt; says that media queries have no meaning in a PDF.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Style values:&lt;/strong&gt; &lt;code&gt;transformOrigin: 'center left'&lt;/code&gt; and &lt;code&gt;'center right'&lt;/code&gt; work, and names such
as &lt;code&gt;toString&lt;/code&gt; are not taken as keywords by &lt;code&gt;transformOrigin&lt;/code&gt; or &lt;code&gt;fontWeight&lt;/code&gt;. Colour keywords
are case-insensitive (&lt;code&gt;'Transparent'&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docs:&lt;/strong&gt; the Fonts section and the Tailwind support table say that &lt;code&gt;font-serif&lt;/code&gt; is Noto Sans,
since no serif face is bundled, and the &lt;code&gt;@inkoxide/schema&lt;/code&gt; README's ajv example type-checks
with &lt;code&gt;module: nodenext&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A component that suspends (&lt;code&gt;use()&lt;/code&gt; of a promise, a &lt;code&gt;lazy&lt;/code&gt; component) fails the render with a
template error that names it (a &lt;code&gt;lazy&lt;/code&gt; component by its &lt;code&gt;displayName&lt;/code&gt;, a &lt;code&gt;render&lt;/code&gt; prop by its
element), or the &lt;code&gt;&amp;#x3C;Suspense&gt;&lt;/code&gt; boundary it is in, and says that templates cannot wait for data.
Without a boundary the error blamed a missing &lt;code&gt;&amp;#x3C;Document&gt;&lt;/code&gt;; with one, the PDF showed the
fallback and the process crashed shortly afterwards.&lt;/li&gt;
&lt;li&gt;A later &lt;code&gt;border&lt;/code&gt;, &lt;code&gt;borderStyle&lt;/code&gt; or &lt;code&gt;borderRadius&lt;/code&gt; for every side, in a style array or over
&lt;code&gt;className&lt;/code&gt;, replaces the per-side styles and per-corner radii set before it, and a per-corner
&lt;code&gt;borderRadius&lt;/code&gt; object keeps the corners it does not name instead of setting them to 0.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/react&lt;/code&gt; errors for a style key an element does not take (&lt;code&gt;maxLines&lt;/code&gt; on a &lt;code&gt;View&lt;/code&gt;,
&lt;code&gt;padding&lt;/code&gt; on a &lt;code&gt;Row&lt;/code&gt; or an inline &lt;code&gt;Text&lt;/code&gt;) have kind &lt;code&gt;'props'&lt;/code&gt; and name the style when the key
comes from &lt;code&gt;style&lt;/code&gt;; only a key from &lt;code&gt;className&lt;/code&gt; is kind &lt;code&gt;'tailwind'&lt;/code&gt;, with the class. A string
directly in a &lt;code&gt;Table&lt;/code&gt;, &lt;code&gt;Row&lt;/code&gt; or &lt;code&gt;Document&lt;/code&gt; says where it belongs (a &lt;code&gt;Cell&lt;/code&gt;, a &lt;code&gt;Page&lt;/code&gt;) instead
of suggesting a &lt;code&gt;Text&lt;/code&gt;, which is not allowed there either. The remote-image error of
&lt;code&gt;@inkoxide/react&lt;/code&gt; names its own option, &lt;code&gt;remoteImages.allow&lt;/code&gt;, and only &lt;code&gt;@inkoxide/render&lt;/code&gt; adds
the &lt;code&gt;INKOXIDE_REMOTE_IMAGES&lt;/code&gt; variable, for every refused URL or request.&lt;/li&gt;
&lt;li&gt;Percentage padding on a &lt;code&gt;Page&lt;/code&gt; (&lt;code&gt;style={{ padding: '10%' }}&lt;/code&gt;, &lt;code&gt;p-[10%]&lt;/code&gt;) is a share of the
page width on every side, as in CSS, instead of being dropped, which also left the header and
footer unaligned. A percentage minimum height on a &lt;code&gt;Row&lt;/code&gt; (&lt;code&gt;min-h-[10%]&lt;/code&gt;), which has no table
height to resolve against, is a template error instead of being dropped.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Text&lt;/code&gt; or &lt;code&gt;Link&lt;/code&gt; inside a &lt;code&gt;Text&lt;/code&gt; with &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;bookmark&lt;/code&gt;, &lt;code&gt;pages&lt;/code&gt;, &lt;code&gt;render&lt;/code&gt;, &lt;code&gt;dir&lt;/code&gt;, &lt;code&gt;orphans&lt;/code&gt;,
&lt;code&gt;widows&lt;/code&gt;, &lt;code&gt;break&lt;/code&gt;, &lt;code&gt;wrap={false}&lt;/code&gt; or &lt;code&gt;minPresenceAhead&lt;/code&gt; is a template error that names the
prop; these need a paragraph of their own and were dropped. A run in another &lt;code&gt;lang&lt;/code&gt; inside a
hyphenated paragraph is no longer hyphenated with the paragraph's patterns.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;textTransform&lt;/code&gt; with a &lt;code&gt;lang&lt;/code&gt; that JavaScript does not take as a language tag (&lt;code&gt;en_US&lt;/code&gt;) cases
the text by the default rules instead of failing with an internal error.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;truncate&lt;/code&gt;, &lt;code&gt;line-clamp-*&lt;/code&gt;, &lt;code&gt;maxLines&lt;/code&gt; and &lt;code&gt;ellipsis&lt;/code&gt; on a &lt;code&gt;Page&lt;/code&gt;, &lt;code&gt;Row&lt;/code&gt; or &lt;code&gt;Spacer&lt;/code&gt; are a
template error, as on other elements, instead of doing nothing; a negative numeric &lt;code&gt;flex&lt;/code&gt;
(&lt;code&gt;style={{ flex: -1 }}&lt;/code&gt;) is rejected where the style is read, like &lt;code&gt;flex: '-1'&lt;/code&gt;, and so is a
negative or non-numeric &lt;code&gt;flexGrow&lt;/code&gt; or &lt;code&gt;flexShrink&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ClipPath&lt;/code&gt; and &lt;code&gt;Defs&lt;/code&gt; have exported props types, &lt;code&gt;ClipPathProps&lt;/code&gt; and &lt;code&gt;DefsProps&lt;/code&gt; (also from
&lt;code&gt;@inkoxide/render&lt;/code&gt;), and &lt;code&gt;ClipPath&lt;/code&gt; takes &lt;code&gt;clipPathUnits&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render&lt;/code&gt;: a remote image that times out or drops its connection after the headers
fails the render with &lt;code&gt;IMAGE_FETCH_FAILED&lt;/code&gt;, as one that fails before them does, instead of a
raw &lt;code&gt;TimeoutError&lt;/code&gt; or &lt;code&gt;TypeError&lt;/code&gt;. &lt;code&gt;remoteImages.timeoutMs&lt;/code&gt; takes &lt;code&gt;Infinity&lt;/code&gt; (no limit) and
values past what a timer holds, as &lt;code&gt;imageTimeoutMs&lt;/code&gt; does, instead of throwing a &lt;code&gt;RangeError&lt;/code&gt;.
A redirect from an allowed URL, still not followed, fails with
&lt;code&gt;HTTP 302, and redirects are not followed&lt;/code&gt; instead of &lt;code&gt;fetch failed&lt;/code&gt;, and the guide and
SECURITY.md say so.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render&lt;/code&gt;: &lt;code&gt;Font.register&lt;/code&gt; fetches a URL with a 30-second limit for the whole
download and a 50 MiB size limit, through the same download code as remote images. A font
host that never answered held up every render in the process for up to five minutes, or for
good when it sent bytes slowly; now the render that loads the font fails with &lt;code&gt;INVALID_FONT&lt;/code&gt;
(&lt;code&gt;timed out after 30000 ms&lt;/code&gt;). The guide lists &lt;code&gt;http:&lt;/code&gt; URLs, which were already fetched.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render/templates&lt;/code&gt;: &lt;code&gt;renderTree&lt;/code&gt; called before &lt;code&gt;init()&lt;/code&gt; has finished, or while fonts
added with &lt;code&gt;Font.register&lt;/code&gt; are not loaded yet, throws an &lt;code&gt;INTERNAL&lt;/code&gt; RenderError that says to
call &lt;code&gt;await init()&lt;/code&gt;, instead of failing with &lt;code&gt;No fonts are registered&lt;/code&gt;, pointing to functions
the package does not export, or building a tree without those fonts. &lt;code&gt;renderSettled&lt;/code&gt; runs
&lt;code&gt;init()&lt;/code&gt; itself. The guide says that &lt;code&gt;renderTree&lt;/code&gt; needs &lt;code&gt;init()&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render/templates&lt;/code&gt;: &lt;code&gt;renderDocument&lt;/code&gt; takes images from the &lt;code&gt;images&lt;/code&gt; option as well as
from its &lt;code&gt;images&lt;/code&gt; argument, which it ignored. &lt;code&gt;generate&lt;/code&gt; resolves with &lt;code&gt;images&lt;/code&gt;, the images the
document was rendered with by id (and &lt;code&gt;renderSettled&lt;/code&gt; with the same), so a stored
&lt;code&gt;result.document&lt;/code&gt; whose images were bytes, files, URLs or functions renders again with
&lt;code&gt;renderDocument(result.document, result.images)&lt;/code&gt;; until now the ids the tree made up for them
(&lt;code&gt;image-0&lt;/code&gt;) failed with &lt;code&gt;UNKNOWN_IMAGE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Font.register&lt;/code&gt; tells faces apart by family, weight and style, and compares byte sources by
their bytes. It throws for a face the bundled fonts have (&lt;code&gt;Inter&lt;/code&gt; 400 from another file was
accepted and never used), for a family whose font id another family has (&lt;code&gt;Brand-Two&lt;/code&gt; after
&lt;code&gt;Brand Two&lt;/code&gt; was skipped without a word), for a different font of the same byte length (was
ignored), and for a &lt;code&gt;src&lt;/code&gt; that is not a path, a URL or bytes (&lt;code&gt;null&lt;/code&gt; failed the next render
with a &lt;code&gt;TypeError&lt;/code&gt;). A registration that throws adds none of its faces. The guide gives font
weights as 1–1000, the range the code accepts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render&lt;/code&gt;: &lt;code&gt;renderToBuffer(null)&lt;/code&gt;, or a string or a number in place of an element,
fails with a &lt;code&gt;TEMPLATE_ERROR&lt;/code&gt; RenderError instead of a &lt;code&gt;TypeError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render/templates&lt;/code&gt;: &lt;code&gt;generate&lt;/code&gt;'s &lt;code&gt;timings.engineMs&lt;/code&gt; no longer includes the tree
rebuilds for the page count, and &lt;code&gt;treeMs&lt;/code&gt; counts each rebuild once, so the stages add up.
&lt;code&gt;renderSettled&lt;/code&gt; resolves with that rebuild time as &lt;code&gt;treeMs&lt;/code&gt;. &lt;code&gt;GenerateOptions&lt;/code&gt; is the same
type as &lt;code&gt;RenderToBufferOptions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render&lt;/code&gt;: the start-up error for missing fonts says what &lt;code&gt;INKOXIDE_ASSETS_DIR&lt;/code&gt; must
point to, and the guide's FAQ explains when to set it (a bundled deployment).&lt;/li&gt;
&lt;li&gt;A font whose &lt;code&gt;GSUB&lt;/code&gt; or &lt;code&gt;GPOS&lt;/code&gt; lookups point to a missing or malformed coverage table is
rejected with &lt;code&gt;FONT_ERROR&lt;/code&gt; when it is registered. It registered, and the first render that
shaped text with it panicked in the shaper: a panic in Rust, an &lt;code&gt;INTERNAL&lt;/code&gt; error in Node. The
Rust &lt;code&gt;render&lt;/code&gt; now returns any panic as an error: &lt;code&gt;FONT_ERROR&lt;/code&gt; naming the font when it happens
while shaping, &lt;code&gt;INTERNAL&lt;/code&gt; otherwise.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>@inkoxide/schema 2.3.0 - 2026-10-13</title>
    <link href="https://inkoxide.dev/changelog#schema-230---2026-10-13"/>
    <id>https://inkoxide.dev/changelog#schema-230---2026-10-13</id>
    <updated>2026-10-13T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Meta.producer&lt;/code&gt;, &lt;code&gt;Meta.pageMode&lt;/code&gt; and &lt;code&gt;Meta.pageLayout&lt;/code&gt;: the PDF producer (default
&lt;code&gt;Inkoxide &amp;#x3C;version&gt;&lt;/code&gt;), what the viewer shows when it opens the document (&lt;code&gt;useNone&lt;/code&gt;,
&lt;code&gt;useOutlines&lt;/code&gt;, &lt;code&gt;useThumbs&lt;/code&gt;, &lt;code&gt;fullScreen&lt;/code&gt;; default &lt;code&gt;useOutlines&lt;/code&gt; when the document has
bookmarks, else &lt;code&gt;useNone&lt;/code&gt;, as before), and how it lays out the pages (&lt;code&gt;singlePage&lt;/code&gt;,
&lt;code&gt;oneColumn&lt;/code&gt;, &lt;code&gt;twoColumnLeft&lt;/code&gt;, &lt;code&gt;twoColumnRight&lt;/code&gt;, &lt;code&gt;twoPageLeft&lt;/code&gt;, &lt;code&gt;twoPageRight&lt;/code&gt;; default: not
set). All optional; documents without them render as before.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Engine parity:&lt;/strong&gt; integers are bounded as the schema states. An integer past the engine's own
integer type (&lt;code&gt;weight: 70000&lt;/code&gt;, &lt;code&gt;level: 300&lt;/code&gt;, &lt;code&gt;orphans: -1&lt;/code&gt;, &lt;code&gt;maxLines: 5000000000&lt;/code&gt;) is
&lt;code&gt;OUT_OF_RANGE&lt;/code&gt;, like one past a bound the schema states; it was &lt;code&gt;INVALID_VALUE&lt;/code&gt;. The schema
states those limits where it had none: &lt;code&gt;maxLines&lt;/code&gt;, &lt;code&gt;Field.maxLength&lt;/code&gt;, &lt;code&gt;colSpan&lt;/code&gt;, &lt;code&gt;rowSpan&lt;/code&gt; and
&lt;code&gt;headerRows&lt;/code&gt; are at most 4294967295, and &lt;code&gt;pages&lt;/code&gt; numbers are within ±2147483647 (from
-2147483648). The engine already rejected larger values.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Engine parity:&lt;/strong&gt; percentages and flexible sizes are read exactly as the &lt;code&gt;Percent&lt;/code&gt; and &lt;code&gt;Fr&lt;/code&gt;
patterns state. The engine accepted &lt;code&gt;".5%"&lt;/code&gt;, &lt;code&gt;"5.%"&lt;/code&gt;, &lt;code&gt;".5fr"&lt;/code&gt; and &lt;code&gt;"-0fr"&lt;/code&gt;, which the
schema rejects; they are &lt;code&gt;INVALID_VALUE&lt;/code&gt;. A table column of &lt;code&gt;"0fr"&lt;/code&gt; is rejected by both now:
the schema states that an fr column width is positive, as a width in points is, and the
engine reports it as &lt;code&gt;OUT_OF_RANGE&lt;/code&gt; instead of &lt;code&gt;INVALID_VALUE&lt;/code&gt; with a misleading message.&lt;/li&gt;
&lt;li&gt;An empty or too long &lt;code&gt;link&lt;/code&gt; or bookmark &lt;code&gt;title&lt;/code&gt; is &lt;code&gt;INVALID_VALUE&lt;/code&gt;, like an &lt;code&gt;Id&lt;/code&gt; or a field
&lt;code&gt;name&lt;/code&gt; of the wrong length; it was &lt;code&gt;OUT_OF_RANGE&lt;/code&gt;, which is for numbers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forced breaks:&lt;/strong&gt; a &lt;code&gt;pageBreak&lt;/code&gt; or &lt;code&gt;break.before/after: "page"&lt;/code&gt; inside a row, grid, wrapping
or reversed box, or inside a positioned, rotated, transformed or fixed node, is
&lt;code&gt;INVALID_NODE&lt;/code&gt; / &lt;code&gt;INVALID_VALUE&lt;/code&gt;, and so is &lt;code&gt;before/after: "page"&lt;/code&gt; on an absolutely
positioned node: the engine accepted them and did nothing. Forced breaks elsewhere work at any
depth and inside &lt;code&gt;inside: "avoid"&lt;/code&gt;, and one at the end of the flow adds no page. &lt;code&gt;Break&lt;/code&gt;
documents how rows, grids and content taller than a page split.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Table cells:&lt;/strong&gt; &lt;code&gt;TableCell.style&lt;/code&gt; says how a cell is sized. A cell's &lt;code&gt;height&lt;/code&gt; is a minimum
(the row grows to fit the content) instead of a fixed height that let the content overlap the
next row. The styles a cell ignored or misapplied are &lt;code&gt;STYLE_NOT_APPLICABLE&lt;/code&gt;: &lt;code&gt;width&lt;/code&gt;,
&lt;code&gt;minWidth&lt;/code&gt;, &lt;code&gt;maxWidth&lt;/code&gt;, &lt;code&gt;maxHeight&lt;/code&gt;, &lt;code&gt;margin&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt;, &lt;code&gt;inset&lt;/code&gt;, &lt;code&gt;alignSelf&lt;/code&gt;,
&lt;code&gt;justifySelf&lt;/code&gt;, &lt;code&gt;flexGrow&lt;/code&gt;, &lt;code&gt;flexShrink&lt;/code&gt;, &lt;code&gt;flexBasis&lt;/code&gt;, &lt;code&gt;gridRow&lt;/code&gt;, &lt;code&gt;gridColumn&lt;/code&gt;, &lt;code&gt;gridArea&lt;/code&gt;,
&lt;code&gt;overflow&lt;/code&gt;, &lt;code&gt;opacity&lt;/code&gt; and &lt;code&gt;rotate&lt;/code&gt;, as &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;transformOrigin&lt;/code&gt; and &lt;code&gt;zIndex&lt;/code&gt; already
were.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collapsed borders:&lt;/strong&gt; a cell's &lt;code&gt;borderStyle&lt;/code&gt; applies in a table with &lt;code&gt;borderCollapse: "collapse"&lt;/code&gt;; dashed and dotted borders were drawn solid. &lt;code&gt;Style.borderCollapse&lt;/code&gt; says how two
borders that meet are merged: the wider wins, then solid over dashed over dotted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pagination:&lt;/strong&gt; &lt;code&gt;Break.minPresenceAhead&lt;/code&gt; is described: it counts from the node's top and
includes its height. On a box's first in-flow child, it and &lt;code&gt;break.before: "page"&lt;/code&gt; (and a
&lt;code&gt;pageBreak&lt;/code&gt;) move the box too, so the box no longer leaves its empty top on the page before.
A table whose first row is taller than a page splits that row where the table starts, instead
of moving to a new page. &lt;code&gt;inset&lt;/code&gt; says that relative offsets count in pagination.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lengths:&lt;/strong&gt; a percentage height of a page child is of the page content height; it was
&lt;code&gt;auto&lt;/code&gt;. &lt;code&gt;Length&lt;/code&gt; and &lt;code&gt;LengthPercent&lt;/code&gt; say what percentages resolve against: gaps against the
box's own content box, as in CSS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text:&lt;/strong&gt; &lt;code&gt;whiteSpace&lt;/code&gt; collapses only spaces, tabs and line breaks, as in CSS; other space
characters keep their width (they became ordinary spaces). Tabs in &lt;code&gt;pre&lt;/code&gt; and &lt;code&gt;pre-wrap&lt;/code&gt;
advance to the next tab stop, 8 spaces of the font apart, instead of becoming four spaces.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;GridPlacement&lt;/code&gt;&lt;/strong&gt; with &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt; and &lt;code&gt;span&lt;/code&gt; places the item between &lt;code&gt;start&lt;/code&gt; and
&lt;code&gt;end&lt;/code&gt;, and ignores &lt;code&gt;span&lt;/code&gt;, as CSS does when both lines are set after a span. The engine used
&lt;code&gt;start&lt;/code&gt; and &lt;code&gt;span&lt;/code&gt; and ignored &lt;code&gt;end&lt;/code&gt;. The description also says that &lt;code&gt;span&lt;/code&gt; can go with &lt;code&gt;end&lt;/code&gt;
alone, which the engine already supported.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Page numbers in flow text&lt;/strong&gt; (engine 0.5.0): a &lt;code&gt;placeholder&lt;/code&gt; run in a page's flow is laid
out with the digits of the page it lands on, as in headers and footers, and the flow is
paginated again when those digits move it. Until then flow text kept the width of a
three-digit stand-in, so a box sized by its text was too wide for a short number and a number
of four digits or more could overflow. Text painted on several pages (a fixed node, a table's
header rows) keeps the stand-in width.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Documentation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Descriptions:&lt;/strong&gt; the schema describes rules the engine already applied. A radio button needs
a non-empty &lt;code&gt;value&lt;/code&gt; (&lt;code&gt;MISSING_FIELD&lt;/code&gt;); a text field's &lt;code&gt;value&lt;/code&gt; is at most &lt;code&gt;maxLength&lt;/code&gt;
characters; &lt;code&gt;gridArea&lt;/code&gt; cannot be combined with &lt;code&gt;gridRow&lt;/code&gt; or &lt;code&gt;gridColumn&lt;/code&gt;;
&lt;code&gt;gridTemplateAreas&lt;/code&gt; has at most 10000 cells; &lt;code&gt;transformOrigin&lt;/code&gt; does not apply to table cells;
a &lt;code&gt;#id&lt;/code&gt; link must name a displayed node in the flowing content, not one in a header or footer
or under &lt;code&gt;display: none&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;TransformOp&lt;/code&gt;:&lt;/strong&gt; the description said angles are within ±1000000; &lt;code&gt;skew&lt;/code&gt; angles are within
(−90, 90), as the schema's own bounds and the engine already required.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SCHEMA.md&lt;/strong&gt; states every bound of the schema: &lt;code&gt;maxItems&lt;/code&gt; and &lt;code&gt;exclusiveMaximum&lt;/code&gt; (an array of
a fixed length reads "exactly N items"), the bounds of array items and of &lt;code&gt;oneOf&lt;/code&gt; branches,
and every required key of an inline object (&lt;code&gt;TrackSize&lt;/code&gt; is &lt;code&gt;{min, max}&lt;/code&gt;, not &lt;code&gt;{min: …}&lt;/code&gt;). Its
table of validation codes is checked against the engine's, and &lt;code&gt;PARSE_ERROR&lt;/code&gt; covers
MessagePack too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Types:&lt;/strong&gt; a &lt;code&gt;TransformOp&lt;/code&gt;, &lt;code&gt;TrackSize&lt;/code&gt; or &lt;code&gt;TemplateTrack&lt;/code&gt; object forbids the keys of the
other forms (&lt;code&gt;rotate?: never&lt;/code&gt; beside &lt;code&gt;translate&lt;/code&gt;), so &lt;code&gt;{ translate: [1, 2], rotate: 5 }&lt;/code&gt; no
longer type-checks; validation always rejected it.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.5.0 - 2026-10-12</title>
    <link href="https://inkoxide.dev/changelog#050---2026-10-12"/>
    <id>https://inkoxide.dev/changelog#050---2026-10-12</id>
    <updated>2026-10-12T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/style&lt;/code&gt;, the style values that &lt;code&gt;@inkoxide/react&lt;/code&gt; and &lt;code&gt;@inkoxide/tailwind&lt;/code&gt; share: the
readers of CSS lengths, colours, &lt;code&gt;flex&lt;/code&gt;, transforms, transform origins and object positions,
and the style object type. They moved out of &lt;code&gt;@inkoxide/tailwind&lt;/code&gt;, which still exports the same
names.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Page numbers in the page body (&lt;code&gt;&amp;#x3C;PageNumber /&gt;&lt;/code&gt; and &lt;code&gt;&amp;#x3C;TotalPages /&gt;&lt;/code&gt; in a &lt;code&gt;Text&lt;/code&gt;) are laid out
with their own digits, as in headers and footers. The text wraps, justifies and sizes its box
at the width of the numbers of the page it lands on, and the page breaks are worked out again
when those numbers move it. Until now the body kept room for a three-digit number: a box
sized by its text was too wide for a short number, and a number of four digits or more could
overflow it. Text the body repeats on several pages (a table's header rows) still keeps that
room, since each page draws its own digits in it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind is optional.&lt;/strong&gt; &lt;code&gt;@inkoxide/render&lt;/code&gt; and &lt;code&gt;@inkoxide/react&lt;/code&gt; no longer install
&lt;code&gt;@inkoxide/tailwind&lt;/code&gt;: they load it when it is installed, for &lt;code&gt;className&lt;/code&gt;. Templates styled with
&lt;code&gt;style&lt;/code&gt; alone need nothing more, and the install is about 10 MB smaller (no Tailwind compiler,
Lightning CSS or PostCSS). &lt;strong&gt;Upgrading:&lt;/strong&gt; if you use &lt;code&gt;className&lt;/code&gt;, run
&lt;code&gt;npm install @inkoxide/tailwind&lt;/code&gt;; without it, a &lt;code&gt;className&lt;/code&gt; fails with a template error that
says so. &lt;code&gt;renderToTree&lt;/code&gt; in &lt;code&gt;@inkoxide/react&lt;/code&gt; loads Tailwind itself, as &lt;code&gt;@inkoxide/render&lt;/code&gt;
does; before the synchronous &lt;code&gt;renderTemplate&lt;/code&gt;, call the new &lt;code&gt;loadTailwind()&lt;/code&gt; once.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.4.1 - 2026-10-12</title>
    <link href="https://inkoxide.dev/changelog#041---2026-10-12"/>
    <id>https://inkoxide.dev/changelog#041---2026-10-12</id>
    <updated>2026-10-12T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/render/templates&lt;/code&gt;, a second entry of &lt;code&gt;@inkoxide/render&lt;/code&gt; for serving templates by
id with a zod schema for their data: &lt;code&gt;generate&lt;/code&gt;, &lt;code&gt;parseData&lt;/code&gt;, &lt;code&gt;renderTree&lt;/code&gt;, &lt;code&gt;renderSettled&lt;/code&gt;,
&lt;code&gt;renderDocument&lt;/code&gt; and the types &lt;code&gt;TemplateLike&lt;/code&gt;, &lt;code&gt;GenerateOptions&lt;/code&gt;, &lt;code&gt;RenderOpts&lt;/code&gt;,
&lt;code&gt;RenderResult&lt;/code&gt;, &lt;code&gt;Timings&lt;/code&gt; and &lt;code&gt;TreeResult&lt;/code&gt;. The guide documents it under
&lt;a href="https://inkoxide.dev/docs/react/serving-templates"&gt;Serving templates&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@inkoxide/render&lt;/code&gt; guide documents the rest of what the package exports: &lt;code&gt;usePage&lt;/code&gt;,
&lt;code&gt;Field&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;engineStatus&lt;/code&gt; and &lt;code&gt;countPages&lt;/code&gt; (health checks), the &lt;code&gt;RenderError&lt;/code&gt; fields
&lt;code&gt;className&lt;/code&gt;, &lt;code&gt;engineCode&lt;/code&gt; and &lt;code&gt;issues&lt;/code&gt; and its JSON form, the exported types, the &lt;code&gt;Document&lt;/code&gt;
props &lt;code&gt;creator&lt;/code&gt;, &lt;code&gt;creationDate&lt;/code&gt; and &lt;code&gt;fallbackFonts&lt;/code&gt;, and &lt;code&gt;pages&lt;/code&gt; on elements of the page
body, with negative page numbers counting from the end.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/tailwind&lt;/code&gt; exports &lt;code&gt;UnsupportedValueError&lt;/code&gt;, which &lt;code&gt;parseTransform&lt;/code&gt;, &lt;code&gt;parseOrigin&lt;/code&gt; and
&lt;code&gt;parseObjectPosition&lt;/code&gt; throw for a value the engine cannot express.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/tailwind&lt;/code&gt; exports the CSS value helpers that &lt;code&gt;@inkoxide/react&lt;/code&gt; shares with it:
&lt;code&gt;PT_PER_UNIT&lt;/code&gt;, &lt;code&gt;parseFlex&lt;/code&gt;, &lt;code&gt;round&lt;/code&gt;, &lt;code&gt;splitTopLevel&lt;/code&gt;, &lt;code&gt;BORDER_STYLES&lt;/code&gt; and &lt;code&gt;OBJECT_FITS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/react&lt;/code&gt; exports &lt;code&gt;imageSource&lt;/code&gt; and its &lt;code&gt;ImageSource&lt;/code&gt; type, &lt;code&gt;remoteRefused&lt;/code&gt; and
&lt;code&gt;innermostComponent&lt;/code&gt;, which &lt;code&gt;@inkoxide/render&lt;/code&gt; uses, and takes the tree option
&lt;code&gt;remoteImagesElsewhere&lt;/code&gt;, another place the caller reads the remote allowlist from, which the
error for a refused URL names. &lt;code&gt;HOST_NAME&lt;/code&gt; is typed by host type
(&lt;code&gt;Record&amp;#x3C;HostType, string&gt;&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@inkoxide/render&lt;/code&gt; exports only what its guide documents&lt;/strong&gt;, a list now pinned by a test.
This breaks only code that used undocumented names:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;generate&lt;/code&gt;, &lt;code&gt;parseData&lt;/code&gt;, &lt;code&gt;renderTree&lt;/code&gt;, &lt;code&gt;renderSettled&lt;/code&gt;, &lt;code&gt;renderDocument&lt;/code&gt;, &lt;code&gt;TemplateLike&lt;/code&gt;,
&lt;code&gt;GenerateOptions&lt;/code&gt;, &lt;code&gt;RenderOpts&lt;/code&gt;, &lt;code&gt;RenderResult&lt;/code&gt; and &lt;code&gt;Timings&lt;/code&gt; moved to
&lt;code&gt;@inkoxide/render/templates&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;renderTemplate&lt;/code&gt;, &lt;code&gt;renderToTree&lt;/code&gt;, &lt;code&gt;assertValidTree&lt;/code&gt;, &lt;code&gt;validateTree&lt;/code&gt;, &lt;code&gt;SchemaProblem&lt;/code&gt;,
&lt;code&gt;TemplateError&lt;/code&gt;, &lt;code&gt;TemplateErrorKind&lt;/code&gt;, &lt;code&gt;fonts&lt;/code&gt;, &lt;code&gt;FontRegistry&lt;/code&gt;, &lt;code&gt;FontFace&lt;/code&gt;, &lt;code&gt;ResolvedFont&lt;/code&gt;,
&lt;code&gt;HOST&lt;/code&gt;, &lt;code&gt;HOST_NAME&lt;/code&gt;, &lt;code&gt;HostType&lt;/code&gt;, &lt;code&gt;isImagePath&lt;/code&gt;, &lt;code&gt;remoteAllowed&lt;/code&gt;, &lt;code&gt;RemoteImagePolicy&lt;/code&gt;,
&lt;code&gt;PendingImage&lt;/code&gt;, &lt;code&gt;parsePath&lt;/code&gt;, &lt;code&gt;fontWeightValue&lt;/code&gt;, &lt;code&gt;TreeOptions&lt;/code&gt;, &lt;code&gt;TreeResult&lt;/code&gt; and &lt;code&gt;Located&lt;/code&gt;
are no longer re-exported; import them from &lt;code&gt;@inkoxide/react&lt;/code&gt; (&lt;code&gt;TreeResult&lt;/code&gt; is also in
&lt;code&gt;@inkoxide/render/templates&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCHEMA_VERSION&lt;/code&gt; is no longer re-exported; import it from &lt;code&gt;@inkoxide/schema&lt;/code&gt;, or read
&lt;code&gt;version().schema&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initEngine&lt;/code&gt;, &lt;code&gt;assetsDir&lt;/code&gt;, &lt;code&gt;DEFAULT_FACES&lt;/code&gt; and &lt;code&gt;RenderErrorInit&lt;/code&gt; are no longer exported:
&lt;code&gt;init()&lt;/code&gt; starts the engine, and &lt;code&gt;INKOXIDE_ASSETS_DIR&lt;/code&gt; still sets where the bundled fonts
are read from.&lt;/li&gt;
&lt;li&gt;The deprecated aliases &lt;code&gt;StudioError&lt;/code&gt; and &lt;code&gt;StudioErrorCode&lt;/code&gt; are removed; use &lt;code&gt;RenderError&lt;/code&gt;
and &lt;code&gt;RenderErrorCode&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;flex&lt;/code&gt; shorthand reads the same in &lt;code&gt;style&lt;/code&gt; and in Tailwind classes:
&lt;code&gt;style={{ flex: '1 auto' }}&lt;/code&gt; sets grow and basis, as &lt;code&gt;flex-[1_auto]&lt;/code&gt; does, and
&lt;code&gt;flex-[initial]&lt;/code&gt; works as &lt;code&gt;flex: 'initial'&lt;/code&gt; does. A lone grow in a class is rounded to six
decimals like every other value.
An empty &lt;code&gt;flex&lt;/code&gt; (&lt;code&gt;''&lt;/code&gt;) is an error instead of a grow of 0, and a negative grow or shrink
(&lt;code&gt;flex-[-1]&lt;/code&gt;) is an unsupported value where the class or style is read, not a schema error
later.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;SVG style attributes and sheets are held to a limit on what the CSS parser does with every
unitless number and &lt;code&gt;#&lt;/code&gt; as well as every declaration: it scans the text from its start each
time, so a style with a long &lt;code&gt;stroke-dasharray&lt;/code&gt; used seconds to minutes of CPU. Found by a
new structure-aware SVG fuzz target (&lt;code&gt;render_svg_tree&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A table with &lt;code&gt;break: { fixed: true }&lt;/code&gt; in the flow repeats whole on every page: its row and
cell backgrounds and borders were painted only on its first page, and the later pages showed
only the cells' content.&lt;/li&gt;
&lt;li&gt;A table with &lt;code&gt;headerRows&lt;/code&gt; that starts at the top of a page no longer paints its header twice
on that page (a translucent header background came out darker, and text extraction read the
header twice).&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.4.0 - 2026-10-12</title>
    <link href="https://inkoxide.dev/changelog#040---2026-10-12"/>
    <id>https://inkoxide.dev/changelog#040---2026-10-12</id>
    <updated>2026-10-12T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;SVG images are held to their limits as the SVG converter expands them: nested SVG images and
every copy of them, markers at every vertex of every shape, references however they are
written, style sheet rules and the declarations they copy, inherited attributes and embedded
raster pixels all count. Entities that expand to more than 1 MiB, &lt;code&gt;href&lt;/code&gt; chains that loop and
radii or path coordinates beyond 10⁷ are refused. A small SVG could otherwise use minutes of
CPU and gigabytes of memory. Each raster image in an SVG is embedded in the PDF once.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;threads: 1&lt;/code&gt; runs the whole render on the calling thread (in Node, the libuv thread that
runs it), as documented. Every one-thread render used to queue for the same worker thread,
so concurrent renders ran one at a time.&lt;/li&gt;
&lt;li&gt;Every failure of &lt;code&gt;renderToBuffer&lt;/code&gt;, &lt;code&gt;renderToStream&lt;/code&gt;, &lt;code&gt;renderToFile&lt;/code&gt; and &lt;code&gt;init&lt;/code&gt; is a
&lt;code&gt;RenderError&lt;/code&gt;. An engine that cannot start and a PDF that &lt;code&gt;renderToFile&lt;/code&gt; cannot write are
&lt;code&gt;INTERNAL&lt;/code&gt;; a &lt;code&gt;render&lt;/code&gt; prop that throws on a later pass is &lt;code&gt;TEMPLATE_ERROR&lt;/code&gt;. The guide's error
table lists &lt;code&gt;INTERNAL&lt;/code&gt;, and gives &lt;code&gt;TEMPLATE_ERROR&lt;/code&gt; as the code of an invalid prop.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Font.register&lt;/code&gt; reads a &lt;code&gt;file:&lt;/code&gt; URL given as a string (&lt;code&gt;'file:///fonts/Lora.ttf'&lt;/code&gt;), not only
as a &lt;code&gt;URL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A font from &lt;code&gt;Font.register&lt;/code&gt; that cannot be loaded no longer keeps the fonts registered with
it from loading. The render still fails with &lt;code&gt;INVALID_FONT&lt;/code&gt;, and the next one can use them.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inkoxide&lt;/code&gt; no longer exports its &lt;code&gt;__testPanic&lt;/code&gt; test hook, which was never public API.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;Svg fixed&gt;&lt;/code&gt; and &lt;code&gt;&amp;#x3C;Svg debug&gt;&lt;/code&gt; no longer write &lt;code&gt;fixed&lt;/code&gt; and &lt;code&gt;debug&lt;/code&gt; attributes into the
drawing.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@inkoxide/react&lt;/code&gt; renderer gives React its real version, from its &lt;code&gt;package.json&lt;/code&gt;, instead
of &lt;code&gt;0.1.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An image over the 50-megapixel limit fails validation with &lt;code&gt;IMAGE_TOO_LARGE&lt;/code&gt;, as documented.
It was reported as &lt;code&gt;INVALID_IMAGE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A document with &lt;code&gt;pages: []&lt;/code&gt; fails validation with &lt;code&gt;INVALID_VALUE&lt;/code&gt; at &lt;code&gt;pages&lt;/code&gt;, as the schema
requires at least one page. It rendered a PDF with no pages, which viewers reject.&lt;/li&gt;
&lt;li&gt;Validation enforces the remaining bounds of the schema: an id (of a node, row, cell, font or
image, or a font named by a run, a field or &lt;code&gt;fallbackFonts&lt;/code&gt;) is 1 to 256 characters and a
font &lt;code&gt;family&lt;/code&gt; is not empty (&lt;code&gt;INVALID_VALUE&lt;/code&gt;), and a grid &lt;code&gt;span&lt;/code&gt; is at least 1
(&lt;code&gt;OUT_OF_RANGE&lt;/code&gt;; &lt;code&gt;span: 0&lt;/code&gt; was laid out as 1). The schema states the engine's bounds of grid
lines: within ±32767 and not 0, and a &lt;code&gt;span&lt;/code&gt; of at most 65535.&lt;/li&gt;
&lt;li&gt;The documentation states that &lt;code&gt;validate&lt;/code&gt; returns at most 100 problems, and only the first
when the document's structure cannot be read. The schema, &lt;code&gt;SCHEMA.md&lt;/code&gt; and &lt;code&gt;API.md&lt;/code&gt; describe
the rules the engine applies beyond the schema's shapes: &lt;code&gt;break&lt;/code&gt; and &lt;code&gt;pageBreak&lt;/code&gt; in headers,
footers and table cells, margins that leave no room for content, and repeated form field
names.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hyphenation patterns are registered for the whole language tag. Patterns for &lt;code&gt;en-GB&lt;/code&gt; no longer
replace those for &lt;code&gt;en-US&lt;/code&gt;; text uses the patterns of its own tag, else of the nearest less
specific one (&lt;code&gt;en-GB&lt;/code&gt;, then &lt;code&gt;en&lt;/code&gt;), else the built-in ones. Three-letter languages such as
&lt;code&gt;haw&lt;/code&gt; or &lt;code&gt;fil&lt;/code&gt; can be registered. &lt;strong&gt;Patterns registered for a region now serve only that
region:&lt;/strong&gt; register for &lt;code&gt;en&lt;/code&gt; (not &lt;code&gt;en-US&lt;/code&gt;) to cover every English &lt;code&gt;lang&lt;/code&gt;. &lt;code&gt;@inkoxide/render&lt;/code&gt;
registers its US English patterns for &lt;code&gt;en&lt;/code&gt;, so they still serve every English &lt;code&gt;lang&lt;/code&gt;;
&lt;code&gt;engineStatus().hyphenation&lt;/code&gt; lists them as &lt;code&gt;en&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Removed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inkoxide::debug_raw_parse_error&lt;/code&gt;, a hidden test hook that was never part of the API. The
schema-parity check that used it is now a unit test inside the crate.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Repository&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://inkoxide.dev/docs/architecture"&gt;Architecture&lt;/a&gt; lists every public item of the crate, and places
parsing in &lt;code&gt;validate&lt;/code&gt;, where it lives.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.3.0 - 2026-10-12</title>
    <link href="https://inkoxide.dev/changelog#030---2026-10-12"/>
    <id>https://inkoxide.dev/changelog#030---2026-10-12</id>
    <updated>2026-10-12T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;#x3C;Link&gt;&lt;/code&gt;&lt;/strong&gt;: a link as a component, next to &lt;code&gt;href&lt;/code&gt; on &lt;code&gt;Text&lt;/code&gt; and &lt;code&gt;View&lt;/code&gt;. Inside a &lt;code&gt;Text&lt;/code&gt; it
is a linked run, elsewhere a box. &lt;code&gt;src&lt;/code&gt; is an alias of &lt;code&gt;href&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Page sizes&lt;/strong&gt;: the ISO A, B and C series, RA, SRA, Folio and ID-1, matched in any case;
&lt;code&gt;size={[width, height]}&lt;/code&gt;, &lt;code&gt;size={[side]}&lt;/code&gt; and a single length for a square page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Image sources&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Files, read only from folders you allow with the &lt;code&gt;localImages&lt;/code&gt; render option or
&lt;code&gt;INKOXIDE_LOCAL_IMAGES&lt;/code&gt;. Paths cannot leave those folders, through &lt;code&gt;..&lt;/code&gt; or symbolic
links, and files are capped at 20 MiB by default.&lt;/li&gt;
&lt;li&gt;Requests with a method, headers and a body: &lt;code&gt;{ uri, method, headers, body }&lt;/code&gt;, under the
same allowlist and limits as URL strings.&lt;/li&gt;
&lt;li&gt;Functions and promises that resolve to any source, with a timeout (&lt;code&gt;imageTimeoutMs&lt;/code&gt;,
10 s by default).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ArrayBuffer&lt;/code&gt; bytes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Style keys&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;objectFit&lt;/code&gt; on images, and &lt;code&gt;textOverflow: 'ellipsis'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;textDecoration: 'line-through'&lt;/code&gt; and &lt;code&gt;'underline line-through'&lt;/code&gt;, which draws the
line-through, plus &lt;code&gt;textDecorationColor&lt;/code&gt;, &lt;code&gt;textDecorationStyle&lt;/code&gt; and
&lt;code&gt;textDecorationThickness&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The shorthands &lt;code&gt;border&lt;/code&gt;, &lt;code&gt;borderTop&lt;/code&gt;, &lt;code&gt;borderRight&lt;/code&gt;, &lt;code&gt;borderBottom&lt;/code&gt; and &lt;code&gt;borderLeft&lt;/code&gt;,
e.g. &lt;code&gt;'1px solid #e5e7eb'&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Units&lt;/strong&gt; in style objects: &lt;code&gt;pc&lt;/code&gt;, &lt;code&gt;rem&lt;/code&gt;, &lt;code&gt;em&lt;/code&gt;, &lt;code&gt;vw&lt;/code&gt; and &lt;code&gt;vh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fonts&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;WOFF and WOFF2 files, in &lt;code&gt;Font.register&lt;/code&gt;, &lt;code&gt;registerFont&lt;/code&gt; and the CLI &lt;code&gt;--fonts&lt;/code&gt; folders.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Font.registerHyphenationCallback&lt;/code&gt; and a &lt;code&gt;hyphenationCallback&lt;/code&gt; prop on &lt;code&gt;Text&lt;/code&gt;, to split
words with your own function.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;renderToStream&lt;/code&gt;&lt;/strong&gt;: the PDF as a Node &lt;code&gt;Readable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fixed&lt;/code&gt;&lt;/strong&gt;: an element placed directly in a &lt;code&gt;Page&lt;/code&gt; with &lt;code&gt;position: 'absolute'&lt;/code&gt; repeats on
every page, placed against the page's content box, for watermarks, borders and page
numbers. A negative &lt;code&gt;zIndex&lt;/code&gt; puts it under the content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;objectPosition&lt;/code&gt;&lt;/strong&gt; on images, in style objects and as Tailwind &lt;code&gt;object-*&lt;/code&gt; classes, places
a fitted image in its box: keywords, percentages or lengths. &lt;code&gt;objectFit&lt;/code&gt; also has its
Tailwind classes now.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;debug&lt;/code&gt;&lt;/strong&gt;: outlines the margin, border, padding and content boxes of an element and labels
its size; on a &lt;code&gt;Page&lt;/code&gt; or the &lt;code&gt;Document&lt;/code&gt;, of every element. Labels are drawn, not text, so
copying text from the PDF is unaffected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@inkoxide/schema&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;hyphenate&lt;/code&gt; on a text run, overriding its text node's setting for
the run's words; &lt;code&gt;objectPosition&lt;/code&gt; on images; &lt;code&gt;debug&lt;/code&gt; on nodes, rows, cells and pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Changed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;An image &lt;code&gt;src&lt;/code&gt; string that names an id you pass in &lt;code&gt;images&lt;/code&gt; is always that image, even when
it looks like a URL. Such strings used to be fetched.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Page&lt;/code&gt; &lt;code&gt;size&lt;/code&gt; strings are checked in full: &lt;code&gt;"A4-landscape-x"&lt;/code&gt; is an error instead of &lt;code&gt;A4&lt;/code&gt;
landscape.&lt;/li&gt;
&lt;li&gt;Every length, percentage and factor in a document has a range (percentages within ±10000%,
points within ±1000000), and a value outside it is &lt;code&gt;OUT_OF_RANGE&lt;/code&gt;. In React, an infinite or
&lt;code&gt;NaN&lt;/code&gt; style value is an error instead of being dropped.&lt;/li&gt;
&lt;li&gt;A fixed, absolutely positioned child of a page in a JSON document is placed against the
page's content box on every page. Its insets used to be resolved against the whole flow, so
&lt;code&gt;bottom&lt;/code&gt; and percentages put it in the wrong place.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Page numbers in headers and footers are laid out with their real digits, so centred and
right-aligned numbers line up, and documents of 1,000 pages or more no longer overflow the
space.&lt;/li&gt;
&lt;li&gt;A panic while registering a font or hyphenation patterns is reported as an &lt;code&gt;INTERNAL&lt;/code&gt; error
instead of ending the process.&lt;/li&gt;
&lt;li&gt;Huge percentages, for example in &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;margin&lt;/code&gt; or a &lt;code&gt;translate&lt;/code&gt;, no longer write &lt;code&gt;inf&lt;/code&gt; or
&lt;code&gt;NaN&lt;/code&gt; into the PDF, which made it invalid. The PDF writer also never writes a non-finite
number, from layout or from an SVG image.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>@inkoxide/schema 2.2.0 - 2026-10-12</title>
    <link href="https://inkoxide.dev/changelog#schema-220---2026-10-12"/>
    <id>https://inkoxide.dev/changelog#schema-220---2026-10-12</id>
    <updated>2026-10-12T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Engine parity:&lt;/strong&gt; the engine enforces every bound the schema states. An &lt;code&gt;Id&lt;/code&gt; of 0 or more
than 256 characters and an empty &lt;code&gt;FontDecl.family&lt;/code&gt; are &lt;code&gt;INVALID_VALUE&lt;/code&gt;, and a
&lt;code&gt;GridPlacement.span&lt;/code&gt; of 0 is &lt;code&gt;OUT_OF_RANGE&lt;/code&gt;; the engine accepted them, and laid out
&lt;code&gt;span: 0&lt;/code&gt; as 1. The schema states the engine's existing bounds of &lt;code&gt;GridPlacement&lt;/code&gt;: &lt;code&gt;start&lt;/code&gt;
and &lt;code&gt;end&lt;/code&gt; are within ±32767 and not 0, and &lt;code&gt;span&lt;/code&gt; is at most 65535.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Documentation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Descriptions:&lt;/strong&gt; the schema describes rules the engine already applied. &lt;code&gt;break&lt;/code&gt; with a value
other than its defaults is &lt;code&gt;INVALID_VALUE&lt;/code&gt; on a header or footer, on a node in a table cell
and on any node inside these, and &lt;code&gt;pageBreak&lt;/code&gt; is &lt;code&gt;INVALID_NODE&lt;/code&gt; there; page margins that
leave no room for content are &lt;code&gt;OUT_OF_RANGE&lt;/code&gt; at &lt;code&gt;margin&lt;/code&gt;; a form field &lt;code&gt;name&lt;/code&gt; used twice is
&lt;code&gt;DUPLICATE_ID&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>@inkoxide/schema 2.1.0 - 2026-10-12</title>
    <link href="https://inkoxide.dev/changelog#schema-210---2026-10-12"/>
    <id>https://inkoxide.dev/changelog#schema-210---2026-10-12</id>
    <updated>2026-10-12T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Added&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; &lt;code&gt;debug&lt;/code&gt; on nodes (all but &lt;code&gt;pageBreak&lt;/code&gt;), table rows and cells, and pages draws
the element's margin, border, padding and content boxes and its size over the page; on a
page, for the page and every element on it. It changes the output and is off by default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text runs:&lt;/strong&gt; &lt;code&gt;hyphenate&lt;/code&gt; on a run overrides the text node's &lt;code&gt;hyphenate&lt;/code&gt; for the run's
words, in the node's language.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Images:&lt;/strong&gt; &lt;code&gt;objectPosition&lt;/code&gt; places an image with &lt;code&gt;fit&lt;/code&gt; &lt;code&gt;contain&lt;/code&gt;, &lt;code&gt;cover&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt; in its
box, like CSS &lt;code&gt;object-position&lt;/code&gt;: &lt;code&gt;{x, y}&lt;/code&gt;, each a keyword (&lt;code&gt;left&lt;/code&gt;/&lt;code&gt;center&lt;/code&gt;/&lt;code&gt;right&lt;/code&gt;,
&lt;code&gt;top&lt;/code&gt;/&lt;code&gt;center&lt;/code&gt;/&lt;code&gt;bottom&lt;/code&gt;), a percentage (within ±10000%) or points (within ±14400). The
default is centred, as before.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fixed&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fixed nodes:&lt;/strong&gt; a fixed node (&lt;code&gt;break.fixed&lt;/code&gt;) with &lt;code&gt;position: "absolute"&lt;/code&gt; that is a child of
the page is placed against the page content box, at the same place on every page, and takes
no space. Before, its &lt;code&gt;inset&lt;/code&gt; and percentages were resolved against the whole flow, several
pages tall, so a &lt;code&gt;bottom&lt;/code&gt; inset or a node after content that moved to a later page landed
elsewhere, and a node below the content could add a page. One placed on the first page with
&lt;code&gt;top&lt;/code&gt;, &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt; in points, before any content moves to another page, renders as
before. &lt;code&gt;fixed&lt;/code&gt; now has a description in the schema. Such a node with a negative &lt;code&gt;zIndex&lt;/code&gt; is
painted under the flow, the others over it. Its own &lt;code&gt;break&lt;/code&gt; may hold only &lt;code&gt;fixed&lt;/code&gt;, and inside
it &lt;code&gt;break&lt;/code&gt; and &lt;code&gt;pageBreak&lt;/code&gt; are refused, as they did nothing there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Number bounds:&lt;/strong&gt; every length and the other unbounded numbers have a range, so layout can
no longer overflow and write &lt;code&gt;inf&lt;/code&gt; or &lt;code&gt;NaN&lt;/code&gt; into the PDF, which made it invalid. A percentage
like &lt;code&gt;"9999999999999999999999999999999999999%"&lt;/code&gt; in &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;margin&lt;/code&gt; or a &lt;code&gt;translate&lt;/code&gt; was
accepted. Percentages are now within ±10000% everywhere; points (lengths, column widths, grid
tracks, border widths and radii, &lt;code&gt;borderSpacing&lt;/code&gt;, &lt;code&gt;minPresenceAhead&lt;/code&gt;, row &lt;code&gt;minHeight&lt;/code&gt;), &lt;code&gt;fr&lt;/code&gt;,
&lt;code&gt;flexGrow&lt;/code&gt;, &lt;code&gt;flexShrink&lt;/code&gt;, &lt;code&gt;rotate&lt;/code&gt; and the scale, rotate and matrix values of &lt;code&gt;transform&lt;/code&gt; are
within ±1000000 (non-negative where they were); &lt;code&gt;aspectRatio&lt;/code&gt; is within [0.000001, 1000000].
A value past these is &lt;code&gt;OUT_OF_RANGE&lt;/code&gt; at its path; errors on grid tracks and skew angles now
point at the value (&lt;code&gt;gridTemplateRows[0].min&lt;/code&gt;, &lt;code&gt;transform[0].skew[0]&lt;/code&gt;). The schema also
states the engine's existing bounds of &lt;code&gt;lineHeight&lt;/code&gt; (at most 100), &lt;code&gt;letterSpacing&lt;/code&gt; (±1000)
and &lt;code&gt;textIndent&lt;/code&gt; (±10000).&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.2.0 - 2026-10-11</title>
    <link href="https://inkoxide.dev/changelog#020---2026-10-11"/>
    <id>https://inkoxide.dev/changelog#020---2026-10-11</id>
    <updated>2026-10-11T00:00:00Z</updated>
    <content type="html">&lt;h3&gt;Breaking: ES modules only&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;inkoxide&lt;/code&gt; 0.2.0 and &lt;code&gt;@inkoxide/schema&lt;/code&gt; 2.0.0 are ES modules and need Node.js 22 or later, like
the React packages already did. Replace &lt;code&gt;require('inkoxide')&lt;/code&gt; with &lt;code&gt;import … from 'inkoxide'&lt;/code&gt;.
&lt;code&gt;@inkoxide/schema&lt;/code&gt; 2.0.0 still describes document schema version 1; documents do not change.&lt;/p&gt;
&lt;h3&gt;Dependency updates&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@inkoxide/tailwind&lt;/code&gt; 0.1.1, &lt;code&gt;@inkoxide/react&lt;/code&gt; 0.1.1 and &lt;code&gt;@inkoxide/render&lt;/code&gt; 0.1.2 depend on the
releases above. The &lt;code&gt;@inkoxide/render&lt;/code&gt; README now shows the project setup the quick start needs.&lt;/p&gt;
&lt;h3&gt;Repository&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/tailwind&lt;/code&gt;, &lt;code&gt;@inkoxide/react&lt;/code&gt; and &lt;code&gt;@inkoxide/render&lt;/code&gt; are developed in this
repository, under &lt;code&gt;packages/&lt;/code&gt;, with the examples and the benchmarks.&lt;/li&gt;
&lt;li&gt;Continuous integration on every pull request.&lt;/li&gt;
&lt;li&gt;Every package is published by the &lt;code&gt;release&lt;/code&gt; workflow, with npm provenance.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>@inkoxide/render 0.1.1 - 2026-10-11</title>
    <link href="https://inkoxide.dev/changelog#inkoxiderender-011---2026-10-11"/>
    <id>https://inkoxide.dev/changelog#inkoxiderender-011---2026-10-11</id>
    <updated>2026-10-11T00:00:00Z</updated>
    <content type="html">&lt;ul&gt;
&lt;li&gt;A new README with examples, and two new guides: &lt;a href="https://inkoxide.dev/docs/react"&gt;the React guide&lt;/a&gt; and
MIGRATING.md.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INKOXIDE_ASSETS_DIR&lt;/code&gt; replaces &lt;code&gt;STUDIO_ASSETS_DIR&lt;/code&gt; for loading the bundled fonts from another
directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StudioError&lt;/code&gt; and &lt;code&gt;StudioErrorCode&lt;/code&gt; are deprecated aliases of &lt;code&gt;RenderError&lt;/code&gt; and
&lt;code&gt;RenderErrorCode&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>0.1.0 - 2026-10-11</title>
    <link href="https://inkoxide.dev/changelog#010---2026-10-11"/>
    <id>https://inkoxide.dev/changelog#010---2026-10-11</id>
    <updated>2026-10-11T00:00:00Z</updated>
    <content type="html">&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inkoxide&lt;/code&gt; 0.1.0: the engine as a Node package, with prebuilt binaries for macOS (arm64, x64),
Linux (x64 glibc and musl, arm64 glibc) and Windows (x64).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/schema&lt;/code&gt; 1.0.0: TypeScript types and the JSON Schema of document schema version 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@inkoxide/tailwind&lt;/code&gt; 0.1.0, &lt;code&gt;@inkoxide/react&lt;/code&gt; 0.1.0 and &lt;code&gt;@inkoxide/render&lt;/code&gt; 0.1.0: React
components to PDF, styled with Tailwind classes or style objects.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>@inkoxide/schema 1.0.0 - 2026-10-11</title>
    <link href="https://inkoxide.dev/changelog#schema-100---2026-10-11"/>
    <id>https://inkoxide.dev/changelog#schema-100---2026-10-11</id>
    <updated>2026-10-11T00:00:00Z</updated>
    <content type="html">&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nodes:&lt;/strong&gt; &lt;code&gt;box&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt; (with styled runs), &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;table&lt;/code&gt;, &lt;code&gt;field&lt;/code&gt; (text, checkbox, radio
and select form fields), &lt;code&gt;pageBreak&lt;/code&gt; and &lt;code&gt;spacer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pages:&lt;/strong&gt; page templates with size, margin, header and footer; &lt;code&gt;pageNumber&lt;/code&gt; and
&lt;code&gt;totalPages&lt;/code&gt; placeholders in text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layout:&lt;/strong&gt; flexbox and CSS grid (track sizing with &lt;code&gt;minmax&lt;/code&gt;, &lt;code&gt;fitContent&lt;/code&gt;, &lt;code&gt;repeat&lt;/code&gt; with
&lt;code&gt;auto-fill&lt;/code&gt; and &lt;code&gt;auto-fit&lt;/code&gt;, named areas), absolute positioning, &lt;code&gt;zIndex&lt;/code&gt;, transforms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual styles:&lt;/strong&gt; backgrounds, borders per side with per-side styles, per-corner radii,
opacity, clipping with &lt;code&gt;overflow&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text:&lt;/strong&gt; font, size, weight and style per run; alignment including &lt;code&gt;justify&lt;/code&gt;, &lt;code&gt;start&lt;/code&gt; and
&lt;code&gt;end&lt;/code&gt;; hyphenation; &lt;code&gt;whiteSpace&lt;/code&gt;, &lt;code&gt;textIndent&lt;/code&gt;, &lt;code&gt;maxLines&lt;/code&gt; with ellipsis; bidirectional text
with &lt;code&gt;direction&lt;/code&gt;; underline and strike-through with colour, thickness and style.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tables:&lt;/strong&gt; column widths, &lt;code&gt;colSpan&lt;/code&gt; and &lt;code&gt;rowSpan&lt;/code&gt;, repeating header rows, collapsed or
separate borders with &lt;code&gt;borderSpacing&lt;/code&gt;; rows split across pages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pagination:&lt;/strong&gt; &lt;code&gt;break&lt;/code&gt; (before, after, inside, &lt;code&gt;minPresenceAhead&lt;/code&gt;), &lt;code&gt;orphans&lt;/code&gt; and &lt;code&gt;widows&lt;/code&gt;,
and &lt;code&gt;pages&lt;/code&gt; to show a node on some pages only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Links and outline:&lt;/strong&gt; &lt;code&gt;link&lt;/code&gt; (&lt;code&gt;#id&lt;/code&gt; or a URI) on nodes and runs, and &lt;code&gt;bookmark&lt;/code&gt; entries
that build the document outline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Images:&lt;/strong&gt; JPEG, PNG, GIF (first frame), WebP and SVG, by id or as a &lt;code&gt;data:&lt;/code&gt; URL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation:&lt;/strong&gt; the JSON Schema carries &lt;code&gt;discriminator: { propertyName: "type" }&lt;/code&gt; on &lt;code&gt;Node&lt;/code&gt;,
so ajv reports errors for the matching node type only, with the engine's error paths.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry>
</feed>
