mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-13 01:14:21 -06:00
bb0f0d5e113aeda52fc7d3bb421908718b742a92
37
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
61005f3dc1 |
Culling: the loupe uses built previews; photos prepared ahead and in the background (#3077)
* PERF-CULL-0: ui.inspect reports how the loupe showed each photo it was stepped to
perf.loupeSteps records, per step, the ms from the step to the first frame
with anything of the photo, with a full-size picture (camera JPEG, cached
or rendered) and with LightCraft's own render, measured on the frames the
app draws, so culling benchmarks exclude the control channel's round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 95f0343848fc0a238f80f07ceb292eaf6779aeaa)
* PERF-CULL-1: browsing a folder shows its first photos within a second
The Local view read and hashed every file of the folder before showing any
of them (2,933 EOS R5 C CR3s: 30.6 s to the first photo). It now hands the
files the library lacks straight to the import, ordered the way the view
sorts them (file time for the date sorts), which adds them batch by batch:
first photo 0.9 s, twelve thumbnails 1.4 s, the whole folder in the same
~34 s as before, in the background.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b766558e7ef4f802919a144da53e444f70e0186b)
* PERF-CULL-2: the loupe makes the next photos' camera previews ahead of the step
Culling steps through photos, and each step waited for the next raw's
embedded JPEG to be read and decoded (p50 187 ms, p95 332 ms on EOS R5 C
CR3s) and then for a full raw develop. Now:
- the loupe's stand-ins for the next five photos in the direction of
travel and the one behind are made ahead (Slot::QuickAhead, priority
just below the loupe's own render) and taken over at the step, so a
step shows the camera JPEG at once (p50 0 ms);
- a slot keeps its photo while the photo stays wanted, so photos that
shift places as the user steps are not made twice;
- decoded camera JPEGs stay in the memory LRU (never on disk), so going
back shows them again without a decode;
- while an arrow key is held (a photo left within 0.25 s), the raw
develop waits until the photo has stayed 0.12 s, and a render queued
for a photo already left is dropped, so the workers make the previews
instead of renders nobody sees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0430b95636d2b6adce8887af7b9464687c95549e)
* PERF-CULL-3: CR3 previews take the smallest embedded JPEG that covers the view; plain sRGB JPEGs decode straight to 8-bit
Upstream's embedded_preview_covering (
|
||
|
|
e235ac71e2 |
Merge PR #3375: LR-IMP-FORMATS: a Samsung panorama JPEG with its SEF trailer imports
docs/whats-new.md: the PR's 2026-10-30 section added above main's sections. |
||
|
|
eb5829cfb5 | LR-IMP-FORMATS: a SEF trailer counts only with nonempty blocks of their entry's type, and an EOI is looked for before it (#3350) | ||
|
|
afee9df020 |
LR-IMP-FORMATS: a Samsung panorama JPEG with its SEF trailer imports (#3350)
Some Samsung panoramas have no end-of-image marker: the scan data runs straight into the SEF trailer at the end of the file, so the header check refused them as truncated, and jpeg-decoder (DCT-scaled previews) and zune-jpeg both fail on the trailer's bytes after the scan. When the file ends with a valid SEF trailer (SEFT, a directory length that points at SEFH, a sane entry count, every entry's block inside the file before SEFH) and no EOI is reached, the image is read as the bytes before the earliest SEF block plus an EOI, for both the header and the decode. The cut must fall in scan data after the first SOS; a JPEG cut short without such a trailer is still refused. |
||
|
|
fbbf5c0c53 |
LR-IMP-FORMATS: GIF and BMP coverage comes from the pixel data, not the file's length (#2553)
Padding counted as pixel data: a file padded with junk could still claim a canvas up to max_pixels, which the decoder allocates before it finds the data missing. - Uncompressed BMP: the rows the header describes (bits per pixel, 4-byte row padding) must all be present after the pixel-data offset; the decoder reads every row in full. - RLE BMP: the commands are walked as the decoder paints them (runs clipped at the row's end, end-of-row, delta, end-of-bitmap); bytes after the end draw nothing. - GIF: the first frame's LZW codes are walked up to the end code, keeping only the length of each table string; data sub-blocks after the end code draw nothing. A canvas the data leaves mostly blank stays limited to 4 MP. New tests: one padded file per case (all four were accepted before), and the LZW walk against frames the encoder wrote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016TuX1qw9ieyuJ1ZFrrUxzc |
||
|
|
1bc7cab6c8 |
LR-IMP-FORMATS: GIF density from the first frame; mostly blank canvases capped at 4 MP
From echelon's review of #2607: - A GIF's LZW data now bounds its first frame's own width x height, not the logical screen's, so a valid GIF with a small first frame on a larger screen imports. - A canvas the file's data leaves mostly blank (a GIF screen larger than its first frame, or an RLE BMP larger than its runs could cover at 255 pixels per 2 bytes) still costs its whole size in from_decoder, so it is limited to 4 MP (SPARSE_MAX_PIXELS) and refused before the decoder allocates it. Densely coded canvases keep the max_pixels limit. Tests: a 64-byte RLE BMP declaring 2001 x 2000 or 32000 x 32000 is refused ("mostly blank"); the RLE BMP that ends early still opens at 2000 x 2000; a GIF with a 1 x 1 first frame opens on a 2000 x 2000 screen and is refused on a 32767 x 32767 one. The first three fail without this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtjHGt5gnCHXgLt4NbGorf |
||
|
|
2d69246d29 |
Merge storytold/main into fix/2553-gif-bmp-header
Picks up #2667 (the shared Linux gate) so this PR runs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtjHGt5gnCHXgLt4NbGorf |
||
|
|
f3bb7a9f30 |
CI: one test binary per crate, line tables only, one WASM check (#2710)
* CI: dev profile keeps line tables for workspace crates and no debug info for dependencies * Control server tests: persistent_requests_and_slow_replies_outlive_the_idle_read_timeout no longer races the idle timeout Each request (and the closing half-shutdown) reaches the socket before the worker reads, so on a slow machine only the reply waits outlast the 100 ms idle timeout, as the test intends. The idle test sends its input the same way, and the no-frames test allows 5 s instead of 2 s to give up. * CI: one integration-test binary per crate Each crate's tests/*.rs become modules of tests/it/main.rs (51 test files into 13 binaries, plus 6 that stay their own). Inner #![cfg] attributes move to the mod lines. Files that change state for the whole process keep a binary of their own: codecs jxl_tail_heap and raw sr2_heap (dhat global allocator), gpu shutdown, shutdown_first and memory, ui-egui shutdown. Docs, comments and the GPU probe name the new paths and targets (--test it <module>::); the CI report names an it binary's skips after the crate. * CI: tests under nextest, doctests apart, one wasm check xtask ci runs the tests with cargo nextest run --lib --bins --tests when cargo-nextest is installed, else cargo test with the same targets and a one-line notice; doctests run after them with cargo test --workspace --doc. The heif and hardware GPU steps use the same runner. Examples stay out of the test build (clippy --all-targets still checks them). Concurrent tests under nextest follow the RUST_TEST_THREADS cap (NEXTEST_TEST_THREADS). The wasm check is one cargo check --keep-going over the same crates, and clippy runs with --keep-going. * CI: tests run under cargo test by default, nextest with LIGHTCRAFT_NEXTEST=1 (per-process scene cache) |
||
|
|
b55daa91da |
LR-IMP-FORMATS: an RLE BMP that ends early opens; a GIF's bound counts only its LZW data (#2553)
Review follow-ups on check_fillable: - RLE8/RLE4 BMPs skip the density check: an end-of-bitmap code zero-fills the rest, so a few bytes are a valid picture of any size (check_size still caps it). Uncompressed BMPs need at least 1 byte per 8 pixels (rows are read in full). - A GIF's bound counts the first frame's LZW sub-block bytes, not comments or palettes, at 2731 pixels per byte (a 4096-pixel code takes 12 bits). - The limits() comment no longer claims it stops the canvas allocation: DynamicImage::from_decoder allocates without consulting max_alloc. - whats-new states the 4 MP cutoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c2cdb112b7 |
LR-IMP-FORMATS: GIF and BMP sizes from the header; a canvas the file can't fill is refused (#2553)
read_header had no reader for GIF or BMP and fell back to a full decode, so a few-dozen-byte file declaring a canvas just under the 1-gigapixel limit made the import probe allocate ~4.3 GB; a failed allocation aborts the process. A decode had the same allocation. - other::header reads the size from the header. A canvas up to 4 MP is still decoded there, so truncated small files are refused at import as before. - check_fillable refuses a canvas larger than the file's bytes can describe (BMP: 128 pixels per byte, covering 1-bit and RLE; GIF: 11,000, an LZW code of at least 3 bits expanding to at most 4096 pixels), in both the probe and the decode, before the decoder allocates. - The GIF and BMP decoders get an allocation limit (the RGBA buffer of max_pixels). The approach follows the abandoned #2182 (PierrunoYT). JPEG XL still decodes in the probe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K93HTyCiwYvbRGaAkjPvXn |
||
|
|
70265ed17a |
Batch 42: #1893's heap test passes the Copy DecodeOptions without clone (clippy 1.99)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f4e7d07e9b | Merge PR #1989: TIFF: read strips and tiles larger than 128 MiB | ||
|
|
0a0cb19011 | Merge PR #1893: JPEG XL: check dimensions during loading & bound tracked allocations (#1517) | ||
|
|
b9159b0fce | TIFF: keep the default 128 MiB intermediate buffer for small pixel budgets | ||
|
|
43a47e5a3b | Issue #1952: clip non-finite float image samples safely (#2071) | ||
|
|
a8bc882092 |
Correct JPEG XL test target guard & tracked-budget claims (#1517)
Pitch-Impact: none Co-authored-by: Codex <noreply@openai.com> |
||
|
|
16beb4db40 |
Bound JPEG XL input feeds through the final frame (#1517)
Retain the pre-frame size check and allocation budget while avoiding untracked input-sized buffers. Add the reported heap regression and container/progressive compatibility checks. Co-authored-by: Codex <noreply@openai.com> |
||
|
|
b67bb01952 | TIFF: read strips and tiles larger than 128 MiB (fixes #1983) | ||
|
|
112e202fed |
JPEG XL: check the size from the header before reading frames, under an allocation budget (#1517)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9ca3cd0926 |
Merge PR #1135
# Conflicts: # Cargo.lock # Cargo.toml # README.md # crates/raw/Cargo.toml # crates/ui-egui/src/theme.rs # xtask/src/main.rs |
||
|
|
a4716d9e4f |
LR-IMP-FORMATS: transparent areas develop on white instead of black (#1212)
The decoders keep the alpha plane, but to_working converted only the colour, so transparent pixels kept the black most encoders store under them: renders, previews, exports and contact sheets showed it. The working image is now flattened onto white, in linear light like the rest of the pipeline. |
||
|
|
3e42f2e05c |
feat(testkit): #1037 tests record why they skip
libtest has no skipped status and hides the output of passing tests, so a test that returns early without a GPU, a corpus file, a craft-fonts face or a model passed silently. `lightcraft-testkit` (the testkit slot the layering table already reserves, dev-dependency only, std only) adds `skip!(Need, "reason")`: it prints the note as before and, when LIGHTCRAFT_SKIP_LOG names a file, appends one JSON line (need, reason, test, binary, file:line). Plain `cargo test` is unchanged. The 61 existing skip sites use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4a0d04b994 |
Merge PR #780
JPEG XL export via the pure-Rust jixel crate (robmdunn; fixes #247). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKWB9TCnvzJHV6xMLAj2J4 |
||
|
|
cbc0e2d7db |
LR-HDR-EXPORT: HDR PNG export as 16-bit Rec. 2020 PQ (cICP, cLLI)
With HDR output on, a photo edited in HDR exports as a 16-bit PNG with the PQ curve in Rec. 2020 (SDR white at HDR_REFERENCE_WHITE_NITS), described by a `cICP` chunk (PNG third edition: BT.2020 primaries, PQ, RGB, full range) and a `cLLI` content light level chunk (the levels the HDR AVIF records). EXIF, XMP and print resolution are kept; no ICC profile next to cICP. Photos without an HDR edit, and HDR edits with HDR output off, export as before. `codecs::encode_png_pq` sits next to `encode_avif_pq` and takes the same input. Follow-up to #646, ported from #466. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
afbc0f9744 |
LR-EXP-TYPE: JPEG XL export (#247)
Export as JPEG XL from the dialog, CLI, MCP and control channel (format jxl): lossy with quality or lossless, 8/16-bit, effort fast/normal, progressive, EXIF/XMP boxes; HDR edits as 16-bit Rec. 2020 PQ. Encoder: jixel (pure Rust, BSD-3-Clause OR Apache-2.0, native only: it spawns its own threads), wrapped in catch_unwind. Lossy files declare an enum encoding (sRGB / P3 / Rec. 2020; Adobe RGB and ProPhoto written as Rec. 2020); lossless files embed the ICC profile. Decoder fix: jxl-oxide desaturated colours outside sRGB when rendering XYB images to wider primaries; lossy wide-gamut JXL now renders to linear sRGB primaries and is converted to Rec. 2020 here. jixel's slowest effort is left out: jxl-oxide misdecodes its (valid) files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e46e7ab265 |
Merge PR #646 (HDR peak threaded through #533's base_tone; V1 at peak 1 unchanged)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e11db8fea0 |
LR-HDR-EXPORT: ISO 21496-1 gain map JPEG codec
`lightcraft_codecs::gainmap`: - `compute` measures a gain map (luminance or RGB, log2, block-averaged) from an SDR and an HDR rendition; `apply` rebuilds the HDR rendition. - ISO 21496-1 metadata: writer (fixed denominator) and reader (also the common-denominator form; refuses unknown versions, backward maps and zero denominators). Adobe `hdrgm` XMP reader and writer, per-channel values as rdf:Seq; Apple's HDRGainMap/HDRToneMap description of the same map. - `encode_jpeg`: base with CIPA DC-007 MPF index (gain map typed 0x050000), the ISO marker and Container XMP merged into the user's packet, followed by the gain map JPEG. `read_jpeg` / `is_gain_map_jpeg` find and decode the map. Also: gain map images are no longer picked as embedded thumbnails (an Ultra HDR JPEG could show its greyscale gain map in the grid). The JPEG APP-segment building is shared with `encode_jpeg`. Fuzzed with the other decoders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
90361935ba |
LR-IMP-FORMATS: HEIC decodes like libheif; release builds include it
heic-rs reconstructs iPhone HEVC tiles bit-exactly (checked against libde265), but its colour stage was 2-27 code values off libheif on every iPhone photo: it ignores the HEVC VUI and assumes BT.709 limited range. lightcraft-heif now converts YCbCr itself (nclx, else VUI, else H.265 defaults; nearest chroma, bilinear for odd clap offsets, as libheif), inherits tile ICC/nclx on grids, follows libheif's imir axis and clap rounding, and outputs 16-bit RGB. Measured on 30 real and synthetic files: within 1 code value of libheif (pillow-heif 1.8 / libheif 1.23). Also: HEIF header reader for import (no pixel decode), embedded thumbnail for small decodes, nclx/VUI colour hint (shared CICP mapping with PNG), clear 4:2:2/4:4:4 Unsupported error, Format::not_decodable as the one source of "why not" (import reports "HEIC/HEIF support isn't included in this build"), `.heif` imports. Release packaging and Nix build with the heif feature, as whats-new promised. lightcraft_heif::testdata writes lossless (PCM) synthetic HEICs for the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8d79bf082e |
Batch fix: the header test's RLE PSD fixture uses PackBits runs of at most 128 bytes
The fixture wrote each row as one literal run of w bytes, which PackBits can't express past 128 (a 160-pixel row's header byte read as a repeat run). The old decoder clipped such rows silently; with the PSD fix in this batch an overrun is an error, as it should be, and the fixture now splits rows into valid runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8 |
||
|
|
f6f5e6f1b7 |
Part of #367: probes read image headers instead of decoding the pixels
The import probe of a non-raw image decoded it (at a 64 px fit, which for
JPEG still entropy-decodes every block and for PNG inflates everything)
only to learn its stored size and orientation: ~95 % of import CPU.
lightcraft-codecs gets `read_header(bytes) -> Header { format, width,
height, orientation }`: JPEG from its markers (EXIF orientation), PNG from
its chunks (IHDR, eXIf anywhere), TIFF from the first IFD (tiff crate,
no strips read), WebP from its RIFF chunks / bitstream header (image-webp),
PSD from its header and image resources. GIF, BMP and JPEG XL keep the
small decode. `files::probe_bytes` uses it; raw handling is unchanged.
The TIFF colour-layout check is shared with the decoder (`layout`).
Measured (release, 4 cores, throwaway benchmark, per file, bytes in
memory): 200 generated 4000x3000 JPEGs (3.6 MB): 72.7 ms -> 2.1 ms
(read_header alone 1.05 ms; the rest is the content hash and metadata);
20 PNGs (30.8 MB): 236-265 ms -> 9.0-9.6 ms.
Corrupt files (behaviour note): the header readers also refuse what a
decode refuses before the pixels (unsupported layout, compression, frame
type, bad IHDR, ...) and truncated files: a JPEG whose markers don't reach
its EOI, a PNG cut before the chunk after its IDAT run, TIFF strips/tiles
that start past the end, a WebP image chunk or PSD merged data past the
end. So truncated files (interrupted copies, partial downloads) are still
marked `error` in the import review, as before; tests check that whenever
the old decode accepted a (cut or byte-mutated) file the header agrees.
Changed: damage *inside* compressed data that is all present (flipped or
garbled bytes) was refused at scan and is now imported, and shows as
unreadable ("!" in the grid) when rendered. Also: tiny JPEGs (< 128 px)
were decoded leniently by the old probe, so a truncated one imported; it
is now refused. A TIFF truncated inside its last strip is not detected
(StripByteCounts are often overstated, so only strip starts are checked).
Tests: header readers vs decode on generated JPEG/PNG/TIFF/WebP/PSD/BMP
with and without EXIF/TIFF orientation; truncations and random mutations
agree with decode; hostile headers (zero/absurd sizes, bad lengths,
truncation, garbage) are errors; read_header added to the fuzz tests;
probe_bytes oriented dims match the decode-based values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
|
||
|
|
88c1e768f1 |
TIFF: decode 1-, 2- and 4-bit samples
Bilevel scans, CCITT fax images and 2- or 4-bit gray TIFFs were rejected with "sample buffer too short". The `tiff` crate returns such samples packed, most significant bits first, eight, four or two to a byte with each row padded to a whole byte; the converter expects one byte per sample. They are now spread out one per byte and scaled to 0–255 (255, 85 and 17 times the sample: exact) before conversion. 3-, 5-, 6- and 7-bit samples, and planar images with more than one sample narrower than a byte, are refused with an "unsupported" error. The new test builds 1-bit (both photometric conventions, a width that leaves row padding, set padding bits), 2-bit and 4-bit strips in code, plus a truncated strip that must fail cleanly; it fails without the fix with the old error. A 600×400 bilevel image, uncompressed and CCITT G4, now decodes to the same pixels PIL reads from it. The module documentation no longer claims palette support: the `tiff` crate refuses RGBPalette images on every read path, so the palette branch here has never been reached. The parity row lists palette TIFF as missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cc425d0cea |
TIFF: invert WhiteIsZero gray once, not twice
A WhiteIsZero (PhotometricInterpretation 0) gray TIFF opened as a negative at every bit depth. The `tiff` crate (0.11) already inverts those samples as it reads them, for every layout it accepts, and refuses the WhiteIsZero layouts it can't invert; the decoder here then inverted them a second time. WhiteIsZero is common in scanner and fax output, and CCITT fax images use it by definition. The second inversion is removed. The new test builds 8- and 16-bit WhiteIsZero and BlackIsZero strips in code and fails without the fix (the lowest sample came out black instead of white). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ee9a989834 |
Codecs: bound thumbnail fallback decodes and WebP metadata allocations
Non-JPEG thumbnail fallbacks decode at source resolution before resizing, so the output size does not bound memory. ThumbnailOptions gains max_pixels (default 64 MP, previously about 1 GP via DecodeOptions::fit); larger sources return Error::TooLarge. Nothing in the workspace calls decode_thumbnail yet. WebP RIFF chunk lengths are untrusted: set_memory_limit(bytes.len()) stops a tiny truncated file requesting gigabytes for metadata. In image-webp 0.2.4 the limit applies only to ICC, EXIF and XMP chunks. Fuzz tests cover both; the roundtrip test only switches to struct update syntax. Split from a327b98 (T0 thumbnail work), where it was bundled. Co-authored-by: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3ce86032ff |
LR-EXP-TYPE + LR-EXP-DIM: export size modes, ppi, Original + XMP and DNG export
- Resize modes: long edge, short edge, width, height, W x H (either orientation), megapixels, percent; "don't enlarge" (default on); ppi written to JFIF density / PNG pHYs / TIFF tags. - Full-size export of a cropped photo now renders the crop at native resolution (it was upscaled to the uncropped long edge). - Formats: Original (file copied byte for byte + XMP sidecar with the edits, named after the output) and DNG (raw photos re-encoded losslessly, edits in the embedded XMP). - Export dialog: Resize-to dropdown, values, Don't enlarge, Resolution; DNG/Original hide pixel options. - lightcraft-cli render goes through the shared exporter (colour space, metadata, ppi) and takes any app.export option via --opt KEY=VALUE; MCP export schema lists the new params. - xtask resolves the workspace root at run time (a moved checkout broke ci). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
52e4809995 |
M1.1: codecs MPF preview thumbnail test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fa59a060dc |
M1.1: codecs fuzz/corpus/bench tests, thumbnail options, README
Property tests (random bytes behind every magic, mutated/truncated files), corpus tests (corpus/images, pngsuite, raw routing, system ICC profiles), ignored 24 MP JPEG benchmark, decode_thumbnail_with(min_embedded_edge), fast deflate levels for PNG/TIFF, README with formats, licences, gaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
304180a500 |
M1.1: lightcraft-codecs decode/encode, ICC, sniffing
Decoders (JPEG via zune-jpeg + jpeg-decoder for DCT scaling/CMYK, PNG, TIFF, WebP, GIF, BMP, PSD composite, JPEG XL via jxl-oxide) produce linear Rgb32f in source primaries with SourceSpace; ICC matrix/TRC applied exactly, LUT/CMYK profiles through moxcms; to_working() to linear Rec.2020 D65. Encoders: JPEG, PNG 8/16, TIFF 8/16/32f, lossless WebP, AVIF (native). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |