mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 10:50:14 +00:00
sonar-fix/enforce-https
39 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8b88a2d14b | M2: fix shading bounds in Type 3 glyphs (#3178) (#3412) | ||
|
|
12fcc64115 |
Tests: make timing-dependent render and UI tests deterministic (#2812)
Seven tests failed at random on slow or busy machines, main included
(FreeBSD run 38834256439 at
|
||
|
|
f65f733041 |
fix(render): faster sampled functions, image decodes across zoom levels and patch meshes (vendored hayro-interpret patches 31–33) (#2436)
* fix(render): sampled functions index a flat sample array (vendored hayro-interpret patch 31) A Type 0 sampled function kept its samples in a HashMap keyed by each point of its /Size grid (two SmallVecs per key, SipHash), and the renderer builds a shading's functions again every time it is painted, so every paint hashed one insert per sample. A 2,097,152-sample ramp painted 30 times took 38 s; the pdf.js corpus's bug1721218 (InDesign) spent most of a 1.5 s page on it. The samples now stay in stream order in one Vec, and a grid point is found by strides (first input fastest). The interpolation keeps the same recursion and arithmetic, so results are bit for bit the same, and a point the stream ended before still gives no value. The repeated-paint page draws in well under a second. The maximum sample value no longer computes 2^32 in a u32 (a panic with overflow checks on at 32 bits per sample). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ * fix(render): one decode of an image serves every size it is drawn at (vendored hayro-interpret patch 32) The decoded-image cache (patch 24) keyed each decode by the size the image is drawn at, but only the JPX filter decodes a reduced resolution for a size: every other filter decodes the whole image. So a JPEG or Flate scan missed the cache at every zoom step, was decoded again, and was kept once per zoom level. The key now carries the target size only when JPX decodes the image, its /SMask or its /Mask (read from /Filter or /F, erring towards keeping the target). `InterpreterCache::decoded_image_decodes` counts the cache's decodes; the new test draws a Flate image at five sizes and sees one decode, and a copy whose soft mask is JPX-encoded still decodes per size. Both assertions fail with the old keying. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ * fix(render): patch meshes copy their vertices' colours less (vendored hayro-interpret patch 33) A Coons or tensor-product patch is drawn as 722 triangles over a 20 x 20 grid of points, and every vertex's colours (a SmallVec) were produced or copied up to five times: each grid cell interpolated its four corners itself (1,444 interpolations a patch for 400 points), cloned all six vertices of its two triangles, and the mesh sampler cloned each vertex again to move the triangle into device space. Profiling page 8 of the annual report in #1584 put a quarter of its time in those clones. Each grid point's colours are now interpolated once, a cell's second triangle takes the vertices its first copied, and the sampler moves a patch's freshly made triangles instead of copying them (`TriangleSampler::sample_owned`; a free-form triangle mesh, which the shading keeps, is still copied). The same inputs give the same values: rendering the first three pages of every pdf.js corpus file is byte for byte the same. Release build, Linux: report page 8 773 -> 696 ms, personwithdog.pdf 149 -> 129 ms, issue13520.pdf 104 -> 87 ms. The new test pins the colours near each corner, at the centre and between them for a Coons and a tensor-product patch (taken before the change); swapping two vertices' grid points makes it fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ * docs: roadmap entries for faster sampled functions, image decodes and patch meshes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
13e3154ab3 |
fix(fonts): read the Annex D.2 alias codes as the ink they draw
#2305 fixed WinAnsi 0xAD in `crates/fonts`, and closed with two loose ends it named: the user-visible extraction path goes through hayro, not that table, so it still reported the invisible character; and the same Annex D.2 note makes 0xA0 the `space` glyph. This finishes both, and makes the two tables in the product agree about the same byte. WinAnsiEncoding gives codes 0xA0 and 0xAD the glyph names `space` and `hyphen` -- the names it also gives 0x20 and 0x2D -- and MacRomanEncoding gives 0xCA `space`. The tables called them `nbspace` and `sfthyphen`, whose Adobe Glyph List values U+00A0 and U+00AD are invisible formatting characters, so extraction hid ink the reader can see: "well-known" drawn with the 0xAD byte matched no search and no redaction pattern, and a non-breaking space did not match the space a reader typed. Evidence. Two sources transcribe the table the same way (an Annex D implementation's docs, and the WinAnsi.enc encoding vector R ships, which gives `/space` at octal 240 and `/hyphen` at 255), and a black-box check settles the rest: on a synthetic fixture pdftotext reads the base-encoded codes as `a-b a b`, and keeps U+00AD for a code an explicit `/Differences` names `sfthyphen`. We now do both. Over the 989-file pdf.js corpus the soft hyphens we extract fall from 117 to 13 and the non-breaking spaces from 30 to 26; everything left comes from `/ToUnicode` maps, which say what they mean and are not touched. Scope. The base table only, in the text direction only. - A `/Differences` entry naming `sfthyphen` or `nbspace` is a deliberate statement about one code and keeps its glyph list value, as pdftotext does. - Glyph lookup is untouched, so a font program's own `sfthyphen` program is still found first. In hayro that is why the alias sits in the Unicode path rather than in the generated encoding table: `code_to_ps_name` is shared with lookup. - Writing is unaffected: `encode` takes the lowest code showing a character, so ' ' and '-' still write 0x20 and 0x2D, and a caller holding a literal U+00A0 still writes 0xA0. Tests. `fonts` pins both tables and the whole decode/encode path including the `/Differences` case; `render` pins the extraction a reader actually gets, over WinAnsi, MacRoman and `/Differences`. Each was watched failing with each cell and each match arm reverted in turn, so no assertion is passing by accident. fonts 70, render 296+4, redact 49, a11y 43 and edit 156 are green, with fmt, clippy, `xtask layers` (37 crates) and `xtask assets` clean. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ab45a1855f |
Keep annotations readable under reading colours (#1548) (#1557)
* fix(render): keep annotations readable under reading colours (#1548) Reading colours remapped annotation paints as line art: the yellow highlight fill is luminance-light, so it was mapped toward the dark background, and Multiply then darkened the remapped-white words under it into invisibility. Two changes in hayro: - The /Annots loop in interpret_page is bracketed by a new begin_annotations/end_annotations Device signal (patch 26), and the renderer's reading-colours mapping leaves annotation paints alone. Reading colours replace document colours, never the colours a comment was given — the Acrobat model. - The page background is painted inside the page transparency group (patch 27), as the initial backdrop, so a Multiply highlight blends with the background too. With the background outside the group the band kept its own flat colour and the words vanished into it. White pages are unaffected: multiplying with white is the identity. Regression test reading_colors_keep_highlight_annotations_readable covers both annotation paths (/AP and the no-/AP /QuadPoints fallback) under WHITE_ON_BLACK; parity entry a11y.replace-page-colors updated with the behaviour and evidence. Co-Authored-By: GLM <noreply@z.ai> * contributors: credit @edurbs as Eduardo Soares The contributor's own entry, chosen by them (real and display name). Co-Authored-By: GLM <noreply@z.ai> * #1557: record the vendored hayro patches in vendor/README.md Main already uses hayro-interpret patches (26)-(28), so the annotation hooks become hayro-interpret (29) and the renderer's parts hayro (17) and (18). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: GLM <noreply@z.ai> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
15d82b5b9c |
fix(render): a Multiply highlight inside a transparency group keeps the text (#1968) (#2027)
Highlight appearances written by other editors are a transparency group whose content is filled with `/BM /Multiply`. Every `/Group` form got a layer of its own, so the Multiply blended with that layer's transparent backdrop and the layer then covered the page as an opaque block. Vendored hayro-interpret patch (28): a group that is neither isolated nor knockout composites its elements with the backdrop (ISO 32000-2 §11.4.7), so when it is entered at full opacity with no soft mask and Normal blending there is nothing for a layer to add, and its content is drawn straight onto the page. `/I` and `/K` are read from the group dictionary. An isolated group keeps its blank backdrop; a knockout group or one entered at partial opacity, under a soft mask or with a blend mode keeps its layer. Test: `raster::tests::a_multiply_highlight_inside_a_transparency_group_keeps_the_text` (the appearance drawn as an annotation and from page content, with `/I false /K false` spelled out and with no group at all; an isolated group as a control; a group entered at half opacity still composited as one). It fails before the fix with the text pixel yellow instead of black. parity: view.render-transparency cites it and says what is still approximated. Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
9c875656de |
fix(render, ui): form-heavy documents draw and fill fast (spot-colour images, widget placement, Fields panel) (#1691)
* fix(render): convert spot-colour images once per pixel value (vendored hayro-interpret patch 28) An image whose pixels hold at most 16 bits of samples (an 8-bit Separation or DeviceN image, a 1-, 2- or 4-bit gray or indexed one) is now converted to RGB through a table of every possible pixel value instead of pixel by pixel. A Separation or DeviceN tint transform is a PDF function, often a PostScript calculator program, and the image decoder ran it once per pixel. Fillable character sheets draw their background art as an 8-bit DeviceN image: one with a 3304 x 4105 background took 3.8 s and 430 MB to draw each page (Poppler: 0.27 s), when opening it and again after every field was filled. It now draws in 0.11 s and 100 MB, pixel for pixel the same. The table decodes each sample through the same `decode_sample` as the per-pixel path, keeps `fix_image_length`'s rules for short and overlong data (the pixels past the data are converted as before), and is used only when the image has more pixels than the table has entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8 * fix(render): place widgets without /P in one pass over the pages Opening a document, the inspector finds the page of a form field whose widget has no /P by searching every page's /Annots for it, once per field: O(fields x annotations). A form of 8,000 such fields took 1.17 s to open instead of 0.27 s. The pages' /Annots are now read once, on the first such field, into a map from annotation to page. A widget that several pages list is now placed on the first of them, as pdfcraft-forms places it; the search took whichever page a HashMap visited first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8 * fix(ui): the Fields panel keeps up with large forms Every frame the Fields panel searched the form for each row by name, once to sort the rows into tab order and again to find what a click selects: O(fields²) a frame. It also laid out, drew and built the tooltip of every row, scrolled into view or not. With 8,000 fields the app used 13.4 s of CPU for 20 pointer moves with the panel open (0.2 s with it closed). The rows are now worked out by `field_rows`, which pairs each row with the first form field of its name through one map a frame, as the engine pairs them, and a row out of view is allocated and keeps its clicks, keyboard focus and accessibility label but isn't drawn and builds no tooltip. The same 20 moves now take 0.86 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8 * docs: roadmap entries for faster form-heavy documents Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8 * fix(render): convert each lookup-table entry on its own (vendored hayro-interpret patch 28) Patch 28 converted the whole table of possible pixel values in one batch. Separation and DeviceN `convert_f32` concatenate each tint result, and a Type 4 program that returns the wrong number of components is only warned about, so a malformed result for a value the image never uses shifted every later entry. A 32 x 32 image whose only sample (255) is red drew black because tint 0 returned one component (PR #1691 review). `convert_each` now converts the table one entry at a time, so a malformed entry changes only itself. The largest table (65,536 entries, a two-ink DeviceN image over DeviceCMYK) takes about 2 ms longer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
685fd7eeea |
fix(text): read a TrueType font without an encoding as WinAnsi (#1477)
* fix(text): read a TrueType font without an encoding as WinAnsi (fixes #1468) * fix(text): describe the WinAnsi reading by pdftotext's output --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
0d300c6a41 | fix(render): Type 3 text ignores rendering modes other than invisible (vendored hayro-interpret patch 27) (fixes #1469) (#1478) | ||
|
|
fcc1d9ead0 |
M2: adapt Lab Bradford white points in LMS (#1417)
Follow up #1237 by deriving Bradford scaling ratios from the source and D65 white points in LMS. Preserve CalRGB and the merged Indexed/Range handling. Add independent neutral/chromatic, identity, asymmetric Range, and degenerate-cone render regressions. Co-authored-by: Codex <noreply@openai.com> |
||
|
|
a259af95c3 |
fix(render): draw Turkish Ğ İ Ş ğ ş in non-embedded fonts (vendored hayro-interpret patch 26) (#1409)
A PDF whose fonts aren't embedded is drawn with PDFium's Foxit stand-ins for the standard 14 fonts. Those carry only the 230-odd glyphs of the Standard, WinAnsi and Mac Roman encodings, so a letter a font names beyond them in its /Differences mapped to .notdef and drew nothing. iText 2 writes Turkish exactly this way (Times New Roman left unembedded, /Gbreve /Idotaccent /Scedilla /gbreve /scedilla over Ð Ý Þ ð þ), and such letters showed blanks for Ğ, İ, Ş, ğ and ş while ı, ç, ö and ü drew. StandardKind now builds a Latin Extended-A letter the stand-in lacks from the base letter and accent it does have, as Type 1 seac composites and the standard-14 AFM composites are built: the accent is centred on the letter, raised over capitals and letters with ascenders (and moved along the slant in italics), cedillas hang below and ogoneks from the right edge. Composite glyph ids sit above any font's glyph count, one per character code; their width comes from the standard metrics of the name. Test: raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts (fails without the patch: Gbreve drew no pixel). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e8e275cbd4 |
Text extraction: honour /ActualText, so Thai from Chrome PDFs is searchable (#1217)
* Text extraction: honour /ActualText, so Thai from Chrome PDFs is searchable Chrome (Skia) writes /ActualText around each Thai, Indic or Arabic cluster whose shaped glyphs (a tone mark moved clear of a vowel, a ligature) have no Unicode of their own. Text extraction ignored it, so those marks were lost: "น้ำหนัก" came out as "นาหนัก", "เป็น" as "เปน", and searching for any such word found nothing. - hayro-interpret patch (25): Device::actual_text hands the device a marked content sequence's /ActualText, inline or named in /Properties. - TextDevice turns the glyphs of a replacement into one glyph carrying the replacement text, boxed around them. The outermost replacement wins, an empty one stands for no text, and one spanning several lines or writing directions keeps per-glyph text so selection and lines still work. - Canva draws text twice (outline or shadow) and marks only one drawing: the unmarked redraw at the same places takes the same replacement, so the existing redraw detection still drops the copy. Checked on 1,114 real-world PDFs against the previous build, with pdftotext as the reference: 957 extract byte-identical text, 157 change and all of them read closer to the reference (exact matches 3 -> 122), none got worse, none newly fail, and total extraction time is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Text extraction: bound the work of the ActualText redraw dedupe `TextDevice::finish()` compared every replacement with every unmatched glyph in up to 4096 grid cells, so a hostile page stacking ~10^5 glyphs and ~10^5 replacements at one spot meant ~10^10 comparisons (a hang). It now charges each visited cell and each candidate run against a per-page budget (`MAX_REDRAW_WORK`, 2^22) and stops deduplicating once it is spent; redraws found so far are still merged. A regression test feeds 100,000 replacements and 100,000 non-matching glyphs at one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: git-monchiant <4667243+git-monchiant@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
bde9dc52fe |
honour the /Lab colour space /WhitePoint (#1203) (#1237)
* fix: recognize paragraph drafts as unsaved work (Issue #811) * fix(color): honour the /Lab colour space /WhitePoint (#1203) The old path fed every Lab value to the bundled ICC transform as if the space's white point were D50, so a /Lab space with any other /WhitePoint (e.g. D65 [0.9505 1 1.089]) rendered off by several levels per channel and a D65 blue came out 45 levels too red. Map L*a*b* to XYZ in the space's own white coordinates (ISO 32000-2 §8.6.5.4), Bradford-adapt it to D65 and convert to sRGB directly, as Poppler does. Missing or degenerate white points fall back to a flat white and hostile components are clamped without panicking. Add a render regression that checks the five reported flat patches of the D65 white-point fixture against their CIE sRGB values. * Lab: map normalised Indexed palette components onto L*/Range; drop unused LAB.icc An /Indexed palette over Lab passes its entries as byte/255 (manual_scale), which the new Lab conversion read as raw L*a*b* and drew near black. Map them onto L* 0..100 and the a*/b* /Range, with a regression test. Record the patch as hayro-interpret (25) in vendor/README.md, and remove LAB.icc (no longer referenced) with its ATTRIBUTION, NOTICE and assets README entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Main gates: rustfmt tests/viewer.rs; a type alias for the refresh-steps array (clippy type_complexity) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d415749556 |
Heavy pages: cull off-tile images, cache decodes, preview backdrops, calmer queues (#1117)
* Heavy pages: cull off-tile images, cache decodes, preview backdrops, calmer queues A large CAD sheet (page 24 of the benchmark file: ~5050 pt wide, 2104 images, 9 MB of content) cost a full page interpret per tile: 4.3 s for a 1024 px tile at 1000%, 10.5 s for the whole page, and a storm of renders on every pan frame. Tiles now cost 0.6 s, the sheet is covered by a handful of large tiles, and gestures no longer queue work faster than workers finish it. - Vendored hayro patch (10): skip images whose stream-size box misses the canvas before decoding them, so a tile no longer decodes, resamples and rasterizes every image on its page. Pixel-identical; the skip regression test now covers images too. - Vendored hayro-interpret patch (23): decoded image pixels shared across renders through the interpreter cache, keyed by stream and decode target size within a 64 MiB budget. - Preview renders (hayro patch 11, RenderRequest::preview through to the device): backdrops, thumbnails, grid cells and signature-drag backgrounds skip images under 16 device px. Tiles, whole pages, print and export always draw everything. Previews may differ by those pixels; finals don't. - Viewer: zoom/pan settle holds apply only to pages whose raster proved slow (slow_pages, 500 ms), so fast pages render with no added latency; tile sides grow with zoom and page size (giant sheets need ~8 tiles, not ~28), queued center-first and keyed by exact coordinates; Edit-text outlines capped. - vendor/README.md records the new patches with their tests. Co-Authored-By: Muse Spark <noreply@meta.com> * Decoded-image cache: key every decode input, survive a poisoned lock The key was the dictionary's bytes and the target size, so two images with byte-identical dictionaries (uncompressed, one size) shared pixels, and an image drawn under a transfer function reused pixels decoded without it. DecodedImageKey now holds the dictionary's place in the document (its data follows it), its bytes and object, mask/stencil/raster output, interpolation, the target size and preview or final quality; images under a transfer function and inline images (named colour spaces, dictionaries in content streams) are not cached, and an image over the budget is never kept. A poisoned lock decodes uncached instead of panicking. Soft masks: the per-renderer cache can't carry a preview mask into a final render (documented). Regression tests for identical dictionaries, preview then final through one cache, and transfer functions. --------- Co-authored-by: Muse Spark <noreply@meta.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
7f1b632519 |
M2, #1060: axial and radial shadings draw as native gradients (backport of hayro 0.8) (#1061)
InDesign writes fades as luminosity soft masks whose group is one extended axial `sh` over the whole page. hayro 0.7 sampled every such shading pixel by pixel through the shading function, so a report page with 40 of them took 10-15 s at 144 dpi. Backport hayro 0.8.0's `gradient` module: an axial or radial shading that a native gradient draws exactly becomes sRGB stops (within 0.01 per channel, a stop at every stitching bound), painted as a vello_cpu gradient. Upstream's half-pixel shift is left out: it suits vello_cpu 0.3 but moves 0.0.8's gradients off pixel centres. Page 4 of the reported file: 10.8 s -> 1.0 s. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dd046a25b2 |
M2: stream page content past the 256 MiB cap (item 1 of storytold/pdfcraft#623) (#797)
* M2: stream page content past the 256 MiB cap (item 1 of storytold/pdfcraft#623) Page content was decoded whole, and the decode stopped at MAX_DECODED_STREAM (256 MiB), so the last ~10 MB of the #623 sample was dropped without a warning. Top-level page content is now read a window at a time, so the decoded content is never all in memory: - the content tokenizer reads windows and holds back an instruction that runs past one; Reader records reads that touch the end of the data, including reads made through clones, so an instruction is only decided once it has been seen whole; - the Flate decoder is resumable and keeps its own 32 KiB history, so it pauses at any output size and back-references do not depend on what the caller has kept; - page content may decode to max(256 MiB, 64 x its compressed size), counting each distinct stream once; content that reaches that budget is cut and the page reports ContentTruncated, as it did not before; - forms, tiling patterns, Type 3 glyph procedures and generic streams keep their existing budgets and caps. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> * M2: Avoid duplicate page content raw reads * M2: Document page content byte reuse --------- Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com> |
||
|
|
74be522c18 |
hayro-syntax: cap the nesting of direct arrays and dictionaries (#772)
Skipping a direct array or dictionary, and the lenient repair of a dictionary that has an object where a key belongs, recursed once per level with no bound, so deep nesting in a trailer entry or a content stream overflowed the stack. Count the depth on the Reader and fail beyond 128 levels like any other malformed object. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
a211e334a0 |
fix(render): key soft masks by inherited context (#716)
Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
306bbcedf9 |
M3: harden render and CLI failure surfaces (#669)
* M3: harden render and CLI failure surfaces Make content truncation observable, reject unknown CLI options, expose deterministic render work counters, and scope render failures to exact requests. Refuse silently reduced whole-page export rasters while preserving tiled rendering, and add heavy-vector and CLI regression coverage. Co-authored-by: Codex <noreply@openai.com> * Render: remember a failed render by page and kind again A key with scale and tag made every zoom level and tile a new key: a pathological page tied up a worker for the watchdog limit per request (exhausting worker replacements) and was re-parsed each time. The repeated-parsing check asks for a new generation and scale again. * Export: pages too large at the chosen dpi export at the most the renderer allows, and say so #669 made every Exporter refuse oversized whole-page renders, which reached File > Export to image, doc_export_* and OCR: at 600 dpi any page side over 13.65 in (Legal, A3, Tabloid) failed instead of exporting. The exporter clamps again; Exporter::dpi_used reports the resolution a page is drawn at, which the Export dialog's message and doc_export_png's lower_dpi list mention. A source with reject_oversize still refuses (CLI render keeps its strict config). * doc_export_images: describe lower_dpi * Render: ContentTruncated is reported once per page; the hayro-interpret patch is recorded Nested forms, Type 3 glyphs and patterns are interpreted again and each reported the exhausted budget; pdfcraft-render now keeps one. The warning is part of hayro-interpret patch (18) (the page content budget): marked in the code and described in vendor/README.md. * CLI render: print render warnings (skipped content) to stderr * CLI: the option check refuses unknown options only; commands report missing values themselves text's own --page parsing (and its test on main) says what a page value must be; the generic "needs a value" pre-empted it. --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
ef9b553f55 |
M2: kanji that Adobe-Japan1 maps to a variation sequence draw in non-embedded fonts (#662)
Adobe-Japan1-UCS2 maps the JIS X 0208 forms of the kanji that JIS X 0213:2004 redrew to a variation sequence (CID 1247 is 噂 U+E0100). hayro only looked up single characters in a substitute font, so a non-embedded Japanese font drew 噂, 逢, 溢, 祇, 箸 and over 200 other common kanji as unrelated glyphs. A character with a variation selector, from Adobe-Japan1-UCS2 or a /ToUnicode, now draws the substitute's glyph for that form when its cmap has one, else its glyph for the character. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
d63436ed10 |
M2: a character a substitute font lacks draws .notdef, not an unrelated glyph (#661)
A Type 0 font that isn't embedded is drawn with a substitute (a craft-fonts Japanese face for Adobe-Japan1). When hayro couldn't find a CID's character in the substitute by Unicode, it took the CID for a glyph index of the substitute, which numbers its glyphs its own way: 𠮷 (CID 13706) drew as "Q" in BIZ UDPGothic, ㈠ and the squared katakana words as unrelated kanji. A CID of an Adobe character collection that the substitute can't find now draws .notdef, unless the substitute is a CID-keyed CFF font, which is looked up by CID as before. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
04f6b1a634 |
M2: bound PDF functions and reject invalid calculator operations (#581)
Limit function construction and calculator work, detect path cycles, and handle input-derived integer and stack errors through the existing optional failure path. Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
f5c99ce059 |
Never crash: mesh shadings can't exhaust memory or run for minutes (#412)
* Never crash: mesh shadings can't exhaust memory or run for minutes `cargo xtask fuzz` (with the dash, JBIG2 and decompression fixes in place) found a page whose Coons patch shading (type 6), a 147-byte stream with 1-bit coordinates decoded onto +-40000, took the renderer past 3 GB. hayro-interpret samples mesh shadings (types 4-7) pixel by pixel over each triangle's device-space bounding box into a map, with nothing but the u16 cast bounding it: up to 65536 x 65536 entries for one triangle. A synthetic single patch passed 6 GB in six seconds; ten thousand triangles that each cover half a 1000 x 1000 page ran for minutes. Two rounds of review of earlier versions found more: every patch was cut into 722 triangles (about 150 KB) before anything limited them; a lattice mesh with `/VerticesPerRow 0` pushed empty rows forever, and one with 4294967295 read the whole stream into one row; a mesh painted a thousand times was triangulated a thousand times; and the first caps cut content without a word. Vendored hayro-interpret patch (18): - `ShadingPattern::encode_within(area)`: the vendored hayro renderer passes the area it will actually draw (the path's bounding box on the canvas, which it already computes) plus a pixel for texels at a fractional edge, and triangles are sampled only inside it, so the map holds at most that area's pixels. - `TriangleSampler`: at most 8 visits per pixel of that area plus 4 per triangle in it. A triangle's bounding box is about twice its area and a sub-pixel triangle's is at most 2 x 2 pixels, so tiling meshes and small dense ones (a patch in a few pixels is 722 triangles) still shade completely; overlapping ones stop. - Coons and tensor-product patches are triangulated one at a time. - Meshes hold at most `MAX_MESH_PATCHES` (2^16; a 200 x 200 gradient mesh is 40 thousand) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, and a lattice needs two (ISO 32000-2 8.7.4.5.6) to 2^19 vertices per row. - A page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25; the 200 x 200 mesh is 29 million) mesh triangles in all, reset per page like the existing paint budget. - Each of these cuts logs a warning. `encode()` keeps its signature; with no area it samples within a fixed 2^24 visits. Synthetic regression tests in render: - `mesh_shadings_far_off_the_page_render`: a Coons patch and a triangle mesh decoded onto +-40000 render and the rest of the page draws; a patch on the page, the part of the huge one that covers it, and a patch filling a 4 x 4 pt rectangle as a pattern are all shaded. - `many_overlapping_mesh_triangles_finish`: 10,000 triangles each covering half of a 1000 x 1000 page finish, and are drawn. - `mesh_shadings_with_many_patches_or_no_rows_finish`: 40,000 patches beside the page, and a lattice with `/VerticesPerRow 0`. Red on main (out of memory, killed at 3.9 GB). Each part is pinned by a mutation: no visit budget stalls past 60 s; no clip leaves the covering patch unshaded; no per-triangle allowance leaves the small patch unshaded; triangulating all patches first runs out of memory (killed at 5.8 GB); no rows check runs out of memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Never crash: decompression bombs can't exhaust the renderer's memory The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and RunLength streams with no output limit. A 2 MB page whose content stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli render` and `text` past 3 GB in under a second, and a few more MB of file scale it further. On the desktop that is the first-page render of an emailed PDF, and the allocation-failure abort takes every open document with it. Our own layers already bound single streams (lopdf's load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few MB; the renderer path didn't. Random mutation almost never makes a high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by hand while triaging the fuzz run. Vendored hayro-syntax patch (6): - `decode_limit` gives each filter an output limit, and Flate (both flate2 and the fallback inflater), LZW and RunLength never write past it (the buffer can still grow to under twice it): `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter of an image whose size, bits per component and components are known, twice its pixel bytes (a PNG predictor tag byte per byte at most), capped at 1 GiB. Earlier filters in a chain produce compressed data and get the general limit. - `Stream::decoded_within(max)` for callers that budget. - A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one bomb twenty times decoded 5 GB), reserving exactly what it appends. Vendored hayro-interpret patch (18): - The content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint budget. Forms nest fifty deep and each level kept its content while the next ran: a form painting itself around a 256 MiB stream would have held over 12 GB. Each level gives its bytes back when it is dropped, so content painted again and again keeps drawing, and a stream without filters (borrowed from the file) costs nothing. - Type 0 sampled functions read every sample into a 4-byte value and then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They now take at most 2^21 table points and 2^23 values (the largest real tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are 1.2 million and 3.6 million), counted with checked arithmetic, and read no further than `/Size` asks. `/Size 0` no longer underflows building the default `/Encode`. Three rounds of independent review found the aggregate cases, buffer doubling past the limit, a chain/predictor shape the first image limit would have truncated, the Type 0 amplifier, and a first, cumulative budget that would have dropped legitimately repeated content, and a first Type 0 cap low enough to refuse real tint transforms; all fixed here. Measured in a release build (render; OS peak commit): - content stream inflating to 2 GB: 389 MB, 0.3 s - 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s - /Contents naming it twenty times: 516 MB, 0.3 s - a form painting itself around it: 644 MB, 0.4 s Synthetic regression tests in render (flate2 is a new dev-dependency there, already in the workspace): - `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through flate2, the fallback inflater (a stored block with a wrong length check, which flate2 refuses and the fallback accepts), LZW and RunLength each decode exactly 256 MiB, and at most 128 bytes as an 8 x 8 image. - `page_contents_and_nested_forms_share_one_budget`: three /Contents references decode one stream's worth; a self-painting form finishes. - `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted 300 times draws the 300th time. - `sampled_functions_with_billions_of_samples_are_refused`. Ten mutations each turn them red: no limit at all; no limit in flate2, the fallback, LZW, RunLength or for images; an unshared /Contents limit; no page budget; no refund on drop; no Type 0 sample cap. (Taking a form out of the budget no longer compiles: its content is the budget guard.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Streetblock <github@streetblock.de> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
e88def3465 |
Never crash: decompression bombs can't exhaust the renderer's memory (#411)
* Never crash: decompression bombs can't exhaust the renderer's memory The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and RunLength streams with no output limit. A 2 MB page whose content stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli render` and `text` past 3 GB in under a second, and a few more MB of file scale it further. On the desktop that is the first-page render of an emailed PDF, and the allocation-failure abort takes every open document with it. Our own layers already bound single streams (lopdf's load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few MB; the renderer path didn't. Random mutation almost never makes a high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by hand while triaging the fuzz run. Vendored hayro-syntax patch (6): - `decode_limit` gives each filter an output limit, and Flate (both flate2 and the fallback inflater), LZW and RunLength never write past it (the buffer can still grow to under twice it): `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter of an image whose size, bits per component and components are known, twice its pixel bytes (a PNG predictor tag byte per byte at most), capped at 1 GiB. Earlier filters in a chain produce compressed data and get the general limit. - `Stream::decoded_within(max)` for callers that budget. - A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one bomb twenty times decoded 5 GB), reserving exactly what it appends. Vendored hayro-interpret patch (18): - The content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint budget. Forms nest fifty deep and each level kept its content while the next ran: a form painting itself around a 256 MiB stream would have held over 12 GB. Each level gives its bytes back when it is dropped, so content painted again and again keeps drawing, and a stream without filters (borrowed from the file) costs nothing. - Type 0 sampled functions read every sample into a 4-byte value and then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They now take at most 2^21 table points and 2^23 values (the largest real tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are 1.2 million and 3.6 million), counted with checked arithmetic, and read no further than `/Size` asks. `/Size 0` no longer underflows building the default `/Encode`. Three rounds of independent review found the aggregate cases, buffer doubling past the limit, a chain/predictor shape the first image limit would have truncated, the Type 0 amplifier, and a first, cumulative budget that would have dropped legitimately repeated content, and a first Type 0 cap low enough to refuse real tint transforms; all fixed here. Measured in a release build (render; OS peak commit): - content stream inflating to 2 GB: 389 MB, 0.3 s - 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s - /Contents naming it twenty times: 516 MB, 0.3 s - a form painting itself around it: 644 MB, 0.4 s Synthetic regression tests in render (flate2 is a new dev-dependency there, already in the workspace): - `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through flate2, the fallback inflater (a stored block with a wrong length check, which flate2 refuses and the fallback accepts), LZW and RunLength each decode exactly 256 MiB, and at most 128 bytes as an 8 x 8 image. - `page_contents_and_nested_forms_share_one_budget`: three /Contents references decode one stream's worth; a self-painting form finishes. - `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted 300 times draws the 300th time. - `sampled_functions_with_billions_of_samples_are_refused`. Ten mutations each turn them red: no limit at all; no limit in flate2, the fallback, LZW, RunLength or for images; an unshared /Contents limit; no page budget; no refund on drop; no Type 0 sample cap. (Taking a form out of the budget no longer compiles: its content is the budget guard.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Streetblock <github@streetblock.de> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
e130af2ef4 |
M3: renders of a dropped render pool stop at the next operator (#409)
The engine replaces a document's render pool after every edit, undo, redo, layer toggle and comment-visibility change, and drops it on close. Workers in the middle of a page kept rendering it to the end before finding that nobody could receive the answer. On heavy pages that is seconds of CPU per worker, competing with the new pool re-rendering the same pages. hayro-interpret, vendored patch 17: `InterpreterSettings::cancelled`, checked before each content operator. The pool sets it only in `Drop`, when its result channel is gone, so a partly drawn page can never be delivered. Live pools, `PageRenderer` and inline pools never set it. 7 heavy pages on 7 workers re-rendered after replacing a busy pool: 2.4–2.5 s before, 1.1 s after (1.0–1.3 s without a replacement, unchanged). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a747f2521f |
M2.6: W/W* clip after whichever path-painting operator ends the path (#362)
ISO 32000-2 §8.5.4: W and W* mark the current path to become the clip once the path-painting operator that ends it has painted it with the old clip, and n is only one such operator. The vendored interpreter applied the clip in n alone. After S, s, f, F, f*, B, B*, b or b* it was never applied, so everything painted afterwards ignored it, and it stayed pending until some later, unrelated n (even after Q) clipped content that should show. Issue #260's test file uses `W* S` on pages 3, 5 and 9: a shading meant for a circle flooded page 3, an image meant for a rounded rectangle and nested clips on 5 and 9 spilled out. Every painting operator now applies a pending clip after painting and clears it (apply_pending_clip, shared with n). The now unused Context::clip getter is removed. Test: raster::tests::clipping_path_applies_after_any_painting_operator. W and W* with each of n, S, s, f, F, f*, B, B*, b, b*: the later red fill stays inside the square, and a stroking operator's outer half still shows outside it (painted under the old clip). A clip used by S doesn't linger for a later n, inside or after q/Q. On main it fails at `W S`. MuPDF, Poppler and PDFium agree on every case. Page 3 of #260's file now differs from PDFium by 0.84/255 on average (was 164). Vendored patch (16). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
1b62d47fa7 |
M3: find text in mixed horizontal and vertical PDFs (#287)
Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
118a12a0ad |
M2.6: map annotation appearance boxes onto /Rect corner to corner (#277)
ISO 32000-2 §12.5.5 maps an appearance onto its annotation with a matrix A that takes the lower-left and upper-right corners of the transformed appearance box to those of /Rect. The vendored interpreter scaled by /Rect size over box size but translated by /Rect.x0 - box.x0, which is only right when the box starts at the origin or the scale is 1. Any other appearance was drawn shifted by (scale - 1) × box origin, partly outside its /Rect: /BBox [10 10 30 30] in /Rect [10 10 60 40] painted x 25..74, y 55..84 instead of x 10..59, y 60..89 (device pixels). MuPDF, Poppler, PDFium and pdf.js all fill /Rect exactly. The translation is now /Rect.x0 - scale × box.x0 (and likewise for y). Boxes at the origin, and boxes as large as /Rect (PdfCraft's own appearances use /BBox = /Rect), map exactly as before. A box that collapses to a line still yields a non-finite matrix and is skipped. Test: raster::tests::annotation_appearance_box_maps_onto_rect. An offset /BBox, an offset made by /Matrix and a negative-origin /BBox now fill exactly /Rect (on main the first is drawn at (25, 55)-(74, 84)); a box at the origin and an offset box as large as /Rect are unchanged. Vendored patch (15) in vendor/README.md (after (14) in #276). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3fc8108a07 |
M2.6: simple-font /Widths: bounded allocation, bad entries skipped, font kept (#276)
The vendored hayro-interpret reads /FirstChar, /LastChar and /Widths of every simple font (TrueType, Type 1, Type 3, standard 14) in read_widths. Three defects there, each checked against MuPDF, Poppler, PDFium and pdf.js on synthetic files: - One entry was pushed per code below /FirstChar before any width was read. 50,000,000 already took 276 MB; 4294967295 asks for 34 GB, and a larger value aborts on allocation (AGENTS.md §4: cap allocations sized by the file). Simple-font codes are single bytes, so only codes 0 to 255 now get an entry. - An entry that isn't a number (null, a name) ended the array, so every later code lost its width and glyphs piled up. It now leaves just that code missing (/MissingWidth); the four renderers do the same. - A /LastChar below /FirstChar, or a /FirstChar so large the count overflowed, made read_widths fail, so the font didn't load and a standard font stood in for the embedded program. The font now loads with no widths from the array; the four renderers keep the font too. Valid /Widths read exactly as before, including explicit zero widths, which all four renderers and Apple Quartz (sips) honour literally. Tests (a Type 3 font, so no font program is needed): raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font fails on main for [null 600], [/W 600], /LastChar < /FirstChar and /FirstChar 4294967295; raster::tests::huge_simple_font_first_char_terminates renders /FirstChar 4294967295 /LastChar 4294967295 quickly (not run against the old code: it would exhaust memory). Vendored patch (14). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8c281b39b2 |
M2.6: draw Highlight annotations that have no appearance stream (#274)
A Highlight annotation (ISO 32000-2 §12.5.6.10) whose /AP is missing, as many older and generated files have, was not drawn at all: the vendored interpreter only draws annotations through their normal appearance. MuPDF, Poppler and PDFium all draw such highlights. When there is no usable /AP /N, the interpreter now fills the /QuadPoints quadrilaterals with /C at /CA opacity, blended with Multiply, the way pdfcraft-annot draws PdfCraft's own highlights. It uses the interpreter's own fill path, so clipping, layers and the device see an ordinary fill. pdfcraft-annot itself can't be reused here: render and annot are both L3, and it builds pdfcraft-cos objects while hayro reads hayro-syntax ones. Nothing changes when an /AP exists. Malformed /QuadPoints (not a non-empty multiple of eight numbers) and an absent, empty or malformed /C draw nothing, as pdfcraft-annot does. Hidden, NoView and "Hide all comments" are checked before this point and still apply. Only Highlight changes; the source PDF is not modified. Test: raster::tests::highlights_without_an_appearance_are_drawn. On main the quad stays white; now it is yellow over white and black over black (Multiply), two quads leave their gap white, /CA 0.5 and gray /C match MuPDF, Poppler and PDFium. An /AP still wins, and ten malformed or hidden variants draw nothing. Vendored patch (13) in vendor/README.md; the parity entry view.render-annotation-appearances cites the test and notes that other comment types without /AP are still not drawn. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7965b497ff |
M2.6: skip annotation appearances whose box collapses to a line or point (#271)
ISO 32000-2 §12.5.5 maps an appearance onto its annotation by transforming /BBox with /Matrix and scaling the resulting box onto /Rect. When /BBox has zero width or height, or /Matrix has a zero row or column, that box is a line or a point, and the vendored interpreter divided the /Rect size by zero. The infinite or NaN matrix then reached the device with every path, clip and glyph the appearance drew (4 of 4 drawing calls in the test). hayro's rasteriser happens to drop such geometry, so pages looked right, but nothing guaranteed it; AGENTS.md §4 asks for division by zero and NaN to be guarded where they arise. Clipped to a collapsed box the appearance shows nothing (MuPDF and Poppler draw nothing for these fixtures either), so the appearance is now skipped, as is one whose mapping overflows. The mapping of valid appearances is unchanged. Test: raster::tests::degenerate_annotation_appearances_are_skipped. A recording device sees non-finite geometry on main for a zero-width, zero-height and point /BBox and for a /Matrix with a zero row or column; none after. Each page renders blank; a valid appearance still fills its /Rect and nothing outside it. Vendored patch (12) in vendor/README.md. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
d3db99818f |
Rename PrintCraft to PdfCraft (#174)
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the PrintCraft settings and crash-recovery folders to the new name, and both apps read settings saved under the old storage key, so upgrades keep recent files, preferences and unsaved work. The signing test fixtures keep their PrintCraft subject names (they are binary). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
20842c72e3 |
Never crash: Type 1 font programs can't hang or overflow the stack (skrifa 0.47)
The nightly fuzz job found a Type 1 font that hung hayro: read-fonts 0.39 (through skrifa 0.42) looked ahead after every integer by parsing the next token, which looked ahead again, recursing through a whole run of numbers. A long run hung; 20,000 numbers overflowed the stack and aborted the process. Upstream fixed the tokenizer; the vendored hayro-interpret now uses skrifa 0.47 (read-fonts 0.44), with its one API change. The finding finishes in 0.5 s. Synthetic regression test (it aborts with the old skrifa). Corpus sweep: 0 regressions; text oracle unchanged (median F1 0.980, mean 0.866, every per-file score identical). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2f364665b8 |
Never crash: the fuzz job's eleven hangs finish (parser retries, inline images, Type 3 fan-out)
Every hang the nightly fuzz job found (11 findings, 8 source files) now finishes in 0.1-2 s; each cause has a synthetic regression test that times out without its fix. - cos (7 findings, our own parser): a stream whose /Length pointed at itself, with no endstream, made every parse attempt fail and each attempt reload the same object: ~2^32 parses. Objects being loaded are a seen-set, /Length values are memoised per load, and the shifted-header retry only runs when the header is shifted. - hayro-syntax (3): deciding where an inline image's data ends re-parsed the rest of the stream, nesting through every inline image; 50 images ran for over ten minutes. The look-ahead no longer nests and is bounded (256). - hayro-interpret (1): a Type 3 glyph showing eight glyphs of its own font, sixteen deep. Nested paints share a per-page budget (50,000). The pdf.js corpus sweep matches its baseline (963 ok, same 20 known). xtask check writes its report under CARGO_TARGET_DIR too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6058068266 |
Never crash: absurd image-mask and CCITT sizes are refused before decoding
The first full nightly fuzz run on main (now that the runner survives) found two 4 GiB allocations, caught by the 4 GiB child cap: - an inline stencil mask claiming /W 4294967295 was decoded in hayro-interpret before the renderer's MAX_IMAGE_PIXELS check could skip it (locally 9.6 GB, 42 s). Vendored patch image_size_ok: images and masks over 2^28 pixels are not decoded. - a CCITT image whose /DecodeParms claimed /Columns 4294967295 (with a small /Width) sized the decoder's line buffers (locally 4.3 GB). Vendored patch ccitt_size_ok in hayro-syntax: at most 2^20 columns and 2^28 pixels. Both findings now run in 7 MB. Synthetic regression test in render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
859f03d26e |
M5: Comment polish: Mark with checkmark (hidden /StateModel /Marked reply; card toggle and menus), Locked comments (Properties ▸ Locked; refuse move/resize/restyle/delete), Copy text, Hide/Show all comments (render setting, vendored hayro patch), Summarize comments (Comments only layout, sort by page/author/date/type); comments panel "…" menu gains hide, summary, export, import; card menu anchored to its button; comment_mark/comment_lock/comments_hide/comments_summarize tools (79 tools)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ee254a91a2 |
M2.6: fuzz findings in the bootstrap renderer: paint-nesting cap now covers Type 3 glyphs (stack overflow), hayro skips images over 2^28 px (hang); regression tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
675b258e77 |
M2.6: hayro-interpret patches from fuzzing: tiling-pattern nesting cap (stack overflow), CID width ranges clamped to 65535 (hang)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
108fdc108c | more progress, roadmap, wip |