# Changelog

> Every change to PdfNetStack by release, newest first: what is in main and not yet on NuGet, and what each published version added.

> **Synced from the repository:** This page is generated from [CHANGELOG.md](https://github.com/jams4code/pdf.netstack/blob/main/CHANGELOG.md) at every build.
## 1.2.0 (2026-11-10)

- `PdfOptimizerOptions.MaxBilevelImageDpi` (`--max-bilevel-dpi`) gives bilevel images (raw, CCITT,
  JBIG2) their own downsampling resolution, in place of `MaxImageDpi`. Null keeps the old behavior.
  `Smallest` now sets it to 300: at 150 dpi one bit per pixel made scanned text ragged, which the
  image quality gate does not judge, since a bilevel reduction is exact by construction.
- OCR: ONNX Runtime 1.31.1. One build property now sets its version everywhere it is named.
- Hyphenation, off by default, by Liang's algorithm as TeX defines it. `PdfParagraph.Hyphenator` takes
  a `PdfHyphenator`: words then break at the line's end where the language allows, showing a hyphen,
  while `PdfParagraph.Text` keeps them whole and extracted text marks the hyphen with U+00AD.
  `PdfHyphenator.Create` takes patterns and exceptions in TeX's formats, `WithExceptions` adds words,
  and `MinWordLength`, `MinLeft`, `MinRight` and `HyphenateAllCaps` set the limits. `FindBreaks` and
  `InsertSoftHyphens` work on text of your own. Left alone: words with a soft hyphen, words joined
  to a digit or holding a capital past the first letter, words of all capitals by default, web and
  e-mail addresses and paths, words over 64 letters. A point inside a ligature is skipped, and fonts
  without a hyphen glyph do not hyphenate. Results do not depend on the current culture.
- `PdfNetStack.Hyphenation`, a new optional package with patterns from TeX's hyph-utf8 for `en-us`,
  `en-gb`, `de` (reformed spelling), `fr`, `nl`, `es`, `it`, `pt`, `af`, `be`, `bg`, `cy`, `da`,
  `el-monoton`, `el-polyton`, `et`, `eu`, `ga`, `gl`, `hr`, `la`, `lt`, `nb`, `nn`, `pl`, `sk`, `sl`,
  `sq` and `uk`, all under permissive licenses, loaded one language at a time on first use by
  `PdfHyphenationPatterns.ForLanguage`. Languages whose only patterns have other licenses (among
  them `ru`, `sv`, `cs`, `hu`, `tr`, `ca` and `fi`) are left out; `eng/check-licenses.ps1` now
  checks every set against the allowlist.
- `hyphens: auto` in `PdfNetStack.Html` hyphenates text in the language of the nearest `lang`
  attribute when `HtmlConverterOptions.Hyphenators` finds patterns for it, with
  `PdfHyphenationPatterns.ForLanguage` for instance. Without the option nothing changes, and
  `auto` behaves like `manual`. Min-content sizes count the points, so narrow table columns can
  hyphenate instead of overflowing. Words are judged as written: text under
  `text-transform: uppercase` hyphenates like its source words, while text written in capitals
  stays whole.
- `text-transform: uppercase` writes the German sharp s as SS, as CSS Text 3 asks, with or without
  hyphenation: `Straße` now reads `STRASSE`, where it read `STRAßE`. Other characters whose full
  upper case takes several letters keep their simple mapping.
- Fixed: justifying a line that ended at a soft hyphen could widen the hyphen along with the spaces,
  when the run the hyphen belonged to ended with a space.
- New package `PdfNetStack.Ocr.Tesseract`: `TesseractOcrEngine`, an `IOcrEngine` that runs an
  installed `tesseract` executable (versions before 4 are refused; CI runs 5.3.4), so `PdfOcr` can
  read any language Tesseract
  has data for and run where ONNX Runtime has no build. One process per image, started without a
  shell: the pixels go in on standard input as a bitmap, with no temporary files, and word boxes and
  confidence come back as TSV on standard output, parsed as untrusted text. Language names are
  checked before they reach a command line. A missing executable or missing language data fails at
  `Load`, saying where to get it. `Timeout`, `MaxOutputBytes` and cancellation kill the process,
  and `MaxConcurrentProcesses` limits how many run at once. Tesseract and its data are installed
  separately, and ONNX Runtime's native libraries stay out: an application using only this engine
  publishes 3 MB of libraries instead of 241 MB. Pages scanned sideways are not detected with this
  engine. A CI job runs it against Tesseract from the Ubuntu packages on x64 and arm64.

## 1.1.0 (2026-11-05)

- `QuantizeImages` (`--quantize`) reduces raw 8 bit RGB images to a palette of at most 256 colors in
  their own color space, for the drawings and screenshots where JPEG rings around fine labels and
  barely saves anything. Each image gets the smallest palette, of 256, 64, 16 or 4 colors, that
  keeps `MinImageSsim`, or 0.97 without a gate, and no more than 8 sample steps of mean change with
  at most 1% of pixels moved by over 32, which catches a hue drift luminance cannot see. A color
  holding most of its cluster stays exact, so white stays white. With `ConvertImagesToJpeg` the
  smaller candidate wins image by image. The proof expands the palette and checks the same bounds,
  and `PdfEquivalence.FindDifference` accepts such images when given a minimum SSIM. On a technical
  manual full of screenshots and diagrams, `JpegQuality = 50` alone saved 18.0% and adding
  `QuantizeImages` saved 47.9%. `QuantizeImages` alone saved 43.7%, with pages rendered by PDFium
  closer to the source than with `JpegQuality = 50` alone (mean SSIM 0.9999 against 0.9996). The
  pass costs CPU: about 2.7 times as long on that file, single threaded. On the public corpus it
  took the lossy profile from 29.5% to 32.1% with no failure: 15 of the 65 rewritten documents got
  smaller, up to 45%, and none got larger.
- The corpus benchmark measures a third profile, `balanced`, the preset of that name: 23.4% on the
  public corpus, at full resolution, with no page below its PSNR warning. `--profiles` picks others.
- `PdfOptimizerOptions.Balanced` and `PdfOptimizerOptions.Smallest` (`--preset balanced`,
  `--preset smallest`) are ready sets of lossy options. `Default` stays lossless.
- The corpus benchmark also runs for every version tag and attaches its report to the GitHub
  release, with a comparison against the report of the previous release made by
  `tools/corpus_compare.py`: savings, time and peak memory per profile, and per document any
  rewrite that grew by more than 1% of the original, took 1.5 times as long, needed 1.3 times the
  memory, stopped being rewritten or now fails a check. Reports record each document's SHA-256,
  so only identical files are compared.
- Opening documents with control over memory, for serverless hosts and large files.
  `PdfLoadOptions` gathers the password, `MaxDecodedStreamBytes`, a new `MaxImagePixels`, the
  `Mode` (`PdfLoadMode.InMemory` or `OnDemand`), `SpoolThreshold` and `TempDirectory`, taken by
  new overloads `PdfDocument.Open(string, PdfLoadOptions)`, `Open(Stream, PdfLoadOptions)` and
  `Open(ReadOnlyMemory<byte>, PdfLoadOptions)`; the existing overloads keep working and call them.
  - The input is read once. `Open(Stream)` copied a stream into a growing buffer and then into an
    array; now a `MemoryStream` whose buffer is exposed is used without copying, a seekable stream
    is read into an array of its length (4.0 MB allocated for a 4.0 MB file, was 8.0 MB from a
    `MemoryStream`), and any other stream is read in pieces joined once.
  - On demand from a path, the document opens the file itself and keeps it until it is disposed.
    `PdfDocument` is now `IDisposable`: disposing a document read on demand closes what it opened
    and makes later use throw `ObjectDisposedException`; the caller's stream is never disposed,
    and disposing a document in memory does nothing.
  - A stream that cannot seek (an HTTP request body, a storage download) can be read on demand:
    `OpenOnDemand` and `Open` copy it to memory up to `SpoolThreshold` (16 MiB) and past it to a
    temporary file in `TempDirectory` (the system's temporary folder by default; /tmp on AWS
    Lambda), opened with `DeleteOnClose` on Windows and unlinked once open elsewhere, and deleted
    when the document is disposed or opening fails.
  - `PdfDocument.OpenAsync(Stream, PdfLoadOptions?, CancellationToken)` reads the stream with
    asynchronous reads only, for hosts that forbid synchronous I/O, and deletes any partial copy
    when cancelled.
  - `MaxImagePixels` (2^28 by default, the previous fixed cap) refuses an image with more pixels
    before its pixels are allocated, with `PdfFormatException`, for raw samples, JPEG, JPEG 2000,
    JBIG2 and CCITT images, by the dictionary's size and by the size the data declares.
    `PdfImage.Load(byte[], int, long maxPixels)` does the same for JPEG, PNG, GIF, BMP, TIFF and
    WebP files, with `FormatException`.
  - `PdfDocument.ReleaseUnusedObjects()` frees the objects a document read on demand has parsed
    and nothing uses any more; they are parsed again when needed. Objects that differ from the
    file, and objects still referenced anywhere (found by a full garbage collection), are kept,
    so no edit is lost. Reading 300 pages that each have their own font kept 1,503 parsed objects
    and 7.8 MB; releasing every 50 pages kept 301 objects and 1.0 MB.

- `PdfDocument.Save(Stream)` writes progressively when `PdfSaveOptions.Optimize` is off: each
  object, or batch of objects for an object stream, goes to the stream as soon as it is
  serialized, offsets are counted as bytes go out, and the cross-reference data comes last.
  Memory beyond the document follows the largest object, not the file; a document opened with
  `OpenOnDemand` has its stream data copied from the source as it is written. The bytes are the
  ones `ToArray` returns for the same options. On a generated 278 MB file opened on demand, an
  unoptimized save peaked at 3 MB of live memory (51 MB working set) instead of 835 MB, and took
  0.9 to 1.2 s instead of 1.4 to 5.6 s on a busy machine. With the optimizer on, the default,
  the file is still built in memory, as the proof needs all of it; the XML docs say so.
- Without the optimizer, encryption is applied in the same single pass; the optimized path still
  encrypts the proven file as a last step.
- `PdfDocument.SaveAsync(Stream, PdfSaveOptions?, CancellationToken)` and
  `SaveAsync(string, ...)`: the same output, written with asynchronous calls only and flushed,
  for response bodies in ASP.NET Core and Azure Functions isolated, which refuse synchronous IO.
  Cancellation is observed before each object and by each write; a cancelled or failed save
  throws, and `SaveAsync(path)` then leaves the file as it was.
- `HtmlConverter.ConvertToPdfAsync(html, stream, options, cancellationToken)`: layout on its own
  thread with no caller thread waiting, then `SaveAsync`.
- Streaming optimizer: `PdfOptimizerOptions.Streaming`. The source is read on demand (as
  `OpenOnDemand` reads it), streams are recompressed a batch at a time (at most 32 MiB of source
  data) and written straight away, and the rewrite goes to a temporary file: the destination's
  own temporary file for `OptimizeFileAsync`, otherwise one in the system temporary folder. The
  proof is the in-memory proof, run on a reader over that file: every reachable object compared,
  every stream by decoded content. Duplicates are found from what streams decode to before
  anything is written, so the output can differ from the in-memory rewrite (JPEG files differing
  only in metadata stay two); on the test documents and the benchmark file it is the same.
  `OptimizeAsync` reads a `FileStream` or `MemoryStream` in place and copies any other input to a
  temporary file first, asynchronously; the caller's output is only written asynchronously.
  Linearization cannot be combined with it. `OptimizationStatistics.Streamed` reports the mode,
  and `pdfnetstack optimize --streaming` turns it on, without an input size limit. On a generated
  278 MB file: 50 MB of live memory (300 to 460 MB working set) against 810 MB (1.4 GB), and no
  slower (8 to 11 s against 10 to 15 s on a busy machine), measured by
  `tools/PdfNetStack.MemoryBench`.
- Serverless and containers. The core, HTML and OCR libraries publish with Native AOT and
  trimming with no warnings (`PdfNetStack.Ocr` is now marked AOT compatible too), checked in CI
  on linux-x64 and linux-arm64 for .NET 8 and .NET 10 by `tests/PdfNetStack.AotSmoke`, which roots
  all three libraries and then creates, opens, optimizes, extracts text and images from, reads
  predefined CJK CMaps in and converts HTML to PDF. CI also runs the test suite with invariant
  globalization and under tr-TR (`PDFNETSTACK_TEST_CULTURE` sets the culture of a test run).
- `PdfFontCollection.AddDirectory` and `PdfFontCollection.System` skip a directory that cannot be
  listed instead of giving up on the whole tree, search at most 16 levels deep without following
  links to directories (a link cycle used to recurse until the path was too long), and visit
  files in ordinal order, so the same fonts win on every machine. With no home directory (HOME
  unset, as in some serverless hosts) the per user font directories are skipped; they used to
  resolve against the current directory.
- Variable font instances are held weakly by their font. A font shared by a long running process,
  such as one in a `PdfFontCollection` used for every HTML conversion, no longer keeps every
  location it was ever asked for (each a full font program); an instance stays shared while a
  document or the application holds it.
- Name constraints fail closed without ICU. Under invariant globalization .NET performs no NFKC
  normalization or IDNA mapping, so a directory string or host name with characters outside
  ASCII cannot be put in canonical form; it now counts as undecided (excluded, and not
  permitted) instead of being compared as written, which let full width letters pass an
  excluded subtree.
- Cold start: the syllable grammars of the Khmer, Myanmar and Universal Shaping Engine shapers
  are built when such text is first shaped, not with the first shaper. The first line of text in
  an embedded font allocates 360 KB less and, under the JIT, takes about 10% less time.
  `tools/measure_cold_start.py` measures the first open and save, optimization, text extraction,
  HTML conversion and embedded font text in a fresh process.
- Samples under `samples/serverless`: a Native AOT AWS Lambda function (provided.al2023, arm64)
  that optimizes PDFs uploaded to S3 and converts HTML to PDF behind a function URL, with a SAM
  template, and an Azure Functions isolated worker app with an HTTP trigger for HTML and an Event
  Grid blob trigger for optimization. Both are built in CI.
- OCR finds pages scanned sideways or upside down. When most of the lines read on a page do not
  run left to right (the detector found them tall, or the line orientation model turned them), a
  copy reduced to 1600 pixels on its long side is read in each of the four orientations and the
  one with the most confident left to right text wins; the page is then read turned back and the
  words map back to page space through the turn. `PdfOcrPage.Orientation` says how far the
  content stood turned; `PdfOcrOptions.DetectOrientation` (on by default) turns it off. Upright
  pages cost nothing more. Sideways pages now read in line order.
- OCR rasterizer hook: `PdfOcrOptions.Rasterizer` takes an `IPdfPageRasterizer` the application
  supplies, for pages with no scan to decode (text as vector outlines, vector art, encodings not
  decoded). It renders the page as shown at `RasterizerDpi` (300, at most `MaxOcrDpi`, lowered to
  fit `MaxImagePixels`); its result is checked for size and shape, and its failures are reported
  per page. PdfNetStack still does not render; a PDFium based renderer can be plugged in.
- `pdfnetstack-ocr`, a command line tool in its own package, `PdfNetStack.Ocr.Tool`:
  `pdfnetstack-ocr in.pdf out.pdf --languages latin,arabic [--models folder]`, with `--text`,
  `--json`, `--no-orientation`, `--all-pages`, `--min-confidence`, `--threads` and `--password`.
  It carries the models and ONNX Runtime's native libraries for Windows, Linux and macOS (a 70 MB
  package), so `PdfNetStack.Tool` stays small and free of ONNX. `pdfnetstack ocr` runs it when it
  is installed and says how to install it when it is not.
- `eng/check-licenses.ps1` allows a package under `onlyIn` to ship with several named projects.
- Signature validation confirms qualified status against EU trusted lists the application
  supplies; the library still downloads nothing. `PdfTrustedList.Load` reads ETSI TS 119 612
  lists (the list of lists and national lists) from bytes or a stream, with DTDs prohibited, at
  most 32 MiB and 64 levels: scheme information, pointers to other lists with their signing
  certificates, providers, services with type, status, history, digital identities
  (certificates, subject key identifiers, subject names), qualifications with their criteria and
  additional service information. Each list's enveloped XMLDSig signature is verified against the
  certificates supplied for it, or for a national list against the pointers of a verified list of
  lists (`Load(xml, listOfLists)`): every reference must digest correctly and one must cover the
  whole list, identifiers must be unique, and Canonical XML 1.0 and 1.1 and Exclusive XML
  Canonicalization are implemented in the library (no new dependency), checked against
  Microsoft's implementation in the tests. RSA PKCS #1 v1.5, RSA-PSS with any salt length (also
  for id-RSASSA-PSS keys) and ECDSA, with SHA-256, 384 and 512; SHA-1 is refused. The signer must
  be valid at the signed XAdES signing time, or else at the issue date.
- `PdfValidationOptions.TrustedLists` and `TrustedListsAsTrustAnchors`; every signer gets
  `PdfCertificateValidation.TrustedListQualification` (`Qualification` at the signing time,
  `AtIssuance`, the deciding `Service`, `Reasons`) as ETSI TS 119 615 decides it: the CA/QC
  service whose identity is the public key of a CA above the certificate, its status from the
  history at issuance and at the signing time (granted, or supervised or accredited before
  eIDAS), the qualifiers its criteria select (key usage, policies, extended key usage, subject
  attributes, nested lists) and the certificate's QCStatements. Conflicts, criteria or critical
  extensions the library cannot evaluate, and missing or expired lists give the new
  `PdfQualification.Indeterminate`. So does a certificate whose own checks did not pass: the
  lists are then not consulted. Services are matched by public key only: a listed certificate
  with the same key, or a listed X509SKI equal to the SHA-1 of the certificate's subjectPublicKey
  (RFC 5280 method 1). The subject key identifier extension a certificate carries is never used.
- Trusted list anchors (`TrustedListsAsTrustAnchors`, default true) are restricted. Only the
  identities of CA/QC and CA/PKC services anchor a chain, and only for a validation time at which
  the service's entry in force has an approved status (granted, recognised at national level,
  or before eIDAS under supervision, in cessation or accredited) and no critical extension the
  library does not interpret. TSA/QTST and TSA services anchor time stamp authority chains under
  the same rules, never a signer's. OCSP, CRL and other services, and withdrawn, deprecated or
  never approved ones, anchor nothing. The default stays true: lists are only used when their
  signature verified against certificates the application supplied, and without anchors the
  qualification could never be confirmed. A chain that reaches none of these anchors fails, and
  the problems say what the list names its top certificate as.
- Timestamps get `PdfTimestampInfo.QualifiedTimestamp`: Passed only when the authority's
  certificate checks passed, the token is intact, and a TSA/QTST service granted at the
  validation time has as its identity the authority's own key, or the key of a CA of its chain
  that the list names as that TSA/QTST service. Otherwise Failed when no such service was
  granted, or Indeterminate when the authority's checks did not pass.
  `tools/PdfNetStack.TrustedListCheck` reads and verifies the live EU lists for a manual check;
  the tests generate every list and PKI.
- JPEG 2000 images decode tile by tile, a band of rows at a time, straight into 8 bit pixels.
  Only a window of rows per resolution and a few rows of code-blocks per subband are held in
  32 bits, so the decode limit has to cover the pixels and those rows instead of 32 bit planes
  of the whole image: a 600 dpi color page (4812 by 7541, one tile) now decodes within the
  default 256 MiB, peaking near 190 MB where it needed about 1 GB. Tiled images reuse one tile's
  buffers. The samples are unchanged, bit for bit.
- JPEG 2000 decoding is faster with unchanged output: tier-1 keeps each stripe column's state in
  one word and skips columns with nothing to code, the MQ decoder renormalizes a byte at a time,
  rows are lifted with their low and high-pass halves apart and columns several at a time, and
  the color transform and level shift run in one pass. A 6 megapixel lossy RGB image decodes in
  about 60% of the time it took; lossless images gain less, being bound by the arithmetic decoder.
- JBIG2 regions combine with the page a byte at a time instead of a pixel at a time, and an
  immediate text region that draws onto the page the same way symbol by symbol (OR onto a white
  region, AND onto a black one, or REPLACE) is drawn straight onto it with no bitmap of its own.
  MMR runs fill bytes, and generic regions read their context rows by offset. A synthetic 600 dpi
  text page decodes about four times faster.
- `PdfPageImage.Decode` maps one component images of 8 bits or less (bilevel scans, gray,
  indexed) through a table of each sample value's pixel instead of converting every pixel, and
  returns a JPEG 2000 image's 8 bit gray or RGB pixels without copying them. With the JBIG2
  changes, a synthetic 600 dpi text page comes out of `Decode` four to five times faster.
- `tools/make_jpx_fixtures.py --large` writes a large synthetic page for measuring memory and
  time; it is not committed.
- Optimizer: bilevel images are coded as CCITT Group 4 when that is smaller
  (`PdfOptimizerOptions.OptimizeBilevelImages`, on by default; `--no-group4` turns it off). It
  takes 1 bit images stored as samples under any general filters, and CCITT Group 3 data that
  decodes without a flaw and whose parameters leave nothing to a reader's defaults. The encoder
  follows ITU-T T.6 and writes the same bytes as libtiff. The proof decodes both versions and
  requires every pixel to match, and `PdfEquivalence` accepts such a recoding from any tool on
  the same terms. `OptimizationStatistics.ImagesRecodedAsGroup4` counts them.
- Lossy optimizer options reach scanned documents. `MaxImageDpi` reduces bilevel images (raw,
  CCITT or JBIG2) and keeps them bilevel: each new pixel takes the share of the source that covers
  it, the less common value wins past half, and a stroke thinner than the new pixels keeps its
  middle, so text stays legible; the result is Group 4 or Flate. It also reduces gray and RGB
  JPEG 2000 images, which become JPEG at `JpegQuality` (or lossless samples without it), and
  `ConvertImagesToJpeg` turns photographic JPEG 2000 images into JPEG. The proof decodes JPEG 2000,
  JBIG2 and CCITT sources, compares a bilevel output with the same reduction of its source, and
  allows a JPEG 2000 image without /BitsPerComponent to gain /BitsPerComponent 8. A JBIG2Globals
  stream no image uses any more is dropped.
- Optimizer: every JPEG the lossy options make is checked against the proof's color and coarse
  scale bounds before it is chosen, so a small image a JPEG would shift too far stays as it was
  instead of failing the proof of the whole file.
- The lossy image pass shares a memory budget of twice `MaxDecodedStreamBytes` between its
  workers, so a document of large JPEG 2000 scans no longer decodes one per thread at once: peak
  memory on two scanned books of the benchmark corpus fell from 5.7 and 7.0 GiB to 2.2 and 2.7.
- CCITT decoding: a row whose every pixel changes color (a 1 or 2 pixel wide image, for one)
  no longer ends decoding there.
- `tools/scan_bench.py` writes synthetic scans of each kind a scanner produces (1 bit under Flate
  and LZW, Group 3 fax, Group 4, JBIG2, gray and color JPEG 2000) and measures both optimizer
  profiles on them.
- HTML conversion is 1.5 to 1.9 times faster with the same PDF, byte for byte. On the benchmark
  samples (fastest of 125 conversions each, on a busy 8 core machine): a one page invoice went
  from 46 to 31 ms, a one page report from 30 to 20 ms, a 21 page invoice with a 640 row table
  from 465 to 244 ms, a 15 page report with lists, images, SVG charts and a grid from 110 to 68
  ms, and a 52 page text from 316 to 193 ms. The first conversion in a new process is 1.1 to 1.8
  times faster. What changed:
  - `ConvertToPdf` saves without the optimizer's equivalence proof, which read both files back
    and took about half the optimizer's time: the converter draws all the content itself. New
    `PdfSaveOptions.VerifyOptimization` (default true) and `HtmlConverterOptions.SaveOptions`
    (default null: as `PdfSaveOptions.Default` without the proof); set `SaveOptions` to
    `PdfSaveOptions.Default` to keep the proof.
  - Each page paints the parts of the document that reach it instead of walking all of it.
  - Table cells are measured for the column widths and laid out from the same text, fonts,
    shaping and break opportunities, and collapsed borders read each style once per table.
  - Shaped runs are kept for the rest of the conversion (up to 256 characters a run and 256K
    characters in all), so repeated labels and numbers shape once.
  - OpenType lookups pass over glyphs their first coverage does not list, as HarfBuzz's set
    digests do; each stage's lookups are worked out once per font. A hostile font's step budget
    is spent more slowly as a result; output is otherwise the same.
  - Installed fallback fonts are looked up only when a character needs them, so a document in
    the generic families never scans the installed fonts: its first conversion in a process went
    from 352 to 258 ms.
  - Font subsets are made and compressed side by side when a document is saved.
  - Less work per character in inline layout: the bidi algorithm is skipped for text below
    U+0590 in a paragraph that is not right to left (every level is 0 there), fonts are chosen
    once per run of one style, break opportunities need no sorting, and CSS keywords and colors
    are read once per value. Attribute values without character references, such as data: URIs,
    are copied whole.
  - The optimizer's content analysis reads operators without a string each, and font program
    merging reads each program once.
  - Fixed: `ConvertFileToDocument` dropped `MaxPages` when it set the base address.
- `tools/PdfNetStack.Benchmarks` has three larger HTML samples (a 21 page invoice, a 15 page
  report with lists, images, SVG and a grid, a 52 page text); `quick`, `cold` and `samples` take
  a sample name, `samples` also writes the PDFs and their page counts, and `quick` counts the
  allocations of every thread.
- HTML: absurd sizes no longer exhaust memory. A font size such as 1e300px laid out one line
  across an unbounded number of pages. Computed lengths are now clamped to a million px either
  way (percentages to a million percent), font sizes to 10000px, unitless line heights to 1000
  and page sizes to 14400 points a side, and `HtmlConverterOptions.MaxPages` (default 10000)
  ends a conversion that needs more pages with the new `HtmlConversionException`.
- Variable fonts in font matching. `PdfFontCollection` counts a variable font for every weight,
  width and slant its axes cover, and registers its named instances as faces; faces are also
  found by full or PostScript name. `Find(family, PdfFontQuery)` matches width, then style, then
  weight with ranges as CSS Fonts 4 does (5.2), and returns the instance at the values found,
  with `OpticalSize` and `Variations` applied over them (7.2): ital for italic, slnt for oblique,
  never both. `PdfFontStyle` gives normal, italic or oblique at any angle. Static families match
  as before, now also by OS/2 width class.
- HTML: `@font-face` takes ranges in `font-weight`, `font-stretch` (and `font-width`) and
  `font-style` (`oblique 0deg 15deg`), and the `font-variation-settings` and
  `font-named-instance` descriptors. A missing descriptor is `auto`: the face matches as normal
  and the variation follows the style unclamped. `font-style: oblique <angle>`,
  `font-optical-sizing` (opsz set to the font size in CSS px) and `font-variation-settings` are
  supported; the `font` shorthand resets the last two. Rules with identical descriptors: the
  last one wins.
- Variable font instances are made once per location and the default location is the font
  itself, so a document embeds one program per location drawn, however many sizes use it.
  `@font-face` rules that share a file share its font. A damaged variation table makes matching
  fall back to the font's default instance.
- HTML bounds the variable font instances a document can ask for: axis values snap to a grid
  (whole units for wght, wdth and opsz, half degrees for slnt, tenths for ital, a hundredth of
  the range for other axes) and each font makes at most 32 instances per conversion, the nearest
  one standing in beyond that. Font values that are not finite are dropped and the rest clamped,
  and a style value that font matching still rejects falls back to the family's default instead
  of failing the conversion. `font-stretch` accepts only finite percentages.
- `PdfFontQuery` checks each value when it is set and copies `Variations`, so an invalid query
  throws where it is built.
- `tools/make_variable_font_fixtures.py` also builds Test-Axes.ttf, a font with wght, wdth,
  opsz, ital and slnt axes for the matching tests.
- PDF/A-1b output (`PdfConformance.PdfA1B`): PDF 1.4 with a classic cross-reference table and no
  object streams, also when the optimizer runs. Transparency (opacity, soft masks, alpha images,
  blend modes, transparency groups), JPEG 2000, optional content, embedded files, file attachment
  and other post PDF 1.4 annotations, OpenType font programs and ICC profiles newer than version 2
  are refused with the rest of the problems; subset CID fonts get a CIDSet stream read from the
  embedded program.
- PDF/A-4, 4e and 4f output (`PdfA4`, `PdfA4E`, `PdfA4F`): PDF 2.0 with `pdfaid:rev` 2020. The
  information dictionary written holds only the modification date; the document keeps its values
  and the XMP carries them. Every embedded file is an associated file with its relationship; 4f
  needs at least one and takes any type, plain 4 and 4e take PDF/A-1, 2 and 4 files only, and 4e
  allows 3D and RichMedia annotations with their GoTo3DView and RichMediaExecute actions.
- PDF/A checks for every part: actions are walked (open action, links, widgets, bookmarks and
  `Next` chains) and Launch, JavaScript, ResetForm, ImportData, Hide, media and other forbidden
  types, and named actions other than page navigation, are refused, as are additional actions on
  pages, widgets and fields, XFA, transfer functions, external stream data, PostScript and
  reference XObjects and OPI. LZW streams are re-encoded with Flate, image interpolation is
  switched off and NeedAppearances is removed. Saving an invoice as anything but PDF/A-3 is
  refused.
- Signature validation, the X.509 checks of ETSI EN 319 102-1: certificate policy processing as in
  RFC 5280, 6.1 with policy mappings, policy constraints and inhibit anyPolicy, on the policy graph
  of RFC 9618 (`PdfValidationOptions.AcceptablePolicies`, `RequireExplicitPolicy`,
  `InhibitPolicyMapping`, `InhibitAnyPolicy`; `PdfCertificateValidation.Policy` and
  `ValidPolicies`); name constraints on DNS names, email addresses, URI hosts, directory names
  and IP addresses, the trust anchor's included (`PdfCertificateCheck.NameConstraints`), with
  hosts compared in one canonical form (one trailing dot dropped, IDN as ASCII, lower case, URIs
  read by `System.Uri`), directory strings decoded in every string type and prepared as RFC 4518
  says, and anything that cannot be read or judged failing closed, including constraints that
  cannot be read, which fail every certificate below them; and the
  QCStatements of ETSI EN 319 412-5 (`PdfCertificateCheck.QcStatements`, `PdfQcStatements`),
  with the qualification they claim (`PdfCertificateValidation.Qualification`: QESig, QESeal,
  AdESig-QC and the rest). No trusted list is downloaded. Critical policy, name constraint and
  QC statement extensions no longer fail a certificate as unknown.
- Text extraction is about 3.8 times faster with identical output: `ExtractText` on a 60 page,
  27,000 word report in an embedded TrueType font went from 255 ms to 67 ms and from 300 MB to
  69 MB allocated. Reading order sorts runs of touching letters instead of single letters and
  decides each line once per size and baseline; the content interpreter no longer copies the text
  state for every glyph, shares operator names and reads CID widths without a copy per glyph.
- HTML conversion does less repeated work per line of text: a tab width is measured only for runs
  that contain a tab, the paragraph text is copied once for all its runs, and a font's OpenType
  features are read once per script. Output is unchanged.
- The command line tool is published as native executables, compiled ahead of time, for
  linux-x64, linux-arm64, win-x64, osx-x64 and osx-arm64, attached to each GitHub release with a
  `SHA256SUMS.txt`. No .NET install is needed to run them.
- `tools/PdfNetStack.Benchmarks` measures text extraction and HTML conversion with
  BenchmarkDotNet; it is not part of the solution or the packages.
- Timing tests take the fastest of several runs, so a busy machine cannot fail them.
- Shaping for more scripts, matching HarfBuzz glyph for glyph on the fonts we compare:
  - The Universal Shaping Engine, with its categories generated from the Unicode Character
    Database and Microsoft's overrides by `tools/make_shaping_data.py`: Balinese, Javanese,
    Sinhala, Tibetan, Sundanese, Batak, Buginese, Cham, Tai Tham, Lepcha, Meetei Mayek, Chakma,
    Grantha, Newa, Sharada, Tirhuta and the other Brahmic scripts HarfBuzz gives it, Indic fonts
    made for it (dev3 and the other version 3 tags), and Mongolian, N'Ko, Adlam and the other
    scripts that join like Arabic (a Mongolian free variation selector takes its letter's form),
    and Egyptian hieroglyph clusters.
  - Thai and Lao: sara am splits into nikhahit and sara aa, and fonts without Thai substitutions
    get their marks placed through the Windows or Macintosh private use glyphs.
  - Khmer (coeng ro and pre-base vowels reordered, split vowels decomposed, cfar), Myanmar
    (kinzi, medials and pre-base vowels reordered) and Hangul (syllables composed or decomposed
    for the font, ljmo, vjmo and tjmo, tone marks).
  - All shapers: invalid vowel sequences of the Indic and USE scripts get a dotted circle, cmap
    format 14 variation sequences pick their glyphs, a combining grapheme joiner that blocks no
    reordering is skipped by lookups, a ZWJ keeps a mark from attaching to the base before it
    (mark and mkmk see joiners), and contextual lookups whose nested lookups add glyphs index the
    sequence as HarfBuzz does.
- Text extraction reads right to left lines in logical order when they start or end with a Latin
  word: the paragraph direction comes from both ends of the line, and punctuation next to the
  word stays in it ("To." rather than "To ."). Numbers in right to left lines follow the
  weak rules of UAX #9: after Arabic, "2026-114" (drawn 114-2026) reads in logical order, while
  dates, amounts and percentages stay whole.
- Public benchmark corpus: `eng/corpus/manifest.json` lists 77 public domain and openly licensed
  PDFs (U.S. government publications, arXiv papers under CC BY 4.0, public domain scans and
  Chinese, Japanese and Korean documents from Wikimedia Commons, and test files of the veraPDF,
  qpdf, PDFium, pypdf and PDF Association projects) by URL, SHA-256, size, license, where the
  license is stated and what each file exercises. No file is committed. `tools/fetch_corpus.py`
  downloads and verifies them; `tools/corpus_bench.py` runs `tools/PdfNetStack.CorpusRunner` on
  each one, one process per measurement, for lossless and lossy optimization (size, time, peak
  memory), checks every rewrite again with `PdfEquivalence`, compares the text and decoded images
  the library reads before and after, compares PDFium renders, and writes a Markdown and a JSON
  report. The `Corpus benchmark` workflow runs it weekly and on demand.
- Fixed, found by the corpus:
  - A cross-reference `/Prev` chain that loops is treated as damage and the file is rebuilt from
    its objects. A newest section naming itself as `/Prev` used to hide every older section, so
    the file opened without its page tree, and the optimizer (with `AllowRepairedFiles` or not)
    could rewrite it as a document without pages.
  - Content stream compaction judges whether a stream ends inside a comment from its tokens. A
    `%` inside a string on the last line, common in the two byte codes of composite fonts, was
    taken for a comment once the stream was compacted onto one line, so the proof rejected the
    rewrite and the whole document was returned unchanged.
  - JBIG2: the work budget is eight times the page instead of four. Scans that code the page as
    one symbol placed by a text region spend five, and failed to decode.
- Optimizer, lossless structural passes, each proven by the equivalence check against its own
  analysis of the source (docs/FIDELITY.md gives the rules):
  - `PruneUnusedResources` removes fonts, XObjects, graphics states, patterns, shadings, color
    spaces and property lists that no content using the resource dictionary names, following
    forms, tiling patterns, Type 3 glyph procedures, soft masks and annotation appearances, and
    inherited and shared resource dictionaries. Anything it cannot read for certain is kept.
  - `CompactContentStreams` rewrites content streams with minimal white space, canonical numbers,
    shortest strings and no comments; inline image data is copied exactly.
  - `MergeFontSubsets` merges subsets of one TrueType font embedded separately, as merged
    documents carry them, into one program when no glyph any page shows changes.
  - Deduplication compares streams by their decoded content, so differently encoded copies of a
    font or image merge.
  - `Linearize` (opt-in) writes a linearized file (Annex F) with page offset, shared object and
    outline hint tables, laid out the way qpdf checks it. Inherited page attributes move onto the
    pages. `OptimizationStatistics` reports `ResourcesPruned`, `ContentStreamsCompacted`,
    `FontProgramsMerged` and `Linearized`; the command line tool takes `--linearize`,
    `--no-prune`, `--no-compact` and `--no-font-merge`.
- `PdfDocument.OpenOnDemand(Stream)` opens a document on a seekable stream without reading it into
  memory: objects are parsed when first used and stream data is read when asked for, with 64 bit
  offsets. `PdfDocument.SaveIncremental(Stream)` saves the changes since opening as an incremental
  update, appending in place when given the source stream; existing signatures stay valid.
- `PdfEquivalence.FindDifference` accepts the structural passes above and inherited page
  attributes moved onto pages.
- `tools/make_optimizer_samples.py` and `tools/measure_optimizer.py` measure each pass on
  synthetic files and check linearized output with qpdf when it is installed.
- Annotations: `PdfAnnotationCollection.AddStamp` (the fourteen standard stamps, or a custom one
  from a `PdfForm` or a drawing callback), `AddInk`, `AddSquare`, `AddCircle`, `AddLine` (with
  `PdfLineEnding` at either end), `AddPolygon`, `AddPolyline` and `AddFileAttachment` (the file
  embedded behind an icon; `PdfAnnotation.Attachment` reads it back). `PdfAnnotationOptions`
  sets the color, interior color, border width, dashes, opacity, comment, author and subject.
  Each comes with a generated appearance; below full opacity the drawing is a transparency group
  faded as a whole. PDF/A-3 output declares the files of file attachment annotations as
  associated files, and PDF/A-2 refuses them.
- Annotation flattening: `PdfAnnotationCollection.Flatten(annotation)`, `Flatten(options)` for a
  page and `PdfDocument.FlattenAnnotations(options)` draw each annotation's normal appearance (or
  the one its state selects) into the page content, fitted to its rectangle by bounding box and
  matrix as ISO 32000-1, 12.5.5 describes, upright on rotated pages for NoRotate annotations and
  inside its optional content group, then remove it with its pop up. Hidden and NoView
  annotations stay; links and widgets stay unless `PdfFlattenOptions` asks for them, and a
  flattened widget leaves the form. `PdfAcroForm.Flatten` uses the same placement.
- Form fields travel with their pages: `Pages.Import`, `Merge` and `Split` copy each widget's
  field and ancestors into the target form, with the source form's default appearance,
  alignment, resources and calculation order. A top level field whose name the target already
  uses is renamed with the first free suffix `_2`, `_3` and so on. Before, widgets were copied
  without their fields.
- Format actions: a field whose format action calls `AFNumber_Format`, `AFPercent_Format`,
  `AFDate_FormatEx`, `AFDate_Format`, `AFTime_Format`, `AFTime_FormatEx` or `AFSpecial_Format`
  shows its value formatted in the generated appearance (red for negative numbers when asked),
  and `PdfTextField.FormattedText` returns it. The call is parsed and the formatting done in C#;
  no JavaScript runs, and any other script leaves the value as it is.
- Fixed: form flattening threw on a widget whose appearance entry was not a stream.
- HTML grid layout follows CSS Grid 1 and 2 further: `grid-auto-flow` with `column` and `dense`
  packing; line names in track lists and `repeat()`, placement by name and by `span name`, with
  the `name-start` and `name-end` lines of areas and the implicit lines past the explicit grid;
  implicit tracks before the explicit grid for negative lines; `subgrid` on rows, columns or
  both, with the parent's line names, the subgrid's own name list, its own gaps, and its items
  sizing the parent's tracks through its margins, borders and padding. `repeat(auto-fill)`
  counts the other tracks of the list when working out how many repetitions fit, and the empty
  tracks of `repeat(auto-fit)` collapse with their gaps. The `grid-template` and `grid`
  shorthands are read, area strings and `auto-flow` included.
- HTML named pages: the `page` property works on any block in the block flow, not only children
  of `body` (CSS Paged Media 3, 8). A change of page name forces a break, and the content after
  it is laid out at its own page type's width. A box that spans the break is sliced: its top
  edge shows before the break and its bottom edge after, and the margin after the break is kept.
- HTML pagination: a forced break before the first content or after the last no longer adds an
  empty page, so `break-before: page` on the first element starts the document on page one.
- Fixed: HTML boxes with a `z-index` now paint in stacking order (CSS 2.1, Appendix E). A
  negative `z-index` paints over the stacking context's background and borders and under its
  blocks, floats and text; a positive one paints over all of it, in z-index order. A
  `position: fixed` box with a negative `z-index` paints under the content of every page, where
  before it painted over it. Positioned boxes, flex items and grid items with a `z-index`, and
  boxes with opacity below one or a transform, start a stacking context.
- Fixed: HTML `text-decoration-style: wavy` draws a wave instead of a straight line, for
  underline, overline and line-through. Its amplitude and wavelength follow the line's
  thickness, and it is clipped to the decorated text.
- PAdES B-T, B-LT and B-LTA. `PdfTimestampClient` builds RFC 3161 requests (hash, random nonce,
  optional policy) and checks the reply (status, hash, nonce, signature, a critical time stamping
  extended key usage); the application supplies the transport. `PdfSignatureOptions.Timestamp`
  adds a signature timestamp, `PdfSigner.AddDocumentTimestamp` a /DocTimeStamp, and
  `PdfSigner.AddValidationData` the document security store: certificate chains, OCSP responses
  and CRLs from an `IPdfRevocationSource`, and a VRI entry per signature, in an incremental update.
  Only revocation data that matches the certificate and is signed by its issuer or delegated
  responder is kept; delta and partitioned CRLs are not used. The library makes no network calls.
- Signature validation: `PdfSigner.Verify(pdf, PdfValidationOptions)` builds chains to caller
  supplied trust anchors and checks validity at the timestamp's time when its authority validates
  (otherwise now), key usage (signer,
  CA with path length, time stamp authority) and revocation from the document security store or
  supplied data, with OCSP freshness and delegated responders. `PdfCertificateValidation` reports
  each check per certificate as passed, failed, indeterminate or not checked, with the reasons.
  `PdfSignatureInfo` now also reports document timestamps and signature timestamps.
- Factur-X and ZUGFeRD: `PdfInvoice.Attach` embeds the invoice XML (checked for well formedness,
  without DTDs) as factur-x.xml, or xrechnung.xml for XRECHNUNG, with the profile's relationship;
  saving as PDF/A-3 writes the fx: XMP properties (MINIMUM to EXTENDED and XRECHNUNG) with their
  extension schema, checks the attachment is still there, and keeps the metadata when an opened
  invoice is saved again.
- PDF/A-3: the catalog's /AF array now references the attachment's file specification itself
  instead of a copy of it.
- CI: a veraPDF job validates PDF/A-2b, 2u, 3b and 3u files made by the library, including a
  Factur-X invoice signed to PAdES B-LTA.
- Lossy image options for the optimizer, all off by default:
  - `MaxImageDpi` (`--max-dpi`) downsamples images drawn above 1.5 times the target resolution,
    measured from the largest placement found on pages, in forms, tiling patterns, soft mask
    groups and annotation appearances. The filter is Lanczos with three lobes. Masks, images with
    a color key mask or a /Matte soft mask, bilevel, indexed, 16 bit, Lab, Separation and DeviceN
    images, and images reachable from anything the measurement does not walk (Type 3 glyphs,
    thumbnails, alternates) are left alone. A downsampled JPEG stays a JPEG, never finer than it was.
  - `ConvertImagesToJpeg` (`--to-jpeg`) re-encodes raw 8 bit gray and RGB photographs as baseline
    JPEG at `JpegQuality` (forward DCT, libjpeg quality tables, 4:2:0 below quality 90, Huffman
    tables optimized per image). A classifier leaves line art, screenshots, charts, palette and
    bilevel images lossless. A lossy result is kept only when it is at least 10% smaller than the
    best lossless one.
  - `MinImageSsim` (`--min-ssim`) is a perceptual gate on luminance SSIM: any image whose lossy
    change, requantization included, falls below it stays as it was, and
    `ImagesKeptByQualityGate` counts them. The proof checks resampled and converted images
    against the source resampled to their new size (components, aspect, mean color, a coarse
    comparison and SSIM), and with a gate also requantized ones. An image whose data is carried
    over unchanged must keep its size. `PdfEquivalence.FindDifference` takes the same gate and
    the optimizer's exclusions: soft masks, masks, stencil and color keyed images must match
    exactly, and images with a /Mask or a /Matte soft mask keep their size.
  - A JPEG with a color key mask is no longer requantized, since the key matches exact sample
    values.
  - `ProgressiveJpeg` (`--progressive`) also writes every JPEG the optimizer produces as
    progressive, with libjpeg's scan script and Huffman tables per scan, and keeps it when
    smaller. Coefficients are unchanged, so this one is lossless.
  - `OptimizationStatistics` reports `ImagesDownsampled`, `ImagesConvertedToJpeg` and
    `ImagesKeptByQualityGate`, and the result's reason names every lossy change.
- `CompressionEffort.Exhaustive` (`--exhaustive`): an original Deflate encoder in the manner of
  Zopfli, written from RFC 1951 and published descriptions: optimal parsing over every match
  length with costs refined over several passes, block splitting, and code lengths limited by
  package-merge. zlib's output is kept whenever it is smaller. On the synthetic files of
  `tools/make_lossy_corpus.py` it saves 3 to 5% more than level 9 on text, scans and photographs
  (nothing on line art and screenshots), for 2 to 150 times the CPU time, the most on long
  repetitive streams.
- `PdfNetStack.Azure` is discontinued so the project can focus on PDF itself. Reading from and
  writing to storage belongs to the application, around the stream APIs. Version 1.0.0 stays on
  NuGet and keeps working.
- Variable fonts: `PdfOpenTypeFont.GetInstance` gives a named instance (`NamedInstances`) or any
  axis values (`VariationAxes`, `IsVariable`), embedded as a static font. TrueType outlines move
  by their gvar deltas, with untouched points interpolated and composite offsets varied; advances
  come from HVAR or the phantom points, font metrics from MVAR (with hhea kept in step), control
  values from cvar, weight and width classes and the italic angle from the axes, through avar.
  CFF2 fonts load (at their default instance) and convert to CID-keyed CFF: blends resolve in
  charstrings and Private DICTs, subroutines are inlined, and argument runs longer than CFF's 48
  operands are split. The shaper applies GDEF variation deltas to GPOS values and anchors, and
  GSUB and GPOS feature variations. Outlines, advances and metrics match fontTools' instancer and
  shaping matches HarfBuzz on the synthetic fixtures and on the variable fonts we compare.
  Damaged variation data is a `FontFormatException`.
- Unicode normalization in the shaper, as HarfBuzz's normalizer does it: characters the font
  lacks decompose into ones it has, marks sort into canonical order (with the shaping tailorings
  for Hebrew, Arabic and others), a base and mark compose when the font has the composite, and
  spaces and the non-breaking hyphen without glyphs draw with the space and hyphen at their own
  widths. Font fallback, `CanRender` and HTML text use the same coverage. Data generated from the
  Unicode Character Database 16.0 by `tools/make_shaping_data.py`.
- Indic shaping for Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada and
  Malayalam, per Microsoft's script development specifications: syllables, base consonant,
  reph, pre-base matras and consonants, the basic forms one feature at a time, final reordering,
  presentation forms, old (deva) and new (dev2) font specifications, and dotted circles for
  broken clusters. It matches HarfBuzz on a synthetic Devanagari and Bengali fixture and on the
  three faces of a font covering all nine scripts.
- Kashida justification (`PdfParagraph.Kashida`, off by default): justified Arabic lines elongate
  one join per word with tatweel, at the point Arabic typography prefers, up to three tatweel
  widths, before spaces widen. Tatweels carry no text, so copying gives the original words.
- Fixed: a mark drawn with a vertical offset (Arabic dots and vowels in many fonts) moved every
  glyph after it by its horizontal offset, so letters failed to join and lines came out wider
  than measured. Spaces at the edge of a right to left run are now written outside its
  ActualText, so viewers no longer read them on the wrong side of the neighboring run.
- Tagged PDF from HTML (`HtmlConverterOptions.Tagged`, on by default): headings, paragraphs,
  lists, tables with header and data cells, figures with their alt text, links tied to their
  annotations, sections and quotes form a structure tree in reading order (ISO 32000-1, 14.8);
  backgrounds, borders, text shadows, repeated table headers, fixed boxes and page margins are
  artifacts. The catalog is marked and viewers show the document title.
- JPEG 2000 images decode (`PdfPageImage.Decode`, JPXDecode): JP2 files and raw codestreams,
  ITU-T T.800 part 1. Every progression order and progression change, precincts, layers, tiles
  and tile-parts, PPM, PPT, SOP and EPH, all code-block styles, ROI, the 5/3 and 9/7 wavelets,
  RCT and ICT, subsampled, signed and 1 to 16 bit components, JP2 palettes and channel
  definitions. The image dictionary's color space wins, otherwise the JP2 header's (ISO 32000-1,
  7.4.9). Lossless images decode bit exact and lossy ones within one step of OpenJPEG. Sizes,
  counts and work are bounded. SMaskInData is not applied; HTJ2K (part 15) and part 2 extensions
  are not supported.
- JBIG2 images decode (`PdfPageImage.Decode`, JBIG2Decode with JBIG2Globals): generic,
  refinement, text and halftone regions with their symbol and pattern dictionaries and code
  tables, arithmetic, Huffman and MMR coded, per ITU-T T.88. Output is bounded by the image's
  size, and damaged data is a `PdfFormatException`. Tested bit for bit against PDFium on the T.88
  Annex H test datastream and synthetic streams.
- Named destinations: `PdfDocument.NamedDestinations` lists the names of the /Dests name tree
  and the catalog's /Dests, resolves them to pages, and renames them with every bookmark, link
  and GoTo action that refers to them, in their own string or name form, rewriting the name tree
  as one sorted leaf; colliding renames are refused. `PdfOutline.DestinationName` gives the name
  a bookmark points at.
- GIF images: `PdfImage.Load` reads GIF 87a and 89a (the first frame, interlaced or not, with its
  transparent color as a soft mask), so `DrawImage` and `<img>` in HTML take them too.
- BMP, TIFF and WebP images: `PdfImage.Load` reads BMP (1 to 32 bits, RLE4 and RLE8, bit fields,
  top down rows, alpha, an embedded JPEG or PNG), TIFF (strips and tiles, chunky and planar,
  uncompressed, PackBits, LZW and Deflate with the predictor, CCITT modified Huffman, Group 3 and
  Group 4, JPEG; bilevel, gray, palette, RGB, CMYK and YCbCr samples of 1 to 16 bits, associated
  and unassociated alpha, BigTIFF) and WebP (lossy and lossless, with alpha, the first frame of an
  animation). Alpha becomes a soft mask. A TIFF page stored as one strip of fax or JPEG data is
  embedded as is (CCITTFaxDecode, DCTDecode). `PdfImage.GetPageCount` and `PdfImage.Load(path,
  page)` give every page of a multi-page TIFF. Lossy WebP decodes to the pixels libwebp gives,
  bit for bit. Decoding is bounded, and damaged files fail with a `FormatException`. `<img>` in
  HTML takes all three formats.
- `PdfGraphics.DrawImage` fits an image into a box (`PdfImageFit`: fill, contain, cover, none,
  scale down), aligns it there (`PdfImageAlignment`, clipping what spills over) and fades it
  with an opacity.
- Form creation: `PdfAcroForm.AddTextField`, `AddCheckBox`, `AddRadioGroup` (buttons may span
  pages), `AddComboBox`, `AddListBox`, `AddSignatureField` and `AddPushButton` (optionally opening a
  web address), styled with `PdfFieldOptions` (standard font, size, colors, border, tooltip, read
  only, required, alignment, multiline, password, length limit, comb, editable, multiple
  selection). Every new field comes with its appearance and the print flag.
- New package `PdfNetStack.Ocr`: `PdfOcr.MakeSearchable` finds each page's
  scan, turns it upright, reads it and lays the words over it as invisible text; `Recognize`
  reports without changing the document. `OnnxOcrEngine` runs the PP-OCRv5 mobile models
  (PaddleOCR, Apache-2.0) on ONNX Runtime for Latin and Arabic script, with DB box detection, line
  orientation, CTC decoding, word boxes and right to left ordering in managed code, and model files
  checked against pinned SHA-256 hashes (`tools/fetch_ocr_models.py`). ONNX Runtime's telemetry is
  turned off before the first model loads. `IOcrEngine` plugs in any other engine.
- New package `PdfNetStack.Ocr.Models` (Apache-2.0 models, versioned on its own from 1.0.0): the
  four PP-OCRv5 model files with their license and notices, copied next to the application on build
  and publish; `OnnxOcrEngine.Load()` without a folder loads them from there. The files are never
  committed: CI and the release workflow fetch them, and packing fails when one is missing or does
  not match its pin. The license check now also covers bundled files and keeps ONNX Runtime out of
  every package but `PdfNetStack.Ocr`.
- OCR reads scans stored as several images: strips or tiles side by side, or a text mask over a
  background. The pieces are painted in drawing order into one gray canvas in page space, at the
  finest piece resolution up to `MaxOcrDpi`, decoded one at a time, with image count, combined
  pixels, canvas size and overlap capped before anything is decoded; word boxes map back to the
  page through the canvas.
- Text extraction: `PdfPage.ExtractText()` returns a page's text in reading order,
  `GetLetters()` each glyph with its text, bounds, baseline, font name, visible size and render
  mode, and `GetWords()` the words with their bounds. Glyphs are placed as the specification
  positions them (character, word and horizontal scaling, rise, TJ adjustments, forms, Type 3
  font matrices, vertical writing). Text comes from ToUnicode maps (bfchar and bfrange, ligatures,
  surrogate pairs, codes of 1 to 4 bytes), from the simple font encodings and /Differences with
  the Adobe Glyph List, from embedded CMaps with usecmap, the Identity and Unicode predefined
  CMaps, and embedded TrueType cmaps; spans marked with ActualText are replaced by their text, so
  right to left text the library writes reads back in logical order. Lines follow each run's own
  baseline, so rotated and vertical text read in order; columns of prose are found by XY cuts
  and read one after the other, right to left for Arabic and Hebrew pages, while tables keep
  reading row by row. CMaps, fonts and content are bounded and
  never throw.
- CJK text extraction: Adobe's predefined CMaps for the Adobe-Japan1, GB1, CNS1, Korea1 and KR
  collections (all 180 of cmap-resources, among them every CJK CMap of ISO 32000-1 Table 118, such
  as 90ms-RKSJ-H, GBK-EUC-H, B5pc-H, KSCms-UHC-H and the Uni...-UCS2, UTF16, UTF8 and UTF32 forms)
  split codes and give CIDs, so widths and vertical metrics follow the CMap, and the CID to
  Unicode tables of mapping-resources-pdf give text to composite fonts without a ToUnicode map,
  by the CMap's collection or the CIDFont's /CIDSystemInfo (ISO 32000-1, 9.10.2). The data is
  packed into one Brotli stream per collection (196 KB in all) and decompressed on first use.
  Embedded CMaps that use a predefined one inherit its collection, and keep their own /WMode.
- Simple fonts without /Encoding read their codes through the built-in encoding of an embedded
  CFF program (FontFile3, Type1C or OpenType): the Standard and Expert encodings and custom
  encodings of both formats with supplements, glyphs named through predefined or custom charsets.
- `PdfGraphics.DrawInvisibleText`: invisible, searchable text stretched to a box, optionally
  rotated, for OCR text layers. A built in glyphless font (Type0, Identity-H, one empty glyph,
  identity ToUnicode) takes any script; right to left text and characters outside the BMP also
  carry ActualText.
- Image extraction: `PdfPage.GetImages()` lists the images a page draws (XObjects, inside forms
  too, and inline images) with their placement, bounds and resolution. `PdfPageImage.Decode()`
  returns a `PdfBitmap` of 8 bit gray or RGB pixels from raw samples of 1 to 16 bits, from
  JPEGs (inverse DCT, fancy upsampling and color conversion as libjpeg does them, within two
  steps per sample) and from CCITT fax data (Group 3 one and two dimensional, Group 4, bit
  exact with libtiff), through the device, Cal, ICC based, Indexed and Lab spaces, and Separation
  and DeviceN (exact for a Type 2 tint transform, gray otherwise); `PdfBitmap.ToPng()` saves it. `GetEncodedData()` returns a JPEG or JPEG 2000 image as stored.
- New package `PdfNetStack.Html`: HTML and CSS to PDF in pure C#, without a browser.
  `HtmlConverter.ConvertToPdf`, `ConvertToDocument` and `ConvertFileToDocument`, configured with
  `HtmlConverterOptions` (base address, resource loader, fonts, page style, extra style sheet,
  bookmarks).
  - HTML parsed the way browsers repair it, with the legacy presentational attributes.
  - CSS: cascade, inheritance, `var()`, Selectors Level 4, media queries for print, `@import`,
    `@font-face`, `@page` with margin boxes, `calc()` and friends, CSS Color 4.
  - Layout: blocks with margin collapsing, inline text with Unicode line breaking, bidi and
    shaping, floats, relative, absolute and fixed positioning, tables in both border models,
    flexbox, grid with fr tracks, `minmax()`, `repeat()` and named areas, inline blocks,
    lists, counters and generated content.
  - Paged media: page sizes and margins, `:first`, `:left` and `:right`, named pages laid out
    at their own size, running headers and footers with page counters and named strings
    (`string-set`, `string()`), running elements (`position: running()` shown with
    `content: element()`, any content: logos, tables, styled text, changing with the page where
    they stand, with page counters inside), margin boxes that widen when
    their neighbours are empty, forced and avoided breaks, orphans and widows, table headers
    repeated on every page.
  - Painting: backgrounds with images and gradients, rounded borders, shadows, outlines,
    transforms, `object-fit`, decorations; links and bookmarks; document metadata from the head.
  - Safe by default for untrusted HTML: no network access without a loader, files only from
    the base folder, and limits on nesting, selectors, `var()` expansion, spans and tiling.
  - It depends on the exact `PdfNetStack` version it ships with.
- The `PdfNetStack` and `PdfNetStack.Azure` packages are checked against 1.0.0 on every pack, so
  a change that would break code built against the published version fails the build.
- SVG: `PdfSvg.Load` and `Parse`, and `PdfGraphics.DrawSvg`, draw SVG as PDF vectors: shapes,
  paths with arcs, groups, `use` and `symbol`, nested viewports with `viewBox` and
  `preserveAspectRatio`, transforms, solid colors, linear and radial gradients, clip paths,
  opacity, dashes, text in the standard fonts, embedded images, and `<style>` rules with type,
  class and id selectors. Each drawing is written once per document. Doctypes are skipped, so
  entity expansion cannot be abused, and size, depth and `use` nesting are capped. In
  `PdfNetStack.Html`, SVG works in `<img>`, CSS backgrounds and inline `<svg>`.
- Public object model: `PdfObject`, `PdfNull`, `PdfBoolean`, `PdfNumber`, `PdfName`,
  `PdfString`, `PdfArray`, `PdfDictionary`, `PdfStream` and `PdfReference` in
  `PdfNetStack.Objects`, with `PdfDocument.Catalog`, `Resolve`, `AddObject` and
  `GetStreamData`, and `PdfPage.Dictionary`, for features the high level API does not cover.
- Drawing: `PdfPage.CreateGraphics()` returns a `PdfGraphics` canvas (top left origin, points)
  for new and existing pages, in front of or behind the existing content. `PdfPath` with lines,
  Bezier curves, arcs, ellipses, polygons and rounded rectangles; `PdfPen`, `PdfColor` (gray, RGB,
  CMYK, opacity), solid, linear and radial gradient brushes; transforms, saved states, clipping.
  Only changed graphics state is written, and shared page resources are never modified.
- Text with the 14 standard fonts: `PdfStandardFont`, `PdfGraphics.DrawString` (fill with any
  brush including gradients, stroke with a pen, or both) and `PdfFont.MeasureString`, with
  ascent, descent, line height, cap height, x-height and underline metrics. Latin text uses
  WinAnsiEncoding; up to 37 further characters per font and document (Central European letters,
  the minus sign) get codes through /Differences. Symbol and ZapfDingbats take their Unicode
  characters. Control and format characters are skipped; a character the font lacks throws.
- Embedded fonts: `PdfOpenTypeFont.Load` reads TrueType and OpenType (CFF) fonts from .ttf,
  .otf, .ttc/.otc collections, WOFF and WOFF2 files. Drawn text embeds a subset of the glyphs
  used as a Type 0 font (CIDFontType2 or CIDFontType0C), with a W array and a ToUnicode map, so
  text is searchable and copyable in any script. Pair kerning from GPOS (formats 1 and 2, extension
  lookups) or the kern table. Line metrics follow browsers (typo metrics when the font sets
  USE_TYPO_METRICS, else hhea). Fonts whose fsType forbids embedding are refused; damaged fonts
  raise `FontFormatException`.
- `PdfFontCollection`: add fonts, files and directories, or use `PdfFontCollection.System` for
  installed fonts; `Find(family, weight, italic)` matches as CSS does. Scanning reads only the
  name, OS/2 and head tables; fonts load on first use.
- WOFF2 fonts: `PdfOpenTypeFont.Load`, `PdfFontCollection` (.woff2 files) and `@font-face` in
  `PdfNetStack.Html` read WOFF 2.0, including transformed glyf, loca and hmtx tables and font
  collections. Table checksums and head.checkSumAdjustment are recomputed. .NET 8 and later
  decompress with the built-in Brotli decoder; .NET Standard 2.0 uses a managed Brotli decoder
  (RFC 7932) with the static dictionary embedded. Lengths, offsets and counts are all checked, the
  decompressed size is capped at 128 MB as for WOFF, and damaged files raise
  `FontFormatException`.
- `PdfFont` gains `StrikeoutPosition` and `StrikeoutThickness`.
- Complex text: `DrawString` and `MeasureString` run the Unicode bidirectional algorithm and an
  OpenType shaper. Arabic and Persian join and take their contextual forms, ligatures form
  (lam-alef, fi), vowel marks sit on their letters, mixed right to left and left to right text is
  ordered correctly with mirrored brackets, and kerning follows GPOS or the kern table as
  HarfBuzz applies them. Shaped and right to left text carries ActualText, so it copies and
  searches in logical order.
- Paragraph layout: `PdfParagraph` (rich text spans of `PdfTextStyle`), `PdfTextLayout` and
  `PdfGraphics.DrawText`. Wrapping at Unicode line break opportunities, start, end, left, right,
  center and justified alignment, right to left paragraphs, fallback fonts per style, letter and
  word spacing, underline and strikethrough, first line indent, CSS line height, soft hyphens,
  tabs, and drawing a range of lines for text split across pages.
- Images: `PdfImage.Load` for JPEG (embedded without recompression, EXIF orientation, Adobe
  CMYK, ICC profiles, JFIF density) and PNG (all color types and bit depths, Adam7 interlacing,
  tRNS as color key or soft mask, alpha channels as soft masks, iCCP, pHYs; simple PNGs keep
  their compressed data with PNG predictors). `PdfGraphics.DrawImage` at natural or given size.
  Each image is written once per document.
- Form XObjects: `PdfForm.Create` for reusable drawings, `PdfForm.FromPage` to place any page of
  any document (rotation honored, resources imported once), `PdfGraphics.DrawForm`, and
  `PdfGraphics.Form` for drawing into a form.
- Annotations: `PdfPage.Annotations` reads every annotation (kind, rectangle, contents, link
  address, destination page including named destinations) and adds web and page links, notes,
  text markup (highlight, underline, strikeout, squiggly) and free text, each with an appearance
  stream. `PdfGraphics.AddLink` maps a canvas rectangle through the current transform.
- `PdfGraphics.BlendMode`: the 16 PDF blend modes, saved and restored with the graphics state.
- Interactive forms: `PdfDocument.Form` lists every field (`PdfTextField`, `PdfCheckBoxField`,
  `PdfRadioButtonField`, `PdfChoiceField`, `PdfPushButtonField`, `PdfSignatureField`) with fully
  qualified names, flags, options and widgets. Setting values regenerates appearances (borders,
  backgrounds, DA font and color, auto size, multiline, comb, password) and drops XFA.
  `PdfAcroForm.Flatten` stamps the appearances into the pages and removes the form.
- Document features: `PdfDocument.Viewer` (page layout, page mode, window options, right to left
  reading, opening page and zoom), `PdfDocument.PageLabels` (roman, decimal and letter ranges
  with prefixes, label lookup), `PdfDocument.Attachments` (embed, read, replace, remove files)
  and `PdfDocument.Language`.
- PDF/A: `PdfSaveOptions.Conformance` writes PDF/A-2b, 2u, 3b or 3u. Problems that cannot be
  repaired are listed together in one exception; the output gets an sRGB output intent with a
  profile the library generates, XMP metadata synchronized with the document information,
  printable annotations, and PDF/A-3 associated file entries for attachments.
- Digital signatures: `PdfSigner.Sign` adds a PAdES B-B signature (ETSI.CAdES.detached, SHA-256,
  384 or 512, RSA or ECDSA, ESS signing certificate v2) in an incremental update, matching the
  file's cross-reference style, optionally with a visible appearance. `PdfSigner.Verify` checks
  each signature's byte range, digest and signature and reports the signer certificate. The CMS
  structure is encoded by the library; no new dependency.
- Packages now include `THIRD-PARTY-NOTICES.md`, which carries Adobe's notice for the standard
  font metrics.

## 1.0.0 (2026-10-28)

First release on NuGet: `PdfNetStack`, `PdfNetStack.Tool` and `PdfNetStack.Azure`.

- JPEG images: lossless re-encoding on by default (`OptimizeJpegImages`). Huffman tables are
  rebuilt for each image, progressive and restart marked JPEGs become baseline, and segments PDF
  readers ignore are dropped. Coefficients are untouched and verification compares them one by one.
- Opt-in JPEG requantization (`JpegQuality`, CLI `--jpeg-quality`): coarser tables at a libjpeg
  quality, never finer than the image's own, applied in the coefficient domain so the color model
  cannot change. Soft masks stay lossless. Verification checks each coefficient is exactly the
  requantized source value. `OptimizationStatistics.JpegImagesRecompressed` and
  `JpegImagesRequantized`; the Azure sidecar records the quality, and lossy blobs carry
  `jpegquality` metadata.
- Document editing API: `PdfDocument.Open`, `Create`, `Save`, `Merge`, `Split`; pages can be
  added, inserted, removed, moved, rotated and imported from other documents with shared
  resources copied once; `Info` for metadata with Unicode and date support; `Outlines` for
  reading, editing and writing bookmarks while keeping their original styling and actions.
- CLI: `merge`, `split`, `extract`, `rotate`, `meta`, `encrypt`, `decrypt`.
- Encryption: open documents protected with any standard security handler revision (RC4, AES-128,
  AES-256), with user or owner password; write AES-256 or AES-128 with permissions; documents
  opened with the owner password keep their protection on save. `PdfDocument.Security`.
- .NET Standard 2.0 target: .NET Framework 4.6.2+, Mono and Unity. Tests run on .NET Framework 4.8.
- Contribution guide with an AI-assisted contribution policy, and a code of conduct.
- Hardening from review: bounded resolution depth, bounded object regions, predictor row
  clamping, identity checks in deduplication and verification, per document decode budget.
- JPEG reader hardening: oversubscribed Huffman tables are refused before use, at most 64 scans
  per image, frames that claim more blocks than their data can code are refused as decompression
  bombs, JFIF and Adobe segments are kept once, and an image with a component no scan covers is
  left untouched.
- Verification accepts requantized coefficients only on the images the optimizer requantized;
  every other JPEG must stay coefficient identical.
- Requantization follows the Adobe marker over `/ColorTransform` when choosing chroma tables, as
  ISO 32000-1 specifies.
- NuGet packages: icon, a readme per package, package validation.

## 0.1.0

First public version.

- Lossless optimizer: unreachable object removal, object streams, compressed xref streams,
  stream recompression, PNG prediction for raw images, iterative deduplication, compact syntax.
- Equivalence verifier on every rewrite, public as `PdfEquivalence.FindDifference`.
- Refuses encrypted, signed, incrementally updated and damaged files unless allowed.
- PDF/A-1 detection with classic xref output.
- `pdfnetstack` command line tool: optimize, batch, inspect, verify.
- `PdfNetStack.Azure`: Blob Storage pipeline with content-addressed output and provenance sidecar.

Source: https://www.jadev-corp.com/pdfnetstack/changelog/
