Repository navigation
Add PDF document rendering support (DemaConsulting.CanvasNet.Pdf) - #31
Merged
Merged
Conversation
Introduces the DemaConsulting.CanvasNet.Pdf package with a stateful, IDisposable PdfDocument API (Open/PageCount/GetPageInfo/Render/Dispose) distinct from the existing stateless codec pattern, per the document vs. image architecture decision. Phase 1 scope: - Tokenizer and internal PDF object model - Classic, cross-reference stream, and object-stream xref parsing with hybrid /XRefStm merging and /Prev chain support - Linear-scan xref recovery fallback for damaged files - /Encrypt detection (fail-closed via UnsupportedImageFeatureException) - Page-tree traversal with /MediaBox and /Rotate inheritance - PdfPageInfo reporting rotation-adjusted dimensions - Render currently returns a blank Surface (rendering lands in later phases) Includes full companion artifacts: design, reqstream, verification, and sysml2 model/views documents, plus README/user-guide updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Render(pageIndex, width, height) now tokenizes and executes the page's /Contents content stream instead of returning a blank Surface. - PdfDocument.ContentStream.cs: content-stream tokenizer/dispatch loop, /Contents single-stream and array-of-streams resolution - PdfDocument.GraphicsState.cs: q/Q graphics-state stack, cm matrix composition, w/J/j/M/d line-style state - PdfDocument.PathOps.cs: path-construction operators (m/l/c/v/y/h/re) baking the CTM into emitted points, and path-painting operators (f/F/f*/S/s/B/B*/b/b*/n) invoking Geometry.PathBuilder and Drawing.PathFiller/PathStroker - Base CTM derivation incorporates MediaBox origin, effective /Rotate, and requested render-size scaling (PdfDocument.PageTree.cs) Phase 2 limitation (documented): all fill/stroke color is fixed to opaque black; no color space, text, image, or clipping operators are implemented yet (silently skipped, not an error). Extends companion artifacts (design/reqstream/verification/sysml2) and adds unit + system-integration tests with concrete pixel assertions, including a rotated-page geometry-placement regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Render now paints real device color (not just fixed black) and composites decoded image XObjects. - PdfDocument.Color.cs: g/G, rg/RG, k/K, cs/CS, sc/SC/scn/SCN device color operators (DeviceGray/DeviceRGB/DeviceCMYK only); operand values are clamped to [0,1] rather than rejected; any other color space (Indexed/Separation/DeviceN/ICCBased/Cal*/Lab) or a /Pattern name fails closed with UnsupportedImageFeatureException - PdfDocument.Filters.cs: generalizes stream decoding into an ordered /Filter + /DecodeParms pipeline; adds PNG (predictors 10-15) and TIFF (predictor 2) predictor reversal reusing PngCodec's approach; any filter other than FlateDecode/DCTDecode fails closed - PdfDocument.Images.cs: /Subtype /Image XObject decoding (DCTDecode via Codecs.JpegCodec.Load; DeviceGray/RGB/CMYK 8-bit raw samples otherwise) and the Do operator, compositing the image's unit square through the current CTM onto the destination surface (nearest- neighbor sampling, treated fully opaque - no SMask/alpha support yet). Form XObjects fail closed rather than being silently skipped. Extends companion artifacts and adds unit + system-integration tests with concrete pixel-color assertions for colored fills and composited image placement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Render now resolves simple embedded TrueType fonts and paints text via the standard PDF text-object/positioning/showing operator set. - PdfDocument.Fonts.cs: /Resources/Font dictionary resolution for /Subtype /TrueType simple fonts requiring an embedded /FontDescriptor/FontFile2 (loaded via Fonts.TrueTypeFont.Load); no font substitution is performed - Type0/Type1/MMType1/Type3 and non-embedded fonts fail closed with UnsupportedImageFeatureException. /Encoding supports /WinAnsiEncoding (default) and /MacRomanEncoding plus /Differences glyph-name overrides; /Widths/FirstChar/LastChar/ MissingWidth resolved with a documented fallback priority. Fonts are cached per resolved font-dictionary identity, scoped to a single Render call. - PdfDocument.Text.cs: BT/ET (resets only the text/line matrices - text state itself persists in the graphics state, including across q/Q), text-state operators (Tc/Tw/Tz/TL/Tf/Tr/Ts - render modes 1/2/4-7 fail closed), positioning operators (Td/TD/Tm/T*), and showing operators (Tj/'/"/TJ). Glyph layout composes the text-rendering matrix (Trm) per PDF spec 9.4.4 and transforms each glyph outline before filling with the current fill color, following the same outline-transform-fill pattern used for SVG text. Extends companion artifacts and adds unit + system-integration tests, including a pixel-level test that independently re-derives expected glyph-ink positions from the shared OpenSans-Regular.ttf font's own metrics rather than hardcoding magic pixel values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extends Fonts.TrueTypeFont in place (no rename) to support CFF/OpenType (OTTO) outlines via a new Type 2 charstring interpreter (CffTable/ CffCharstringInterpreter), and TrueType Collection (ttcf) containers via new Load(stream/path, faceIndex) and GetFaceCount overloads. CID-keyed CFF and seac-style accent composition are explicitly out of scope and fail closed with InvalidDataException. Adds real .otf/.ttc fixtures (Source Sans 3, locally-assembled ttcf container) with license/provenance documentation, unit tests for the CFF interpreter and TTC parsing, and updates companion artifacts (design, reqstream, verification, sysml2, reviewmark). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds TrueTypeFont.GetNameInfo() (family/subfamily/full/PostScript name strings from the sfnt 'name' table, preferring typographic nameID 16/17 over 1/2, Windows-Unicode platform records over Macintosh-Roman) and IsBold/IsItalic/IsFixedPitch properties (derived from OS/2 fsSelection/ usWeightClass, head.macStyle, and post.isFixedPitch/italicAngle). New internal NameTable/StyleTable parsers follow the existing per-table helper convention; both tables are optional per spec and degrade gracefully (never throw) when absent or too short for a given field. Purely additive - no existing TrueTypeFont member changed. Adds unit tests plus real-fixture integration tests confirming actual Open Sans/ Source Sans 3/TTC metadata values, and updates companion artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds Fonts.SystemFontCatalog (core package): lazy, process-lifetime cached, directory-scan-only discovery of installed fonts across Windows/macOS/Linux well-known font directories, tolerant of missing directories and unparseable files; FindBestMatch(familyNameHint, bold, italic, serif, fixedPitch) two-tier (exact family, then generic sans/serif/monospace) scoring; and LoadBundledFallback, backed by 12 bundled Liberation Sans/Serif/Mono TrueType fonts (SIL OFL 1.1, ~4.16 MB, shipped as embedded resources in the core NuGet package with the OFL license text also packed at licenses/LiberationFonts-OFL.txt). Rewrites PdfDocument.BuildResolvedFont's no-/FontFile2 branch (PdfDocument.FontFallback.cs): Standard-14 name -> fixed style mapping, /FontDescriptor flag/weight/angle-based flavor classification for non-Standard-14 names, system-catalog match first, then bundled Liberation fallback. Symbol/ZapfDingbats and other symbolic fonts remain fail-closed (no substitution) since they have no generic-family equivalent. Fallback is fully automatic/silent - no diagnostics API, no new Render parameter. Adds unit tests for SystemFontCatalog scoring/matching and bundled-font dogfooding, PDF integration tests for Standard-14 fallback rendering and Symbol/ZapfDingbats fail-closed behavior, and updates companion artifacts (design, reqstream, verification, sysml2, reviewmark). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the final four stream filters: LZWDecode (PDF variant per ISO 32000-1/2 section 7.4.4 - MSB-first 9-12 bit variable-width codes, early-change growth timing per /DecodeParms /EarlyChange, independent from-scratch implementation distinct from GifCodec's LZW), ASCII85Decode (base-85 groups, z shorthand, final-group padding), ASCIIHexDecode (hex-pair decoding with whitespace tolerance and odd-digit padding), and RunLengthDecode (PackBits-style literal/repeat runs). All four are wired into GetStreamDecodedBytes's filter dispatch; LZWDecode reuses the existing predictor-reversal path shared with FlateDecode. Adds ~24 unit tests (including the ISO 32000-1 section 7.4.4 LZWDecode worked example) plus 4 end-to-end integration tests with fixture PDFs, and updates companion artifacts (design, reqstream, verification, sysml2, reviewmark). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a small hand-authored PDF fixture corpus (honestly labeled as synthetic - no suitable, responsibly-licensed real-world corpus could be sourced): combined-vector-text-image.pdf (breadth: vector shape + image XObject + embedded-font text), standard14-font-fallback.pdf (the first fixture exercising the Phase 6 non-embedded-font fallback path), and malformed-content-stream.pdf (a dedicated malformed-data scenario). Adds PdfFixtureTests.cs (fixture-conformance tests mirroring SvgFixtureTests.cs's rigor) and extends PdfSystemIntegrationTests.cs with the previously-missing empty-path, malformed-data, and out-of-range-page-index validation-contract scenarios. Brings the PDF system design/verification docs and README/introduction docs current through Phases 5-7 (previously still described Phase 4), and finalizes .reviewmark.yaml coverage for the PDF package's complete file set. This is the final planned phase of the feature/pdf-document-support roadmap - no production rendering-logic changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phases 5/5b/6 of this branch added correct unit-level requirements for the Fonts subsystem's CFF/OTF, TTC, name/style metadata, and OS font discovery/fallback capabilities, but no system-level requirement decomposed down to them, leaving them unreachable from any system-tagged root requirement. Adds 4 system-level requirements to canvas-net.yaml's Fonts API section (CanvasNet-Lib-FontsCffOutlineDecoding/TtcSupport/NameStyleMetadata/ SystemFontDiscovery), each linking to a new system-integration test in CanvasNetTests.cs exercising the corresponding capability through the public API end-to-end, plus matching verification test-scenario entries. dotnet reqstream --enforce now reports 0 orphaned requirements (was 43). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Real-world PDF testing surfaced a genuine parser bug: content streams
containing an inline dictionary operand (e.g. the BDC/DP marked-content
operators' properties dictionary, as commonly emitted by tagged-PDF
producers like Word/LibreOffice/browser print-to-PDF) threw
InvalidDataException("Unexpected token 'DictStart'") instead of parsing
the dictionary and continuing, since ExecuteContentStream's operand loop
only special-cased ArrayStart, not DictStart.
Reuse the existing generic ParseValue/ParseDictionaryOrStream parser
(already used for regular indirect objects) for DictStart tokens, exactly
as already done for ArrayStart. Added a regression test and a cspell
entry for the newly-referenced MCID term.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… encoding Phase 9 of the Type0/CID font roadmap. Adds IResolvedFont abstraction over simple (TrueType) and composite (Type0) font resolution, CIDFontType2 CIDToGIDMap handling (Identity or embedded stream), /DW+/W width resolution, and 2-byte Identity-H code decoding in ShowText/ShowGlyph. Also fixes a spec-correctness bug found during review: word spacing (Tw) must apply only to a simple font's single-byte code 32, never to a composite font's 2-byte code even when it numerically equals 32 (PDF spec section 9.3.3). Gated the ShowGlyph word-spacing check on CodeByteWidth == 1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds ResolveToUnicodeMap, a narrow bfchar/bfrange-only CMap parser reusing the existing content-stream tokenizer/parser primitives. Resolves a Type0 font's optional /ToUnicode entry into a code-to-Unicode-codepoint map stored on ResolvedCompositeFont.ToUnicode - groundwork only, not yet consumed at rendering time. Fails closed on usecmap/cidrange/cidchar operators and the CIDSystemInfo- style array-of-arrays bfrange destination sub-form; silently ignores the mandatory Adobe CMap PostScript wrapper boilerplate and begincodespacerange/endcodespacerange, which carry no mapping-relevant data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ype CFF) Phase 12 of the composite-font roadmap. Adds a /CIDFontType0 descendant- font dispatch branch alongside the existing /CIDFontType2 path: loads a non-CID-keyed CFF program embedded via an /OpenType-wrapped /FontFile3, using identity CID-to-glyph-index per PDF 32000-1 section 9.7.4.2 (ignoring any non-standard /CIDToGIDMap entry on the descendant, which is not a valid key for this subtype). No Fonts-subsystem changes needed - Fonts.TrueTypeFont.Load already parses these OpenType/CFF containers unmodified; only unsupported/CID-keyed CFF (ROS/FDArray/FDSelect) still fails closed via the existing CffTable.Parse rejection, surfacing as InvalidDataException. A bare/naked /CIDFontType0C CFF stream (no SFNT wrapper) and general CID-keyed CFF support (a separately-scoped future phase) remain explicit non-goals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… Phase 1) Adds SampledFunction/ResolveFunction: restricted to /FunctionType 0 (sampled), 1 input, 8/16-bit samples, with clamped-/Domain, /Encode sample-index mapping, linear interpolation between adjacent samples, and /Decode-mapped, /Range-clipped output. Groundwork only - not yet wired into any rendering path (no /Pattern color-space plumbing yet). Rejects /FunctionType 2/3/4 and multi-input Type 0 functions (the /DeviceN//Separation tint-transform shape, already out of scope) with UnsupportedImageFeatureException. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ping Real-world producers (e.g. WeasyPrint) emit an empty hex-string destination (<>) to mean 'this code has no single-character Unicode equivalent'. The Phase 10 parser treated this as malformed and threw InvalidDataException, which propagated out of BuildResolvedCompositeFont and aborted the whole font resolution - blocking rendering entirely for an otherwise-renderable page, even though /ToUnicode is diagnostic-only groundwork not yet consumed by rendering. Skip mapping the affected code(s) instead of failing closed; a non-empty destination with an odd byte count still throws, since that remains genuine corruption. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per PDF 32000-1:2008 section 8.5.2.1, 'If the current subpath is already closed, h shall do nothing.' The interpreter previously threw InvalidDataException whenever 'h' was issued without an open subpath, including the extremely common 're h' idiom (a redundant explicit close right after 're', which already self-closes its rectangle) - real-world producers such as MuPDF/fitz emit this routinely. l/c/v/y still correctly require an open subpath, since drawing without a preceding move is genuinely invalid. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds general-purpose Type 1 font loading to the core DemaConsulting.CanvasNet Fonts library, available to every consumer (Pdf, Svg, Pptx, Vsdx, or any future caller of TrueTypeFont), not just PDF rendering: - Type1CharstringDecryption: the eexec/charstring LCG decryption cipher. - Type1Table: parses the eexec-decrypted private dictionary's /Subrs and /CharStrings (name-keyed, token-agnostic RD/ND/NP scanner), assigns glyph indices (.notdef -> GID 0), exposes outlines/advance widths/name lookup. - Type1CharstringInterpreter: the full Type 1 charstring opcode set, including real flex geometry (OtherSubrs 0/1/2 - converts the seven buffered rmoveto points into two genuine cubic Bezier segments, not a hinting no-op) and hint-replacement pass-through (OtherSubrs 3). seac (accented composites) is a documented Non-Goal, rejected explicitly. - TrueTypeFont.LoadType1(stream, length1, length2, codepointToGlyphName): new public entry point for embedding formats (like PDF's /FontFile) that supply segment lengths and an authoritative encoding out-of-band. - TrueTypeFont.Load(stream)/Load(string) now also auto-detect standalone .pfb (binary segment-framed) and .pfa (hex-encoded) Type 1 font files with zero extra parameters, exactly like existing TrueType/OpenType/CFF support - via new Type1PfbReader/Type1PfaReader plus a core-owned Type1StandardGlyphNames default encoding table. PDF-project integration (dispatching /Subtype /Type1 font dictionaries to this new API, plus /StandardEncoding support) is a separate, follow-up phase - this commit only touches the core Fonts library and its own tests/ requirements/design/verification docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Integrates the core Type1 font support (previous commit) into PDF font resolution: - /Subtype /Type1 now dispatches through BuildResolvedSimpleFont alongside /TrueType, with three cases per font descriptor: /FontFile2 (unchanged embedded TrueType path), /FontFile (new - embedded classic PostScript Type 1 program, via new PdfDocument.Fonts.Type1.cs delegating to Fonts.TrueTypeFont.LoadType1), neither (existing non-embedded Standard-14 fallback path, now reachable for Type1 too). /FontFile3 on a /Type1 descriptor (embedded Type1C/CFF as a simple font) remains explicitly rejected via UnsupportedImageFeatureException, distinct from the already-supported CIDFontType0 composite CFF path. - Adds /StandardEncoding as a third recognized named base encoding (alongside /WinAnsiEncoding/MacRomanEncoding), backed by the Adobe Standard Encoding table, needed by many real-world Type 1 fonts/PDF producers. - Two new hand-authored fixtures: an embedded Type 1 font (/FontFile) and a non-embedded Standard-14 Type1 fallback. /MMType1 and /Type3 subtypes remain rejected (out of scope). seac (accented composites) and /FontFile3-on-/Type1 remain documented Non-Goals, consistent with the core library's own scope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Type1Table's lightweight advance-width peek (used eagerly at Parse time, without running the full charstring interpreter) required a glyph's charstring to literally begin with 'hsbw'/'sbw'. Real-world Type 1 fonts (found via a pdfLaTeX Computer Modern font from py-pdf/sample-files) commonly encode a fractional side-bearing/width - which plain Type 1 integers cannot represent - as '<numerator> <denominator> div' immediately before hsbw/sbw (for example '0 20225 61 div hsbw' for a width of 331.56...), which the peek rejected outright. The peek now also recognizes and evaluates a leading div (escape 12 12) operator, matching the full Type1CharstringInterpreter's own div handling, while remaining deliberately narrow (no subroutine calls or other operators) so that a font containing other, unrelated unsupported glyphs (for example seac-based accented composites) can still load successfully as long as those glyphs are never actually rendered. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lback Per PDF 32000-1 section 9.6.2.2, /FontDescriptor is optional for the standard 14 fonts (and several real-world producers, e.g. ReportLab, omit it entirely). BuildResolvedSimpleFont previously required /FontDescriptor unconditionally, throwing InvalidDataException for these otherwise perfectly valid font dictionaries instead of resolving them via the existing ResolveFallbackFont substitution path, consistent with this method's already-lenient 'substitute automatically' policy for any other non-embedded font. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hNames The Adobe-glyph-name table used to resolve /Encoding/Differences glyph names only recognized 'fi' and 'fl'; the 'ff', 'ffi', and 'ffl' Latin ligatures were missing, causing a real-world pdfLaTeX Computer Modern font (which declares these in its own /Differences array) to throw InvalidDataException. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 'nacute' (Polish/Czech 'n-acute') Adobe glyph name was missing from StandardGlyphNames, causing a real-world pdfLaTeX Computer Modern font (which declares it in its own /Differences array) to throw InvalidDataException. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements /Subtype /Form XObject execution: decodes the Form's content stream, concatenates its optional /Matrix into the current CTM, resolves its own /Resources (falling back to the invoking stream's resources when absent), and recursively executes the nested content implicitly bracketed like q/Q around the invoking stream's graphics state. Guards against pathological/self-referencing nesting with a depth limit of 12. /BBox clipping and /Group transparency groups remain out of scope for this phase, matching the existing lack of clipping-path support elsewhere in the interpreter. Verified against the py-pdf/sample-files corpus and a real-world 26-page document previously blocked solely by this gap (now renders all pages without error). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the enum-only PdfColorSpaceKind representation with a PdfColorSpace class carrying per-instance data, enabling: - /ICCBased: resolved via /Alternate when supported, else via /N (1/3/4 -> DeviceGray/DeviceRGB/DeviceCMYK). - /Indexed: a 1-component palette lookup over any supported base color space (device spaces or /ICCBased), with out-of-range indices clamped rather than rejected. Usable both for image XObjects and for cs/sc/scn painting operators. Full ICC profile color management and non-device-mappable bases (e.g. /Separation under /Indexed) remain out of scope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements a from-scratch ITU-T T.6 Group 4 (Modified Modified READ) two-dimensional decoder for CCITTFaxDecode image XObjects: full White/Black Modified Huffman run-length code tables, the nine two-dimensional mode codes (Pass/Horizontal/V0/VR1-3/VL1-3), /K, /Columns, /Rows, /BlackIs1, and /EncodedByteAlign support. Decoded samples route through the existing /ColorSpace pipeline. Group 3 (/K >= 0) and explicit /EndOfLine markers are out of scope for this phase and fail closed with UnsupportedImageFeatureException. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds CFF Charset (Top DICT operator 15) parsing to CffTable - the full 391-entry Standard Strings table plus custom charset formats 0/1/2 and the predefined ISOAdobe default - enabling glyph-name-to- glyph-index resolution (CffTable.TryGetGlyphIndex), previously only possible for classic Type 1 (/FontFile) fonts via Type1Table. Also surfaces proper CFF advance widths via Private DICT defaultWidthX/nominalWidthX. Wires this into PdfDocument.Fonts.cs: a /Subtype /Type1 font whose /FontDescriptor declares only /FontFile3 (/Subtype /Type1C, a bare CFF stream with no SFNT wrapper) now loads via the new TrueTypeFont.LoadType1C, instead of unconditionally throwing UnsupportedImageFeatureException. Predefined Expert/ExpertSubset charsets remain a documented scope boundary (glyphs silently unmapped, not thrown, matching Type1Table's existing behavior for unmapped glyph names). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a ResolvedType3Font (IResolvedFont) resolving /FontMatrix, /Encoding (via /Differences), /CharProcs, /Resources, and /Widths from a /Subtype /Type3 font dictionary. Each glyph is painted by re-entering ExecuteOperators against its own /CharProcs content stream, mirroring OpDrawFormXObject's save/restore/recurse pattern, with the glyph-space-to-device matrix computed as FontMatrix * ComputeTextRenderingMatrix(). Advance widths are transformed through /FontMatrix as a vector (Vector2.TransformNormal) rather than the fixed /1000 convention other font kinds use. IResolvedFont.Font is now nullable to accommodate Type3 fonts, which have no outline-glyph font program; ShowGlyph branches on the concrete font type before ever touching it. The d0/d1 glyph- description operators are recognized and their operands parsed and discarded (both are no-ops outside a Type3 glyph proc); /Widths remains the authoritative width source for text layout. A dedicated nesting-depth guard bounds recursive Type3-glyph-proc invocation. True d1 in-glyph color-operator suppression is a documented scope boundary: a glyph proc's own color-setting operators are not forcibly overridden. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A malformed/adversarial PDF with a /Resources/ColorSpace entry that resolves back to itself (directly, or via a cycle of several names) caused ResolveColorSpaceByName/ResolveColorSpaceValue to recurse until the process' call stack was exhausted, raising an unrecoverable StackOverflowException that crashed the whole process instead of failing just that document's open/render call. Found via a batch-rendering survey against the py-pdf/sample-files, pdf-association/pdf20examples, sambitdash/PDFTest and luigigubello/PayloadsAllThePDFs corpora (GeoTopo-komprimiert.pdf). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Several real-world PDF producers emit a wrong/placeholder Adler-32 trailer over an otherwise well-formed DEFLATE payload; mainstream PDF readers tolerate this. ZlibDecompress no longer validates the trailer, matching that ecosystem tolerance instead of rejecting an otherwise perfectly decodable stream. Found via a batch-rendering survey against public real-world PDF test corpora (141 occurrences in one corpus alone). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A /W [0 0 0] entry-width table produces a zero per-entry stride, so the decode loop in ParseXrefStreamSection never advances position. With a crafted large /Size or /Index range this can spin for an unbounded number of iterations without making progress or hitting the truncation check (which also degenerates to position + 0). Reject a zero total entry width before iterating so malformed cross-reference streams fail closed instead of hanging. Adds a regression test exercising /W [0 0 0] via the existing crafted xref-stream-with-custom-widths test helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetObject only guarded against resolution cycles reached via the compressed-object-stream path (LoadCompressedObject). A cycle reachable through a mix of direct and compressed resolution - for example an object-stream container whose own /Length is an indirect reference to a compressed object living inside that same container - was not guarded, recursing indefinitely. Renamed the per-document resolution-tracking set from _compressedObjectResolutionStack to _objectResolutionStack and moved the cycle guard in GetObject to cover both the direct and compressed branches, so any repeat entry into GetObject for an object number still being resolved now fails closed with InvalidDataException. Adds a regression test building a self-referential xref-stream-only PDF whose container's /Length indirectly references a compressed object living inside itself, exercising this cross-branch cycle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LoadCompressedObject's header-reading loop trusted a crafted /N even after the decoded object-stream header had no real content left, calling NextToken() up to /N times regardless of the stream's actual size. A tiny decoded stream paired with a huge /N made this spin through a correspondingly huge number of EndOfFile tokens before failing, a CPU-time denial-of-service proportional to the attacker's chosen /N rather than the stream's real size. Added an upfront check rejecting /N whenever it exceeds the decoded byte length (each header entry needs at least one byte), plus an explicit in-loop check that stops and throws as soon as the header tokenizer has no bytes left, rather than relying on the eventual malformed-token check to notice. Adds a regression test building an object stream that declares /N as one million while its decoded content holds a single real entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Added an upfront check rejecting a declared classic cross-reference subsection entry count that exceeds the real bytes remaining in the buffer, plus an in-loop EOF guard before each entry triplet is read. Exposed PdfTokenizer.BufferLength to support the bound check. Added a regression test building a classic xref table whose subsection header declares one million entries with no real entry bytes following it, expecting InvalidDataException instead of a CPU-time denial of service. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reordered the constructor so InitializeEncryption runs immediately after cross-reference parsing succeeds, before IsValidCatalogRoot resolves /Root. Previously, when the catalog (or an ancestor page-tree object) lived inside a compressed, encrypted /Type /ObjStm object stream, resolving it before the file key existed decompressed ciphertext as if it were plaintext, spuriously failed, and incorrectly triggered the linear-scan fallback even though the document was otherwise well-formed. The fallback path (xref parsing failure, malformed /Encrypt dictionary, or a genuinely invalid catalog) is unaffected: InitializeEncryption is still called again once a fallback trailer is recovered, matching the prior behavior and exception-propagation semantics exactly. Updated the stale cache-invalidation comments that assumed IsValidCatalogRoot's own /Root lookup as a pre-key caching source in the normal path. Added a regression test building an encrypted document whose /Type /Catalog object is itself compressed inside an encrypted /Type /ObjStm container, proving it now opens and renders correctly instead of falling back to a linear scan. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EnumerateFontFiles now skips any subdirectory marked as a reparse point (a symlink or junction) entirely, and tracks the canonicalized path of every other subdirectory queued or visited in a HashSet, so a cycle - for example a symlink pointing back at an ancestor directory - cannot make the scan loop indefinitely. Added a regression test that creates a directory symlink cycling back to its own ancestor alongside a real font file, proving the scan terminates and still discovers the sibling font. The test skips itself when the current process lacks the privilege to create a directory symlink (environment-dependent, for example Windows without Developer Mode or elevation). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixed PNG predictors (10-14) apply a single filter uniformly to every row with no per-row filter-tag byte, unlike adaptive predictor 15 which has a tag byte per row. ApplyPngPredictor previously assumed the adaptive layout unconditionally, corrupting fixed-predictor image data. Also validate that data length is an exact multiple of the row stride, throwing InvalidDataException instead of silently truncating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ParseValue/ParseArray/ParseDictionaryEntries/ParseDictionaryOrStream recursed without a depth limit, allowing a maliciously crafted PDF with deeply nested arrays/dictionaries to trigger an uncatchable StackOverflowException. Thread a depth counter through these methods and throw InvalidDataException once MaxObjectNestingDepth (64) is exceeded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PdfTokenizer only guards against Position >= Length; a negative Position falls through to direct buffer indexing and throws an uncatchable IndexOutOfRangeException. A malformed startxref value, /Prev or /XRefStm link, classic-xref entry offset, or compressed object's computed First+relative offset could all reach this path unchecked. Add ValidateBufferOffset and apply it at every offset derived from untrusted PDF input before any Position assignment, and compute compressed-object offsets using long arithmetic to avoid Int32 overflow/wraparound. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When primary xref/trailer parsing fails and PdfDocument falls back to a linear scan, InitializeEncryption is re-run against whatever trailer the fallback recovers. If that trailer has no /Encrypt entry, the previously computed encryption key and cipher were left set from the first (failed) attempt, so plaintext content streams recovered by the fallback would be incorrectly decrypted. Reset _encryptionKey/_encryptionCipher before re-initializing encryption in the fallback path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Pull Request
Description
Adds PDF document rendering support to CanvasNet via a new
DemaConsulting.CanvasNet.Pdfassembly (
PdfDocument). This covers PDF parsing (xref tables and cross-reference streams,page tree, content streams), rendering of vector paths, text (TrueType, Type 1, Type 1C, Type 3,
Type 0/CID-keyed fonts, with OS/bundled-font fallback including Symbol/ZapfDingbats), raster
images (JPEG/DCTDecode, CCITT G4 fax, PNG-predictor-filtered raw samples), device/CalRGB/ICC/
Indexed/Pattern color spaces, axial/radial shading and tiling pattern fills, Form XObjects, and
optional
/Standardsecurity-handler decryption (RC4, AES-128, AES-256). It also adds aTilePaintpaint type and aPdfDocument.Render(int, float dpi)convenience overload to the coreDemaConsulting.CanvasNetlibrary'sDrawingsubsystem.This branch also incorporates a full pre-merge quality pass: 16 formal reviews were run against
requirements/design/verification documentation and the
CanvasNetPdf-PdfDocumentimplementation;all findings from that pass have been remediated, including:
ordering, an object-stream reference-cycle guard) plus 4 Medium-severity robustness fixes, each with
a regression test
docs/design/introduction.md(folder-layout trees, prose, Xref/FontFallback claims) andseveral unit-level design docs that had self-contradictory "sole software unit" claims
after document splits
SystemFontCatalogstale bundled-font count, a new requirement + verification section for theRender(int, float dpi)overload, and two new genuine system/integration tests (a real FormXObject render and a real RC4-encrypted-PDF decrypt+render) replacing two previously mis-cited
test links
Type of Change
Related Issues
Closes #
Pre-Submission Checklist
Build and Test
pwsh ./build.ps1— Build succeeded, 0 Warning(s), 0 Error(s); 7218/7218 tests passedCode Quality
Quality Checks
pwsh ./lint.ps1— passed (YAML, cspell, markdownlint, ReqStream, VersionMark, ReviewMark, SysML2 Tools,dotnet format --verify-no-changes)Testing
Documentation
Additional Notes
dotnet reqstream --requirements requirements.yaml --tests "artifacts/**/*.trx" --report ... --matrix ...runs clean with no unsatisfied/orphaned-requirement errors against this branch's HEAD.
16 formal reviews were re-run against this branch after the doc-sync/hardening pass described above;
a further round of re-review is planned immediately after this PR is opened to confirm all review-sets
now PASS before requesting merge.