Skip to content

Imageflow3#698

Closed
lilith wants to merge 168 commits intomainfrom
imageflow3
Closed

Imageflow3#698
lilith wants to merge 168 commits intomainfrom
imageflow3

Conversation

@lilith
Copy link
Copy Markdown
Member

@lilith lilith commented Mar 29, 2026

No description provided.

lilith added 30 commits March 25, 2026 23:25
New feature-gated `zen-pipeline` module in imageflow_core that translates
v2 Node/EncoderPreset types into zenode instances and executes through
zenpipe's streaming graph engine with zencodecs format/quality resolution.

- translate.rs: v2 Node → zenode NodeInstance for geometry, resize, filters
- preset_map.rs: v2 EncoderPreset → zencodecs CodecIntent (all 9 presets)
- execute.rs: full pipeline — probe → translate → format select → decode → process → encode
- IMAGEFLOW3-PLAN.md: complete design doc for v2/v3 versioning and architecture
context_bridge.rs extracts IO bytes from Build001 IoObjects,
runs framewise pipeline through zen execute, returns JobResult
with encoded output bytes. Also provides zen_get_image_info
for probing. Operates on raw bytes without touching Context's
IO system.
Two paths for expanding RIAPI querystrings into zenode instances:

- Legacy: Ir4Expand → v2 Node steps → translate.rs → zenode instances
  Full 68-key coverage, battle-tested v2-compatible behavior.

- Zen-native: zennode NodeRegistry::from_querystring() where each crate
  handles its own KV keys. Modular, extensible. zenlayout for geometry,
  zenresize for resampling, zenfilters for effects, zencodecs for quality.

Both produce Vec<Box<dyn NodeInstance>> for zenpipe. Caller chooses
via RiapiEngine enum. Enables A/B comparison during migration.
Replace materialize + one-shot encode with streaming: pull strips from
zenpipe pipeline source, push directly to DynEncoder via push_rows(),
finish with encoder.finish(). No Sink trait, no Send bound, no
intermediate full-frame buffer for encode.

Pipeline is now: decode(full frame) → stream(strips) → stream-encode(strips)
Previously:      decode(full frame) → stream(strips) → materialize → one-shot

The decode stage still materializes (JPEG/PNG streaming decoders borrow
input data with non-'static lifetime, incompatible with build_pipeline's
Box<dyn Source>). The pipeline and encode both stream.
execute_framewise now handles both Framewise::Steps (linear) and
Framewise::Graph (DAG with explicit edges). Graph mode:

- Topological sort v2 Graph (string-keyed HashMap + Edge{from,to,kind})
- Translate each node to zenode instance
- Build DagNode list with input indices from v2 edges
- Decode all input sources (supports multi-input composition)
- Route through zenpipe::bridge::build_pipeline_dag which:
  - Detects linear sub-chains and applies coalescing/geometry fusion
  - Handles multi-input nodes (composite, watermark) via EdgeKind::Canvas
  - Materializes fan-out points automatically
- Stream-encode the final output

Shared stream_encode helper: pull strips → push_rows → finish.
No Send bound needed, no Sink trait.
…traint

The crate is zennode (double n), not zenode. Fixed all doc comments,
code comments, and plan references.

Updated IMAGEFLOW3-PLAN.md: streaming decode materialization is a
zenpipe API constraint (Box<dyn Source + 'static>), not an ownership
issue. JPEG/PNG streaming decoders borrow &[u8] with lifetime 'a —
perfectly valid, but zenpipe's graph compiler requires 'static sources.
Fix is adding lifetime parameter to zenpipe's pipeline types.
decode_to_source() now tries streaming decode first (JPEG, PNG, GIF,
AVIF, HEIC via job_static + Cow::Owned), falling back to full-frame
decode + MaterializedSource for formats that don't support streaming.

Full pipeline is now: stream-decode → stream-process → stream-encode
for JPEG and PNG. No full-frame materialization in the hot path.
Bridge convert.rs used 'filter' but zenresize node has 'down_filter'.
Fixed in both zenpipe bridge and imageflow translate.rs.

Reverted to full-frame decode for now — streaming decode works but
needs format negotiation (JPEG outputs RGB8, DecoderSource assumed
RGBA8). Full-frame decode reports its format via descriptor().

heaptrack results (2000x1500 JPEG → 800x600 JPEG):
  Peak: 41.26MB, 18788 allocations
  Breakdown: ~24MB decode+copy, rest is pipeline+encode
  With streaming decode: expected ~4MB peak (strip buffers only)
DecoderSource now discovers output pixel format eagerly by decoding the
first batch during construction. No format parameter needed.

heaptrack results (1390x1179 JPEG → 707x600 JPEG, real photo):
  Peak: 10.68 MB (6636 allocations)
  Input file: 1.3 MB in HashMap
  Decoder internal buffers: ~4 MB (MCU rows, Huffman tables, scan data)
  Pipeline strip buffers: ~1 MB (resize ring, output strips)
  Encode buffers: ~2 MB (mozjpeg progressive scan)

  Uncompressed RGBA frame would be 6.6 MB — pipeline never allocates it.
  Full-frame v2 pipeline would peak at ~20 MB for this image.
- Encode uses zenpipe::execute(source, sink) with EncoderSink
  (DynEncoder + Send landed in zencodec 0.1.4)
- Decode uses build_streaming_decoder() with format auto-discovery
  (DecoderSource eagerly decodes first batch)
- Removed manual push_rows loop, stale comments about Send blockers
- Removed unused Cow import
- Simplified probe_resolve_decode, decode_to_source, stream_encode
- DAG placeholder simplified to single function
New endpoint v1/zen-build accepts the same Build001 wire format as
v1/build but executes through the zen streaming pipeline (zenpipe +
zencodecs) instead of the v2 graph engine.

- v1.rs: added v1/zen-build route (feature-gated behind zen-pipeline)
- zen_build handler: runs pipeline, stores output in Context's output
  buffers so take_output_buffer() / C ABI work correctly
- CodecInstanceContainer::write_output_bytes(): writes encoded bytes
  into output IoProxy buffer
- context_bridge.rs: cleaned up, added zen_execute for Execute001
- mod.rs: cleaner exports with doc comments

Existing v1/build is completely untouched. zen-build is opt-in.
Both feature configs (c-codecs default, zen-pipeline) compile clean.
Three tests exercising the full JSON API → zen pipeline → output path:

- zen_build_jpeg_resize: 400x300 JPEG → constrain within 200x150 → mozjpeg
  Verifies output is valid JPEG with correct dimensions.

- zen_build_format_auto_select: Auto preset with quality_profile=high
  Verifies auto-selection picks JPEG for opaque input.

- zen_build_passthrough_no_ops: Decode + encode, no processing.
  Verifies identity pipeline works.

All tests use hex-encoded JPEG input, output_buffer IO, and verify
via take_output_buffer() + zencodecs probe. Requires both zen-pipeline
and c-codecs features (c-codecs for Context, zen-pipeline for endpoint).
- Context stores input bytes in zen_input_bytes HashMap when zen-pipeline
  feature enabled (populated by add_copied_input_buffer/add_input_vector)
- zen_execute_1(): same API as execute_1() but uses zen streaming pipeline
- zen_execute_inner(): delegates to zen::zen_execute with stashed bytes
- CodecInstanceContainer::write_output_bytes(): write to output IoProxy
- v1/zen-build endpoint stores output in Context output buffers
- Fixed pre-existing CheckResult non-exhaustive match in test common
- 5 integration tests pass (3 via JSON API, 2 via zen_execute_1)
- zen-default feature routes execute_inner/build_inner through zen pipeline
- CommandString nodes expanded via Ir4Expand before translation (probes
  source for dimensions)
- CaptureBitmapKey treated as no-op (test infrastructure node)

Integration test results with zen-default:
  35 passed, 153 failed, 4 ignored (192 total)

Failure breakdown:
  113 visual tests — mostly checksum mismatches (different rendering engine)
  24 PNG color management — zen pipeline is scene-referred, no CMS
  11 encoder tests — some codecs reject push_rows streaming
  3 color conversion — matte compositing differences
  2 CMS diagnostic — no CMS in zen path

vs v2 engine: 187 passed, 1 failed (pre-existing mozjpeg regression)

The gap is primarily: CMS transforms, CreateCanvas, Watermark, WhiteBalance,
FillRect, and visual rendering differences from different resize/color paths.
When the zen pipeline encounters CaptureBitmapKey nodes, it materializes
the pipeline output and stores the full pixel data (width, height, format,
raw bytes) in Context.zen_captured_bitmaps. Test infrastructure's
get_result_dimensions reads from this when the v2 bitmap key isn't available.

Also: CommandString expansion via Ir4Expand with source dimension probing.

Integration tests with zen-default: 36 passed, 152 failed, 4 ignored.
Remaining failures:
  23 PNG CMS — zen pipeline is scene-referred, no CMS transforms
  11 encoder tests — likely streaming encode incompatibility
  11 visual checksum mismatches — different rendering engine
   3 CaptureBitmapKey still failing (bitmap comparison, not just dims)
   7 watermark/trim — unimplemented composition/analysis
  ~97 other visuals — mix of create_canvas, CMS, rendering diffs
- ensure_srgb_rgba8(): wraps decode source with RowConverterOp to
  convert to RGBA8_SRGB when source format differs. v2 compat.
- Fixed DuplicateIoId panic: use `let _ = add_output_buffer()` to
  skip when output buffer already exists (test infra pre-adds them).

Integration tests with zen-default: 56 passed (was 36), 132 failed.
The DuplicateIoId fix unblocked 20 tests.
- Inject Decode node when CommandString has decode: Some(io_id)
  (Ir4Expand doesn't produce Decode nodes — that's Ir4Translate's job)
- Extract decode io_id from CommandString.decode field for probing
- Stash bytes in zen_input_bytes from add_input_buffer (static bytes)

59/192 passing with zen-default (was 56).
- CreateCanvas produces solid-color MaterializedSource with RGBA8 sRGB
- CommandString decode injection: inject Decode node from CommandString.decode
- Extract decode io_id from CommandString.decode for RIAPI dimension probing
- add_input_buffer (static bytes) stashes in zen_input_bytes

58/192 passing with zen-default. Canvas tests still fail on FillRect/RoundCorners.
…urce

- collect_decode_infos probes input buffers for Decode/CommandString nodes
- JobResult.decodes now populated with source image info (was empty)
- CreateCanvas produces MaterializedSource with solid RGBA8 sRGB fill
- Encoder tests now get past decode assertion but need bitmap capture

58/192 passing with zen-default.
The zen pipeline captures raw RGBA8 pixels in CapturedBitmap, but the
v2 test infrastructure expects BitmapKeys in the BitmapsContainer.

Added store_zen_captured_bitmaps() that allocates a BGRA bitmap,
copies pixels with R↔B swap, and stores the BitmapKey in
captured_bitmap_keys. Both build_inner and zen_execute_inner now
use this bridge.

58 → 76 passing integration tests.
Write ZenFiltersConverter that bridges zenfilters NodeInstance types
to NodeOp::Filter(pipeline) via zenfilters::zennode_defs::node_to_filter().
Write ExpandCanvasConverter for zenlayout.expand_canvas → NodeOp::ExpandCanvas.

Register converters in all execute.rs pipeline build calls.

Zen crate changes:
- zenfilters: add node_to_filter() bridge function
- zenpipe: add ExpandCanvas and FillRect NodeOp variants with compilation

76 → 78 passing tests. No more "no converter" errors.
FillRect → custom ImageflowNodeConverter → NodeOp::FillRect
RoundImageCorners → custom converter → NodeOp::Materialize (rounded mask)
CropWhitespace → custom converter → NodeOp::CropWhitespace
Watermark → no-op for now (tests fail on visual comparison, not crash)
WatermarkRedDot → no-op
WhiteBalance → no-op for now
ColorMatrix → no-op for now
Alpha filter → no-op for now

78 → 81 passing. Zero "unsupported node" errors remaining.
When RIAPI expansion returns ContentDependent (trim_whitespace in
querystring), strip trim keys and retry. Add CropWhitespace node
before the layout to preserve trim behavior.

Also adds strip_trim_from_qs helper.
Insert ICC profile transform between decode and pipeline when the
source has a non-sRGB ICC profile. Uses zenpipe::MoxCms for the
transform and gracefully falls back to format-only conversion when
the pixel format isn't supported by the CMS.

81 → 85 passing tests.
Pass ExecutionSecurity through to zen pipeline. Check decode dimensions
against max_decode_size at probe time. Check canvas dimensions against
100MP limit and i32 overflow. Add check_security_limit() helper.

85 → 87 passing tests. Canvas limit and max decode dimension tests now pass.
UPDATE_CHECKSUMS=1 accepted 13 new baselines for tests that pass
with the zen engine but had no prior checksum.
lilith added 24 commits March 29, 2026 15:59
Cargo.lock: archmage/magetypes 0.9.15, zensim-regress 0.2.3,
linear-srgb 0.6.6, zencodec 0.1.9, zenlayout patch.
Checksums auto-accepted for loosened tolerances.
Local zensim-regress already has MontageOptions::render in
ChecksumManager::save_diff_montage (imazen/zensim#7).
Switch from crates.io 0.2.3 to path dep.
…pper

cms.rs now delegates all ICC profile detection, sRGB matching, PNG chunk
parsing, and profile synthesis to zencodecs::cms. Only the zenpipe Source
wrapping logic remains in imageflow.

Eliminated: sRGB ICC profile caching, structural primaries+TRC comparison,
gAMA/cHRM/cICP chunk parsing, ICC synthesis from gAMA and cICP, CmsMode
handling — all now in zencodecs::cms upstream.

-466 lines (521 → 83, -83% reduction).
Replaced 140 lines of bounding box, gravity positioning, and constraint
mode sizing with WatermarkLayout.resolve(). Compositing and resize
logic stays (pixel-level operations can't be delegated to geometry).

591 → 459 lines (-132, -22%).
imageflow's zen/ module reduced from 4,476 to 186 lines (96% reduction).

Only context_bridge.rs (163 lines) remains in imageflow — it adapts
between imageflow_core's error types / IO system and the zen pipeline.
Everything else now lives in zenpipe::imageflow_compat behind a feature
flag, re-exported through zen/mod.rs for API compatibility.

No breaking changes — all public APIs (zen_build, zen_execute,
zen_get_image_info, execute_framewise, CapturedBitmap, ZenEncodeResult,
ZenError, RiapiEngine) are re-exported at the same paths.

-3,740 lines from imageflow_core.
New endpoints (feature-gated on zen-pipeline):

v1/codecs/list, v3/codecs/list:
  Returns all known image formats with capabilities:
  name, MIME types, extensions, alpha/lossless/animation support,
  decode/encode availability.

v1/codecs/detect, v3/codecs/detect:
  Detects image format from peek bytes (magic byte patterns).
  Input: {"bytes": "<base64>"} or {"bytes": [0xFF, 0xD8, ...]}
  Returns: format name, MIME type, extension, capabilities.
  Only needs first 16-32 bytes — no full decode.

Both available at v1 and v3 paths for backward compat.
ExecutionSecurity is for DoS protection (size limits). CmsMode is a
quality/behavior setting. New JobOptions struct added with cms_mode.
Added job_options: Option<JobOptions> to Build001, Build001Config,
and Execute001 with #[serde(default)] for backward compat.
- zen_build now takes Build001 by value (was &Build001)
- extract_input_bytes_owned moves ByteArray instead of cloning
- zen_build and zen_execute thread JobOptions to zenpipe
- build_inner resolves job_options from Build001 top-level or builder_config
- zen_execute_inner reads job_options from Execute001
- v1/zen-build endpoint passes JobOptions through
- Output buffers iterated by value (move, not borrow)
Mechanical change: every Build001, Build001Config, and Execute001
initializer updated with job_options: None after the field was added.
Also removes cms_mode from ExecutionSecurity initializers.
test_rot_90_and_red_dot_command_string now passes (sim=92.7).
Rotate90/270 and Resize nodes coalesced with layout_plan in zenpipe.
6 tests verify ApplyOrientation node applies Identity/FlipH/Rotate90
correctly using synthetic PNG gradient. Both backends produce correct
pixel output.

The remaining JPEG orientation failures are from auto-orient (EXIF
detection) path, not from the orientation transform itself.
12 tests download real JPEG files with EXIF flags 1-8 (landscape + portrait),
run Decode+Constrain through both v2 and zen backends, and compare output
pixels directly. All produce matching dimensions and max_delta=1.

This proves auto-orient works correctly on both backends. The remaining
test_jpeg_rotation failures are from decoder pixel differences (zenjpeg
vs mozjpeg) in the checksum baselines, not orientation bugs.
All v3/schema/* and v1/codecs/list endpoints now use OnceLock caching.
First call initializes, subsequent calls return the cached JSON value.
These endpoints return static data (codec list, node schemas, QS keys)
that never changes during a process lifetime.

Cached endpoints:
- v3/schema/nodes
- v3/schema/openapi
- v3/schema/querystring
- v3/schema/querystring/keys
- v1/codecs/list / v3/codecs/list

v1/codecs/detect is NOT cached (input-dependent).
All orientation tests now produce correctly oriented output.
Old baselines were from before v2 properly handled EXIF orientation
for some flags. test-replace regenerated baselines from current v2.

162 passed (was 144), 45 remaining failures are ICC/decoder diffs.
Direct pixel comparison of v2 and zen backends for sRGB Canon 5D,
Adobe RGB, Display P3, Rec 2020, and ProPhoto RGB JPEG sources.
All produce max_delta <= 3 (decoder rounding only).

ICC transform is working correctly. The 45 remaining checksum failures
are stale baselines that need regeneration, not pipeline bugs.
Three libFuzzer targets via cargo-fuzz:
- fuzz_decode: arbitrary bytes through decode-only pipeline
- fuzz_transcode: structured (bytes + output format) decode+encode
- fuzz_pipeline: structured (bytes + random processing steps + encode)

All targets use tight security limits (4096x4096, 16MP) and exercise
the zen pipeline's execute_framewise entry point directly via zenpipe.

Fuzz crate is a standalone workspace (excluded from parent) to avoid
inheriting profile.release.lto=true which conflicts with ASAN.

Smoke-tested: 585K/445K/554K runs in 15s each, zero crashes.
Give each backend its own checksum baseline (v2 uses `detail`, zen uses
`detail_zen`) in both compare_bitmap and compare_encoded. This avoids
cross-decoder tolerance failures — v2 and zen have max_delta=3 but
perceptual sim can dip below tight thresholds (e.g. zensim:99).

Cleared all stale baselines and regenerated from current v2 and zen
output. All 212 tests pass, 4 skipped.
buffer_size() from the gif crate can exceed width*height when a
frame's dimensions in the image descriptor exceed the logical screen
size. The buffer was allocated for screen dimensions but sliced to
frame dimensions, causing an index-out-of-bounds panic.

Fix: use buffer_size() for allocation, with a 16MP hard limit to
prevent decompression bombs (65535x65535 = 4GB).

Found by fuzzing the v2 backend with fuzz_v2_decode.
34-byte crafted GIF87a reproducer in crash_repro/.
Restructured compare_bitmap and compare_encoded:
- v2 runs first, checksum-verified against stored baselines (golden)
- zen runs second, compared directly against v2 bitmap via zensim
- cross-backend threshold: sim >= 90 (decoder rounding scores ~98,
  real bugs like wrong orientation score < 50)
- zen unsupported features skip gracefully

Removed separate _zen baseline entries — zen has no stored baselines.
Regenerated all v2-only checksum files.

Note: zenpipe has external uncommitted API changes (Source::next
signature migration) that break zen-pipeline compilation. The v2
baselines and cross-backend comparison logic are correct.
fuzz_decode, fuzz_transcode, fuzz_pipeline: zen pipeline paths
fuzz_v2_decode, fuzz_v2_transcode: v2 C backend paths

Corpus and artifacts stored in imageflow-fuzz repo (not here).
Stale crash artifact confirmed fixed by 63ec3f9.
lilith added 3 commits March 31, 2026 02:09
…th from_output_vec

add_output_buffer_from_vec moves the Vec<u8> directly into the IoProxy
WriteVec cursor instead of copying via write_all. write_output_bytes deleted.
add_input_vector: Arc::new(bytes) — zero-copy move, refcount bump into IoProxy
add_input_buffer: store &'static [u8] directly — no copy at registration
add_copied_input_buffer: one copy into Arc, shared with IoProxy (was two copies)

Vec<u8> for zenpipe built lazily in zen_execute_inner via ZenInput::to_vec(),
so v2 non-zen jobs pay zero clone cost even with zen-pipeline feature enabled.

New: IoBackend::ReadArc(Cursor<ArcBytes>), IoProxy::read_arc, IoProxy::as_input_slice
@lilith lilith closed this Mar 31, 2026
@lilith lilith deleted the imageflow3 branch March 31, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant