Commit Graph
190 Commits
Author SHA1 Message Date
Dylan M. Taylor 9a348da8bb Refactor codecs for strict Clippy checks 2026-10-27 23:47:06 +00:00
David Mann 3a689f217c Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/engine/src/file_cmds.rs
#	crates/engine/src/retouch_cmds.rs
#	crates/engine/src/vector_cmds.rs
#	crates/io/src/psd_export.rs
#	crates/io/src/psd_import.rs
#	crates/io/src/smart_map.rs
#	crates/psd/src/layer.rs
#	crates/raster/src/lib.rs
#	crates/ui-egui/src/camera_raw_ui.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/filter_dialog.rs
#	crates/ui-egui/src/gpu_canvas.rs
#	crates/ui-egui/src/panels.rs
#	crates/ui-egui/src/retouch_ui.rs
#	crates/ui-egui/src/transform_tool.rs
#	crates/ui-egui/src/vector_ui.rs
#	docs/gaps.md
2026-10-27 14:26:26 +01:00
David Mann 281f8e95f5 Merge PR #8704: fix(codecs): check header dimensions against the file size before allocating 2026-10-27 13:52:36 +01:00
c65b7d93d6 Integration: file-size bound fits DWA EXR, RLE BMP and GIF screens (#8704)
The new plausibility check refused valid files that main opens:
- EXR DWAA/DWAB parts: they go past 2^16:1 (a flat 8192x4320 DWAB frame
  with 12 alpha-scheme mattes is 29,948 bytes, 75,629:1; run-length
  channels zlib-packed on top reach about 132,000:1). DWA parts now get
  2^18:1, so a header of a few hundred bytes can ask for about 100 MB,
  not gigabytes.
- BMP RLE8/RLE4: end-of-line, end-of-bitmap and delta escapes leave pixels
  unwritten, so no ratio bounds a valid file (an EOB-only 4000x4000 RLE8 is
  1,080 bytes). Skipped like HDR.
- GIF: the decoder's size is the logical screen, which needs no data (a
  4000x4000 screen with a 1x1 frame is 35 bytes). The first frame must be
  backed, and the screen only past 256 MiB, so a 35-byte GIF still can't
  declare a 30000x30000 screen.
The declared-size products saturate: a Radiance header of 2^31 x 2^31
read without limits overflowed u64 in debug builds.

Co-authored-by: FinnNk <1424666+FinnNk@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:46:11 +01:00
053f0b7198 Integration: level 3 instead of fast() under zlib-rs, io test into tests/it (#7689)
zlib-rs maps level 1 to deflate_quick, which writes static Huffman blocks
only, so every Compression::fast() caller (PDF export and print, brush
presets, PNG Fast) wrote 5-32% bigger streams than miniz's level 1 did.
Level 3 is the lowest level that is never bigger than miniz level 1 on the
docs/images photos, raw and Up-filtered (level 2 was +0.9% on one); it is
0-16% smaller and takes 1.2-3.4 times as long. The new test exports a
noisy-gradient RGB document as PDF and checks the image stream against
miniz level 1 of the same samples: 603007 vs 547315 bytes with fast(),
passes with level 3. The PR's doc no longer says the levels are unchanged.

Main moved the io integration tests into crates/io/tests/it/, so the new
deflate_compatibility.rs moves there and uses crate::common.

Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:12:13 +01:00
David Mann cd485b6c2c Merge remote-tracking branch 'origin/main' into pr-7689-fixup 2026-10-27 01:54:27 +01:00
aiongg bb641e918a refactor(codecs): split tiff.rs into tiff/ 2026-10-26 22:54:56 +00:00
aiongg ea6268d736 refactor(codecs): move src/codecs/mod.rs to src/codecs.rs 2026-10-26 22:13:07 +00:00
aiongg 24a11846e6 refactor(codecs): move src/codecs/vp8/mod.rs to src/codecs/vp8.rs 2026-10-26 22:13:07 +00:00
aiongg cece50d513 Merge pull request #9386 from davidmann4/integration-batch-8
Integration batch 8: 37 reviewed community PRs on main
2026-10-25 20:04:41 -04:00
Dylan M. Taylor 3877bb1dbe chore: resolve SonarCloud maintainability issues (#9044)
* chore: resolve SonarCloud maintainability issues

* fix: format imports and fix effect_entry argument type

* fix: restore test closure and wildcard import that broke compilation

* refactor: split composite_to_surface to reduce cognitive complexity
2026-10-25 19:38:31 -04:00
ae4b778258 Integration: TIFF warns when extra samples past the cap are left out (#7415)
A decode kept the first MAX_EXTRA_SAMPLES (16) extra samples and dropped
the rest without a warning, though files with up to 64 samples a pixel
open: an RGB TIFF with 20 extra samples opened with 16 and no notice, and
a save then lost the other 4. The decode now adds ExtraSamplesOverLimit
with the count left out.

Found by CodeRabbit on #9386.

Co-authored-by: Jason DeBoever <615848+jasallen@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:56:00 +01:00
David Mann 5939e12dfb Merge PR #7415: codecs: read and write TIFF extra samples (spot and alpha channels) 2026-10-25 16:33:53 +01:00
f71c8b4680 Integration: open TIFFs whose extra samples exceed the memory limit (#7415)
The extra-sample budget check failed the whole decode, so a TIFF whose
image fits max_alloc but whose extra channels don't (e.g. 12000x8000
RGB8 with 5 alpha channels under wasm's 1 GiB limit) no longer opened,
though main opens it. Now the image decodes without the extra samples
and carries a DecodeWarning::ExtraSamplesDropped.

ExtraSamples values above 2 are reserved (TIFF 6.0, p. 31); they now
read as 0 (unspecified), so a save never writes a reserved value back.

Co-authored-by: jasallen <615848+jasallen@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:32:08 +01:00
David MannandClaude Opus 5.5 699e909278 Integration: merge #7415's approved head (#8885)
The stack carried #7415's codecs code from before its last review. This
brings in 1c5343d5, approved by FredApps: every TIFF extra plane must be
exactly width x height x sample bytes, with its two regression tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:08:00 +01:00
David MannandClaude Opus 5.5 4365c0537d Integration: merge main, resolve conflicts (#8885)
psd_import.rs keeps main's warning for an incomplete merged extra channel
next to the PR's Photoshop spot polarity. people.toml takes main's entries
plus jasallen's own. Docs take both sides' status lines and revision rows;
docs/scorecard.md is regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:07:44 +01:00
.CooCoo 09dc629b42 Codecs: save Amiga IFF (.iff) as 24-bit ILBM (#9080)
Photoshop reads and writes IFF; PhotoCraft opened Amiga IFF/ILBM files
but had no writer. Saving as .iff now writes a FORM ILBM with a BMHD and
a ByteRun1-compressed BODY of 24 bitplanes (red, green and blue, least
significant bit first), one plane row at a time. An image with alpha
also gets a mask plane (masking 1): alpha of 128 or more is opaque, and
when any pixel is not fully opaque the export warns that alpha is
reduced to on/off transparency. Gray is expanded to RGB, 16-bit and
float images are reduced to 8 bits, and images wider or taller than
65535 pixels are refused.

The reader also opens 24-bitplane ILBMs, so a saved file reopens with
the same pixels. A transparent color index in a 24-bitplane file is
refused as unsupported.

IFF leaves ASYMMETRIC_EXCEPTIONS, Save As offers Amiga IFF, and the
codecs README, file-format parity and G10 are updated.
2026-10-25 10:20:09 -04:00
Finn Newick 90841daaf7 Merge branch 'main' of https://github.com/storytold/photocraft into fix/codec-plausible-dims
# Conflicts:
#	crates/codecs/src/codecs/jpeg.rs
2026-10-25 01:49:12 +01:00
aiongg 3d0c176e80 JPEG: a craft-jpeg frame without scan data is refused before decoding 2026-10-25 00:00:01 +00:00
aiongg ae368d5a61 JPEG: lossless and 12-bit progressive files are unsupported, not damaged 2026-10-24 23:20:40 +00:00
aiongg 4198b9fa3b JPEG: follow craft-jpeg's review fixes (non_exhaustive Limits) 2026-10-24 22:06:38 +00:00
skylar taylor-barrick 1c5343d5f7 codecs: say what the second extra-plane check guarantees
The first comment claimed the check covers the Lab re-type, which happens later
in `write_image`. Say instead what the check actually adds: the planes are
validated in the sample type they now hold after `converted`, and a plane that
`Image::convert` took but could not rebuild arrives here empty rather than
retyped.
2026-10-24 15:43:33 -06:00
skylar taylor-barrick e2ecad7442 codecs: refuse TIFF extra samples that are not one sample per pixel
The public `ExtraSample` contract is one sample per pixel in the image's sample
type, but the writer only stopped when a requested slice was missing, so an
oversized plane was silently truncated and a short one was dropped mid-image.
`Image::convert` leaves a plane whose length does not match the image alone
(deliberately: importers drop such a plane with a warning), so encoding a Lab
F32 image also reinterpreted a wrong-sized plane at the file's 16 bits.

Check every plane against width * height * sample.bytes(), with checked
arithmetic, at both ends: on the source image before `converted` brings it to
the plan's sample type, and on the image actually written, which the Lab path
re-types. A plane of any other length is a CodecError::Encode naming the plane,
its length and the length the image needs.

Regressions: too long, too short and empty planes at 8 bits, and a 7- or 9-byte
plane for a two-pixel Lab F32 image, which the U16 conversion used to accept.
2026-10-24 15:32:54 -06:00
skylar taylor-barrick b6835441dc Merge main into the TIFF extra-samples branch
Resolve the contributors/people.toml append conflict by keeping both sides:
main's three new entries and this branch's entry.
2026-10-24 15:29:53 -06:00
aionggandMichael Higgins 7cf4e6eca0 JPEG: open 12-bit and arithmetic-coded files through craft-jpeg (#4934, #5003)
Arithmetic-coded frames (SOF9-SOF11) and SOF0/SOF1 frames whose precision
isn't 8 bits go to craft-jpeg; everything else stays on zune-jpeg. 12-bit
files open as 16-bit images. PhotoCraft's own marker scan still supplies
ICC, EXIF, XMP, resolution and Photoshop resources, and damaged or truncated
entropy data adds the Truncated warning. A truncated scan too short for the
frame it declares is refused, as on the zune-jpeg path.

craft-jpeg is pinned to the aiongg fork until it lands on storytold.

Co-authored-by: Michael Higgins <mhiggins217@gmail.com>
2026-10-24 21:19:48 +00:00
Finn NewickandClaude Opus 5.5 f98983fff4 fix(codecs): stray bytes in a JPEG never count as scan data
Junk where a marker should be (which the decoder skips) bought frame
budget as padding did. The walk still resynchronises at the next marker
but no longer counts the skipped bytes, and junk with no marker after
it ends the image data there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:39:56 +01:00
Finn NewickandClaude Opus 5.5 d03c23e12f fix(codecs): skip stray bytes in the JPEG walk instead of stopping
A stray byte before APP padding stopped the walk, and the unread tail,
padding included, counted as scan data. The walk now resynchronises at
the next marker, as a lenient decoder does, so padding after stray
bytes is still recognised; the skipped bytes count as scan data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:34:39 +01:00
Finn NewickandClaude Opus 5.5 8f782cab1c fix(codecs): bound complete JPEGs by their scan data too
A complete file's frame-size budget was its length, which APP padding
inflates. Every file is now measured on the scan data the walk crossed;
where the walk stops early on an odd layout, what it couldn't read
counts as scan data, so such files still decode while padding before
that point never counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:27:34 +01:00
Finn NewickandClaude Opus 5.5 07f91c6d37 fix(codecs): one JPEG frame-size guard for truncated and complete files
main gained a guard refusing truncated scans too short for their
frame; this branch's own header check refused those files first with a
different error. Drop it and widen main's guard instead: a complete
file is measured on its length (the scan walk may stop early on odd
marker layouts), a truncated one on its scan data as before. A frame
needs at least a one-bit DC code per 8x8 block, so 2048 pixels a byte
stays far above any real JPEG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 23:57:23 +01:00
Finn Newick 81d2f3d651 Merge remote-tracking branch 'origin/main' into fix/codec-plausible-dims 2026-10-23 23:54:18 +01:00
FinnNkandClaude Opus 5.5 def4646c0e fix(codecs): WebP export keeps the resolution (#8775)
* fix(codecs): WebP export keeps the resolution

Every WebP export with metadata warned that the resolution would be
dropped, and lost it unless the image already had EXIF with resolution
tags. WebP has no resolution field of its own, so both encoders now
write the resolution into EXIF, adding the tags (or a minimal EXIF
block when the image has none), and WebP reports DPI support.

Part of #2399

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(codecs): report WebP DPI as dropped when the source EXIF can't be parsed

That block is written as it is rather than replaced, so its metadata
survives and the lost resolution is reported. The metadata test also
checks the rest of the EXIF (Orientation) survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(codecs): spell the WebP metadata test's bytes as escapes

The Orientation tag and the unparseable EXIF block were raw control
bytes, which also made git keep the file's CRLF line endings; both are
escapes now and the file is LF again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(codecs): no EXIF block just for WebP's default 72 ppi

A WebP without EXIF already opens at 72 ppi, so writing a minimal EXIF
for the default only turned every web export into the extended (VP8X)
container (and broke the automation test expecting a simple lossy file).
EXIF the image already has still gets its resolution tags updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(codecs): WebP DPI kept by XMP isn't reported as dropped

When the image's EXIF can't be parsed but its XMP records the
resolution, the WebP keeps it through XMP, which readers check first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:41:02 -04:00
Przemyslaw Majdak 612c4a3c7f Codecs: warn when DPX or Cineon saves drop bits (#8793)
DPX at 10 or 12 bits and Cineon (always 10) are planned as U16, so
DepthReduced, which compares sample types, never fired: a 16-bit document
saved at 10 bits lost six bits per sample silently, and a float one was
told it would be reduced to 16-bit.

FidelityWarning::BitsReduced { from, bits } names the stored depth. Float
samples always get it; U16 ones only when some sample is not already a
code value at that depth, so a frame opened from a 10- or 12-bit file
saves back without a warning. U8 widens losslessly and stays silent.
2026-10-23 17:38:29 -04:00
Dylan M. Taylor a691aaf65b codecs: use u32 in the padded-truncated-JPEG test's bounds arithmetic (u64: From<i32> does not implement — E0277) 2026-10-23 15:08:50 -04:00
Dylan M. Taylor a8983a4060 security(codecs): budget truncated JPEG scans by scan data, document the animated WebP ICC charge
CodeRabbit follow-up:
- data_end counts the scan-data bytes it crosses, and decode's
  truncated-input bound is measured on those, not on bytes.len(): APP
  padding padded before the scan used to charge the budget without
  describing one more pixel.
- a regression test builds a truncated JPEG with ~256 KiB of complete
  COMMENT padding whose patched 20000x20000 frame was admissible under
  the file-length bound, and verifies padding alone cannot admit it
  (while the same file at its real size still recovers).
- Limits::max_alloc docs state the animated WebP charging, next to the
  PNG text/XMP sentence.
2026-10-23 14:44:28 -04:00
Dylan M. Taylor c9892c8f8d security(codecs): refuse implausibly short truncated JPEG scans; charge WebP frame ICC copies
A truncated JPEG scan under a large declared frame was filled out by the
backend to the full admitted size; reject it when the file cannot plausibly
account for the pixels. Animated WebP frames each keep a copy of the ICC
profile, so the aggregate admission now counts it.
2026-10-23 14:12:04 -04:00
Jason DandClaude Opus 5.5 009329b10d codecs: allocate the TIFF extra-sample write buffers fallibly
interleave_extras and the 16-bit and float sample copies used infallible
allocations sized by the image and its extra planes, so a save too large
for memory aborted the process. They now reserve with try_reserve_exact
and return an OutOfMemory error, which the encoder reports, as the
decoder already does through `zeroed`. Raised by CodeRabbit on #8885.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:43 -04:00
Jason DandClaude Opus 5.5 e6fca0548f Merge current main into split/tiff-extra-samples-codecs
Conflicts: crates/codecs/src/image.rs (main's new `cicp` field next to
this branch's converted extra-sample planes in `meta`) and
contributors/people.toml (main's new entries kept, jasallen's added after
them). The codecs integration tests moved into tests/it/ on main; this
branch's test changes followed them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:43:11 -04:00
Finn NewickandClaude Opus 5.5 dde976d03c Merge upstream main into fix/codec-plausible-dims
Main's own PCX and XPM preflight (827d949af) replaces this branch's PCX
ratio check and XPM row check; the PCX test now expects main's refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 00:45:05 +01:00
Dylan M. Taylor 1bd922f6d1 fix(codecs): use exact minimum encoded row length for PCX preflight 2026-10-22 19:35:37 -04:00
Dylan M. Taylor 827d949af9 security: preflight PCX and XPM output allocations 2026-10-22 19:28:08 -04:00
Finn NewickandClaude Opus 5.5 2c00ba2e36 fix(codecs): exempt Radiance HDR from the size-ratio check
Its old-style RLE repeat markers multiply their counts by 256 each in a
row, so a valid file has no ratio bound; the allocation limit still
applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:40:32 +01:00
Finn NewickandClaude Opus 5.5 fb04b02893 fix(codecs): check header dimensions against the file size before allocating
JPEG, the image-crate formats (GIF, BMP, TGA, ICO, QOI, HDR), PCX, XPM and
flat EXR sized their output buffers from header dimensions, checked only
against the absolute limits. A few-KB file could declare ~3 GB and the
decoder allocated it before finding the data missing (on wasm that aborts
the tab). Each now refuses, before allocating, a file too small to hold
the declared image at that format's best compression ratio, the guard
PNG, WebP and IFF already use. XPM checks its text rows up front.

Fixes #8597

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:26:11 +01:00
aiongg ffa400c754 Keep tests correct and docs current with one test binary per crate
- gpu: the test modules share one GPU_LOCK, and timing and vector_mask
  take it too, so one process drives one wgpu device at a time.
- ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu,
  marquee_autoscroll and layout_perf take gpu_lock().
- affinity compound_limits: the child process selects its test by the
  module path (compound_limits::<name>); with --exact, the bare name
  matches no test and the child passes without running the check.
- xtask scorecard reads crates/io/tests/it/corpus.rs.
- Docs and comments name the new test paths and the --test it filters.
2026-10-22 16:55:34 +00:00
aiongg a6428d3dfb Build each crate's integration tests as one binary
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs; helper modules under tests/common move with them.

These keep their own binary (own process):
- engine panic_hunt: a process-wide panic hook counts every panic.
- engine text_gamma: changes the process-wide text gamma.
- engine smart_filter_stages, mask_cache_purge: assert on global caches.
- codecs pnm_peak_memory: reads the process's peak RSS.
- photocraft mac_menu_appkit: harness = false.
2026-10-22 16:55:34 +00:00
aiongg 934eb71d84 Integration: merge main
crates/engine/src/file_cmds.rs: batch keeps the save-point closure (takes the output path) and also copies the session's authorize hook into each scratch session, as main now does.
crates/raw/src/sensor.rs: both sides cap the memory of retained tiles; kept the existing retained_segment_bytes check (checked multiplication, same limit) and dropped main's duplicate inline loop.
2026-10-22 15:39:51 +00:00
aiongg 8e206e60b0 Integration: #5864 #5993 the DDS level cut-out test sets the level's row pitch 2026-10-22 15:39:21 +00:00
aiongg 4d1b922ddf Integration: track t02
# Conflicts:
#	contributors/people.toml
#	crates/engine/src/file_cmds/tests.rs
#	crates/engine/src/print_cmds/tests.rs
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
2026-10-22 15:38:14 +00:00
aiongg 82cf3c828c Integration: track t01
# Conflicts:
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	docs/file-format-parity.md
#	docs/gaps.md
2026-10-22 15:38:14 +00:00
Dylan M. Taylor f42d7ef41a security(codecs): index FITS header keywords for O(1) lookup
Header::get scanned every card linearly, and axis resolution looks up
NAXIS1..NAXISn while find_image/data_len re-resolve the axes. Both the
commentary-card count and the NAXIS count are file-controlled, so a
crafted header multiplied them into quadratic header-processing work
without a correspondingly large decoded image.

Build a first-occurrence keyword index once in parse_header (skipping
HIERARCH cards, matching the prior filter) and use it in get().

Finding: eb2121d8-9dee-4422-bd57-f78dce60a61b (CWE-407)
2026-10-22 07:11:58 -04:00
Dylan M. Taylor 3495a4b225 security(codecs): dedup Cryptomatte keys and cache stream lists
Cryptomatte metadata enumeration had two file-driven amplifiers:
- cryptomatte_keys() tested key membership with iter().any(), O(n^2) in
  the number of distinct cryptomatte/<key>/<field> attribute groups;
- layers() called layer_streams() for every key, rescanning all channels
  and allocating a fresh owned stream list even when many keys name the
  same layer.

Use a HashSet for key membership (keeping file order) and cache
layer_streams results by resolved layer name within each header, so
repeated keys reuse the discovered streams.

Findings:
- 120f4d51-b981-4fcc-abec-26f45f3efd29 (CWE-407, quadratic key membership)
- dae6e76b-a491-4089-892f-4cf34b911f36 (CWE-400, repeated per-key stream scans)
2026-10-22 07:11:50 -04:00