Skip to content

Still-image Processing Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [x]) syntax for tracking.

Goal: Add rotation, flips, grayscale and ordered Bayer dithering with renderer evidence.

Architecture: ImageArt stores private transform settings and prepares source pixels once before fit/sampling. CLI and worker/config paths pass these options unchanged; default processing remains identical.

Tech Stack: Rust 2021, Rust 1.90, Cargo and the existing public Rich APIs.

Spec: Approved design.

Global Constraints

Read the release index and AGENTS.md before execution. Preserve default output; keep new library behaviour in rich-ext/rich-art. Do not add required Renderable methods or fields to ConsoleOptions/ImageOptions. No implicit ambient probes, source-file reads or global logger installation. Rust 1.90, Rust 2021 and unsafe_code = "deny" apply. Before each commit run cargo fmt --all --check, cargo clippy --all-targets -- -D warnings, and env -u NO_COLOR cargo test; commit only after all pass. Publication is a separate handoff.

Review Focus

  1. Rotate then flip produces the agreed orientation and retains RGBA (F1).
  2. Grayscale transforms transparent content and coloured contain padding (F1).
  3. Partial cells, odd heights and each Braille dot retain correct mapping (F2).
  4. Bayer output remains stable across workers and repeated runs (F2).
  5. Unsupported animation/protocol combinations fail explicitly, lean CLI remains usable (F2).

F1: Transform pipeline and library API

Files: Create crates/rich-art/src/transform.rs, crates/rich-art/tests/transforms.rs; modify art src/image_art.rs, src/lib.rs, README.md.

Interfaces: Rotation::{None,Clockwise90,Clockwise180,Clockwise270} with None default; ImageTransforms { rotation:Rotation,flip_horizontal:bool,flip_vertical:bool,grayscale:bool } derives Default/Clone/Copy. ImageArt::transforms(self,transforms:ImageTransforms)->Self stores private state. Internal prepare(image:&DynamicImage,transforms:ImageTransforms,background:[u8;3])->(DynamicImage,[u8;3]) returns transformed source and effective background; no field added to ImageOptions. With grayscale disabled geometry preserves alpha; existing fit/composite behaviour follows. With grayscale enabled composite against effective background then use integer luminance and reuse grayscale background for padding.

  • [x] Build a2x3 RGBA fixture with six distinct pixels (including one transparent). Assert exact pixel coordinates after clockwise90 then Hflip then Vflip, input immutability, and alpha retention for geometry-only. Unit-test luminance:
assert_eq!((77u32 * 255 + 128) >> 8, 77);
assert_eq!((150u32 * 255 + 128) >> 8, 149);

Render non-square transparent content with contain fit and saturated red background; all resulting RGB triplets including padding must have equal channels. Add zero/one-pixel dimensions under existing validation policy and all no-transform baseline comparisons. - [x] Run env -u NO_COLOR cargo test -p rs-rich-art --features image --test transforms; expect missing transform API. - [x] Apply image crate rotate90/180/270 then fliph then flipv to owned working pixels. Grayscale uses (77R+150G+29B+128)>>8, u32 arithmetic; composite alpha before conversion. Return effective background with equal luminance channels and make the contain canvas consume it. Fit/sample/renderer stages retain their existing responsibilities.

let y = ((77 * u32::from(r) + 150 * u32::from(g)
    + 29 * u32::from(b) + 128) >> 8) as u8;
let gray = [y, y, y];
  • [x] Run transforms and current art image goldens, then global gates. Commit feat: add still-image rotation flips and grayscale.

F2: Bayer, CLI options and raster evidence

Files: Modify art src/image_color.rs, src/image_art.rs, src/braille.rs, src/block.rs; create crates/rich-art/tests/raster_acceptance.rs; modify CLI src/main.rs, src/config.rs, src/batch.rs, src/demo.rs, tests/image_options.rs, docs/cli.md, docs/PORTING.md, scripts/capture_v9_demos.py; create docs/media/cli-v9-image-transforms.svg using actual rendering.

Interfaces: Add Dither::Bayer4x4. CLI --image-rotate 0|90|180|270, --image-flip-horizontal, --image-flip-vertical, --image-grayscale; extend existing image-dither values with bayer4x4. Config uses corresponding snake_case names and existing image_dither key. Workers receive ImageTransforms and Dither values. Document exhaustive enum-match migration. Reject selected transforms on animation paths not covered by the still-image pipeline; reject Bayer unless ANSI256 ASCII/blocks. A2 context rendering must use the same preparation path.

  • [x] Add a constant-grey4x4 raster case: compare each quantised pixel to independently enumerated nearest palette results after offsets below (lowest index wins ties). Assert repeated processing identical, None fixture unchanged and truecolour/Braille/Sixel Bayer errors. Test individual Braille dot bit patterns, partial2x4 cells and odd-height half-block background; tests use exact Unicode code points and RGB values, not snapshots generated by the code under test.
let offsets = [[-15,1,-11,5],[9,-7,13,-3],[-9,7,-13,3],[15,-1,11,-5]];
assert_eq!(offsets[0][0], -15);
assert_eq!(offsets[3][0], 15);

CLI tests cover options/config precedence, serial/parallel identity, invalid rotate45, unsupported animation and exports via explicit targets. Rendering the same image across ASCII/blocks/Braille/Sixel produces real example assets; no synthetic screenshot. - [x] Run env -u NO_COLOR cargo test -p rs-rich-art --all-features --test raster_acceptance and CLI image_options; expect missing Bayer/options failures. - [x] Add Bayer branch before nearest-palette lookup on final sampled pixels. Add offset to each channel using signed arithmetic and clamp0..255, with matrix coordinate relative to final raster origin. Retain existing None and Floyd–Steinberg paths.

const BAYER: [[i16;4];4] = [[0,8,2,10],[12,4,14,6],[3,11,1,9],[15,7,13,5]];
let offset = 2 * BAYER[y % 4][x % 4] - 15;
let adjusted = (i16::from(channel) + offset).clamp(0,255) as u8;

Wire CLI flags→resolved config→worker settings→ImageArt builders once; validate unsupported selections before rendering. Generate media from deterministic small local fixture and record capture command/source. - [x] Run all-feature art/CLI image tests and cargo check -p rs-rich-cli --no-default-features; run global gates. Commit feat: add Bayer dithering and document raster workflows.