mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 02:29:31 -06:00
sonar-fix/enforce-https
19
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b955c2eefb |
M2: a tiling pattern drawn once shows its cell at full scale (#3349) (#3356)
* M2: a tiling pattern drawn once shows its cell at full scale (#3349) PowerPoint writes a picture fill as a tiling pattern with /XStep 2147480100. Fitting that step into the 3000 px cell pixmap shrank the picture below a pixel. When what is painted can only show the first cell, the cell's /BBox is drawn at full scale with a transparent border and padded instead of tiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q * M2: a tiling cell whose scale isn't finite keeps the old path (#3349) Review follow-up on #3356: a /BBox 1e-40 wide passes the zero-area check and gives an infinite cell scale, which would send non-finite geometry to the cell renderer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q * M2: a stroke's miter counts in the one-cell check (#3349) Review follow-up on #3356: shows_one_cell took the line width as a stroke's reach; a miter join reaches half the width times the miter limit. The reach formula patch (9) uses is now one function, stroke_reach, used by both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q * M2: a skewed stroke's reach uses the transform's largest stretch (#3349) Review on #3356: under a skew, a direction between the axes stretches further than either axis, so the larger axis stretch could place a miter tip short of the next cell when it reached it, and the one-cell path left that cell unpainted. The reach now uses the spectral norm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q --------- Co-authored-by: Claude Opus 5.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> |
||
|
|
210fb904cc | fix(render): a soft mask fades a gradient fill again (fixes #2128) (#2132) | ||
|
|
e8fcf76c33 | Render: a soft mask shrunk below its size is filtered, so thin strokes drawn through it survive (fixes #1649) (#1654) | ||
|
|
88d96c93a8 |
M2: snap shrunk interpolated images to whole pixels so scans stay sharp (#1377)
* M2: snap shrunk interpolated images to whole pixels so scans stay sharp A scanned page (one 300 dpi image with /Interpolate true) was shrunk to a ceil-sized raster and then sampled a second time at a fractional scale and offset, which softened every edge next to Acrobat. An axis-aligned shrunk interpolated image is now shrunk once to its rounded device rectangle and drawn 1:1 at the rounded corner. Rotated, skewed, flipped, Type 3 and non-interpolated images are unchanged. On a synthetic scan page the mean error against an area-average reference falls from 3.4 to 1.6 grey levels at 96 dpi (3.0 to 1.5 at 144 dpi). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * vendor: list the hayro snap patch as (14) in the Patches and Tests columns; correct gaps.md patch counts The patch description had been appended to the Tests column of one of the two hayro rows. gaps.md now counts the distinct patches vendor/README.md lists (26 hayro-interpret, 15 hayro, 3 hayro-jbig2, 17 hayro-syntax). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
4541974ef9 |
Reading colours: replace document colours for reading comfort (#897) (#1270)
* test: give faux-redaction audit its own workdir (parallel-test flake) * hayro: ColorReplacement type and luminance mapping (reading colours, #897) * fmt: engine dates (upstream Greek localization miss) * hayro: apply ColorReplacement to solid text and line-art paints (#897) * render: ReadingColors view setting flows into RenderSettings (#897) * engine: per-document reading colours state (#897) * automation: view_reading_colors tool; page_render honours it (#897) * ui: Reading colours preference (Replace Document Colors, #897) * ui: seed reading colours on revision and signed-version opens (#897) * parity: a11y.replace-page-colors shipped (#897) * render: map stencil-mask paints through reading colours (#897) An ImageMask stencil's solid paint was baked into the raster bytes raw, bypassing the reading-colours hook every other solid fill goes through: under white-on-black, CCITT scans, JBIG2 text and stencil logos drew black ink on the black mapped background and vanished. Route it through replaced(paint, false) — stencils are line art in disguise — and cover it with an ImageMask fixture test sampling mapped ink on a mapped background. * render: print previews render with reading colours off (#897) Print previews went through the document's render pool, whose config now carries the reading colours: a white-on-black user saw black sheets in the Print dialog while the actual print stayed uncoloured. The request tag is the per-request channel that already crosses the render boundary, so PRINT_TAG now lives in pdfcraft_render and the renderer honours it by rendering previews with reading colours off — a preview's job is fidelity to the printout. The main view stays recoloured. Covered on both render paths (inline PageRenderer and pool worker). * ui: seed reading colours on created-document opens (#897) open_created (comment summaries) and open_created_bytes (images, text, blank pages, clipboard) pushed new DocViews without seeding the reading colours, unlike every other open path. A created document now starts with the app-wide preference too; the seed-on-open test drives both paths. * docs: reading-colours flag echo and display-fidelity parity notes (#897) The view_reading_colors description now says a flag that differs from the mode's preset default makes the echoed mode read custom, and the a11y.replace-page-colors parity notes record that print previews and view snapshots render with the document's true colours: reading colours apply to the reading view only. * Merge follow-ups: reading colours beside preview renders and the new catalogs - Renderer::new carries both color_replacement and the new preview flag - our raster tests construct RenderRequest with preview: false - the preference dialog grew past the window: scroll the combo into view and give the fixture a valid xref so no repair banner covers the dialog - checkboxes use the crate's square-box widget (clippy disallows egui's) - Kazakh and Turkish catalogs gain the ten reading-colours strings * Reading colours: record the hayro patch in vendor/README.md; add missing translations vendor/README.md gains hayro patch (13): `RenderSettings::color_replacement` / `ColorReplacement`, the `render_pdf` signature change and the removed `[dev-dependencies.log]`, with its tests. Adds the ten reading-colours labels to el, es, pl, te and zh-hant. 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> |
||
|
|
dce9b9f912 |
M2: keep non-interpolated minified images sharp (#635)
* Fix #624: preserve hard edges for non-interpolated images Select nearest-neighbour for Hayro's image pre-minification when /Interpolate is absent or false, retaining Catmull-Rom for true. Add a synthetic regression covering default, false, and true behaviour and record the vendored patch. Co-authored-by: Codex <noreply@openai.com> * M2: test non-interpolated minification with exact colours The #624 regression used alternating black and white columns. Nearest 8→4 sampling reads only white columns from that pattern, so an image skipped entirely still passed, and the check only asked for near-black or near-white pixels. Replace it with 2×2 colour blocks whose exact device output is asserted, with /Interpolate omitted equal to false, and /Interpolate true still blending. Add exact cases for gray, a non-integer 10→6 minification, and a matched soft mask. All pass on this head. On origin/main the same block image renders with blended mid-tones (for example 12eb12ff where pure green is expected), so these tests fail without the #624 change. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> * M2: plan nearest-neighbour pre-resize without filter limits Non-interpolated images are pre-resized with nearest-neighbour, which copies source pixels and needs no filter tables, crossed intermediate or scratch. The preflight still applied the Catmull-Rom checks to it: a source wider than 2^20 pixels, or a filter plan over 128 MiB, was refused, and once such a source was wider than the backend's u16 limit the image was skipped. resampling_byte_len and image_resampling_size take the image's interpolation flag. Nearest keeps the source and target buffer bounds and refuses any scratch it reports. Filtered images keep their limits unchanged, and the existing image_resampling_size tests pass true. Measured: a 1.5M-pixel-wide RGB row drawn at 750×4 device pixels renders 0 non-white pixels on origin/main (the image is skipped) and all 3000 pixels with this commit. The 1.1M-pixel gray case is covered by wide_non_interpolated_sources_draw_after_shrinking. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> * M2: keep non-interpolated Type 3 glyph images nearest Minified images inside Type 3 glyphs were drawn with ImageQuality::High, a bicubic sampler, whatever their /Interpolate. A non-interpolated image had already been pre-resized with nearest-neighbour, and bicubic sampling of that result blended it again. High is now used only for interpolated images, as elsewhere. Before this commit the new test saw an interior glyph pixel of [28, 28, 27] (not a palette colour) for a 4.6 × 2.3 px glyph of a palette image; after it, every interior pixel is a palette colour. /Interpolate true still blends. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> * M2: nearest-sample masked minified images straight into premultiplied RGBA A non-interpolated image with a matching soft mask used to build a full-resolution RGBA copy, nearest-resize it, then premultiply the target. For a minified image most of that copy was discarded. nearest_premultiplied_rgba reads colour and mask from the same source pixel, selected with PicScale's nearest index (same fixed-point centre sampling as ResampleNearestPlan) and premultiplies only the target pixels. It falls back to the previous path if its target buffer cannot be allocated. Interpolated images, unmasked images and magnification are unchanged. New test: a ten-column gray ramp with a soft mask drawn six pixels wide must pick the same columns for colour and mask. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> * M2: record the #624 image patch in vendor/README.md The hayro row listed the #624 regression test but not the patch itself. Add patch item (10) covering the nearest pre-resize and its planning, the Type 3 bicubic restriction and the premultiplied soft-mask path, and list every regression test that covers them. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.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> |
||
|
|
a5e378fd4b |
M3: tiles skip paths that can't paint them (item 5 of #623) (#670)
A tile of a large page renders the whole page with an offset, and hayro stroked and filled every path on it before vello discarded what fell outside the tile. Vendored hayro patch (9), `may_paint_canvas`, skips a fill or stroke whose box misses the canvas: the box of the path's points and control points (a curve stays inside their hull), grown by the stroke's reach (half the width times the larger of the miter limit and sqrt 2), by 0.5 user units for vello's and kurbo's approximation tolerances, and by 2 device pixels for antialiasing. Empty paths and non-finite bounds or miter limits are always drawn. Renders are byte-identical with and without skipping (`skipping_paths_off_the_canvas_changes_no_pixel`; the hidden per-thread `set_skip_offscreen_paths` exists for that test). On #623's sample at scale 2, a 1024 px tile takes about 38% less CPU time; whole pages are unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
1f0466fcae |
M2: bound image resampling buffers before allocation (#592)
Check source, destination, convolution and filter-plan sizes before allocation. Preserve original image geometry when a resize exceeds the policy, while allowing valid wide sources to shrink. 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> |
||
|
|
63363ec438 |
M3: rasters reach egui without a copy on the UI thread (#555)
* M3: rasters reach egui without a copy on the UI thread Every page, tile and thumbnail raster was copied pixel by pixel on the UI thread (`ColorImage::from_rgba_premultiplied`), because egui's `Color32` is 4-byte aligned and a `Vec<u8>` isn't. That is 0.5 ms per 1024² tile, 5.4 ms for a 33 MB page and 36 ms for a 197 MB page, against a 16.7 ms frame. hayro, vendored patch 7: `render_size` and `render_into` render into a buffer the caller allocates. `render` is `render_into` on a new pixmap, so its output is unchanged. pdfcraft-render renders straight into `Pixels`, the same premultiplied RGBA bytes kept in one 4-byte word per pixel. `Pixels` dereferences to `[u8]`, so every reader of `RenderedPage::rgba` compiles unchanged. The canvas reinterprets the words as `Color32`s and hands them to egui as they are. Pixels are identical: all 33 renders of #260's test file hash the same. The UI-thread copy is gone, and rendering a 6182×8000 page takes 67 ms instead of 71–76 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M3: render_into leaves a buffer of the wrong length untouched instead of reaching vello's assert * M3: regression test for render_into with a wrong-length buffer --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
8ce0a905bf |
Never crash: dash patterns can't exhaust memory or hang the renderer (#405)
`cargo xtask fuzz` found a 392-byte page whose ExtGState sets `/D [[11] 0]` on a line that starts at x = 92234775807. Stroke expansion keeps every dash, so it cut the line into billions of them and allocated more than 5 GB before the run was stopped. Triage found the same class in ordinary-looking geometry, and a second bug next to it: - A tiny dash on an ordinary line (`[0.000001] 0 d` on 20 pt) passed 10 GB in under two seconds; so did `[1] 0 d` under a shrinking matrix, and a long pattern on many short subpaths, because kurbo restarts the pattern at every subpath. The render watchdog can skip a stuck page but can't stop it allocating, so on a desktop this ends in an abort that takes every open document with it. - A dash array whose entries sum to less than zero (`[-1 -1] 0 d`) made kurbo's search for the starting dash loop forever. ISO 32000-2 §8.4.3.6 requires nonnegative entries. Vendored hayro patch (5, 6): `usable_dash_pattern` bounds the pieces a pattern can cut the path into, in user space where kurbo dashes: (control-polygon length / period + restarts) x entries per period, where restarts are kurbo's: every MoveTo and ClosePath. Over 250,000 the stroke is drawn solid, as it is for a pattern with a negative or non-finite entry or a period that isn't positive. Measured in release builds: a 250,000-piece stroke takes about 16 ms and 75 MB at its peak (a million took 80 ms and 290 MB). A non-finite phase becomes 0. Every case now renders in about 0.2 s and a few MB. Synthetic regression tests in render. Both were red before the fix (the first killed at 2.7 GB, the second stalled past 20 s), and four mutations each turn them red: the first estimate (one piece per subpath) with a 100k cap runs out of memory, a 100k cap draws a fine legitimate dash solid, dropping the negative-entry check draws `[-1 3]` dashed, and dropping it together with the period check hangs. (The period check alone is defence in depth: hayro-interpret turns 0 entries into 0.01, so a pattern without negative entries always has a positive period.) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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> |
||
|
|
a5d830bf25 |
Never crash: predictor rows and stroke widths from the input can't exhaust memory
The second full nightly fuzz run on main found two more: - hayro-syntax computed a Flate/LZW predictor row as columns × bits in wrapping arithmetic: /Columns 9223372036854775807 asked for 2^61 bytes and aborted on any machine. Row widths are now saturating usize (colors × bits also overflowed u8), a zero width is refused (it divided by zero), and a row longer than the data is not allocated (empty output, as before). - hayro stroked /LW 9223372036854775807 as given, and stroke expansion allocated 10 GB. Widths are clamped to 4 × (canvas width + height) device pixels, which covers the same pixels. Both findings now run in 8 MB. Synthetic regression test in render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1449b71a19 |
Never crash: bound the allocations behind the nightly fuzz runner's out-of-memory deaths
Fuzzed inputs drove one check-one process to 2-6 GB (the 16 GB CI runner running two of them was killed every night). Three causes, each with a test: - lopdf (bootstrap inspector) sized PNG predictor rows from /Columns before reading data: /Columns 4294967295 allocated two 4 GiB rows. Vendored lopdf 0.45.0 refuses rows longer than the data (and uses saturating row widths); its load-time stream decoding also gets a 256 MiB limit (it had none). - hayro sized a tiling pattern's cell pixmap from /XStep x scale, bounded only by u16: a step far beyond /BBox allocated 2 GB. The vendored patch keeps the cell within 3000 px a side, lowering the resolution instead. - cos decoded cross-reference streams up to the general 1 GiB limit; they now stop at what 8,388,607 objects' rows can hold (~200 MB at most), and the section is reconstructed (logged) instead. On the nightly's seed (41) the worst captured input fell from 6.2 GB to 6 MB, and a 300 s fuzz run had no process above 300 MB (before: many at 2-5 GB). 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> |
||
|
|
108fdc108c | more progress, roadmap, wip |