# Encoder API Specification

## Design Principles

1. **Config is dimension-independent** - reusable across images
2. **Layout is explicit** - enum or type proves you know what you're providing
3. **Stride at push time** - not baked into encoder construction
4. **Cancellation everywhere** - `impl Stop` on every push
5. **Non-generic where possible** - minimize monomorphization

---

## 1. Dependencies

```rust
// Re-export Stop trait from enough crate
pub use enough::Stop;
// Users use enough::Never for no-op cancellation
```

---

## 2. Quality & Quantization

```rust
/// Quality/compression setting.
///
/// All variants map to internal quality through empirical lookup tables.
/// Results vary by image - these are rough approximations, not guarantees.
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum Quality {
    /// Approximate jpegli quality scale (this is a fork, not exact jpegli).
    /// Range: 0.0–100.0, where ~90 is visually lossless for most images.
    ApproxJpegli(f32),

    /// Approximate mozjpeg quality behavior.
    /// Range: 0–100. Maps to quality producing similar file sizes.
    ApproxMozjpeg(u8),

    /// Approximate SSIMULACRA2 score target.
    /// Range: 0–100 (higher = better). 90+ is roughly visually lossless.
    ApproxSsim2(f32),

    /// Approximate Butteraugli distance target.
    /// Range: 0.0+ (lower = better). <1.0 excellent, <3.0 good.
    ApproxButteraugli(f32),
}

impl Default for Quality {
    fn default() -> Self { Quality::ApproxJpegli(90.0) }
}

impl From<f32> for Quality {
    fn from(q: f32) -> Self { Quality::ApproxJpegli(q) }
}

impl From<u8> for Quality {
    fn from(q: u8) -> Self { Quality::ApproxJpegli(q as f32) }
}

/// Quantization table configuration.
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub enum QuantTableConfig {
    /// Jpegli's perceptual tables, scaled by Quality. (default)
    #[default]
    Perceptual,

    /// Custom base matrices, scaled by Quality.
    /// Provide f32 matrices (typically 1.0–255.0 range).
    CustomBase {
        luma: [f32; 64],
        chroma: [f32; 64],
    },

    /// Exact quantization tables. **Quality is ignored.**
    Exact {
        luma: [u16; 64],
        chroma: [u16; 64],
    },
}
```

---

## 3. Color Mode & Subsampling

```rust
/// Output color space with bundled subsampling options.
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub enum ColorMode {
    /// Standard YCbCr with configurable chroma subsampling.
    #[default]
    YCbCr {
        subsampling: ChromaSubsampling,
    },

    /// XYB perceptual color space (jpegli-specific).
    /// Computed internally from linear RGB input.
    Xyb {
        subsampling: XybSubsampling,
    },

    /// Single-channel grayscale.
    Grayscale,
}

/// YCbCr chroma subsampling (spatial resolution).
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub enum ChromaSubsampling {
    /// 4:4:4 - Full chroma resolution
    Full,
    /// 4:2:2 - Half horizontal resolution
    HalfHorizontal,
    /// 4:2:0 - Quarter resolution (half each direction)
    #[default]
    Quarter,
    /// 4:4:0 - Half vertical resolution
    HalfVertical,
}

/// XYB component subsampling.
///
/// Unlike YCbCr where only luma is full, XYB keeps X and Y full
/// even in subsampled mode - only B is reduced.
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub enum XybSubsampling {
    /// X, Y, B all at full resolution (1×1, 1×1, 1×1)
    Full,
    /// X, Y full, B at quarter resolution (1×1, 1×1, 2×2)
    #[default]
    BQuarter,
}

/// Chroma downsampling algorithm for RGB→YCbCr conversion.
///
/// **Only applies to RGB/RGBX input.** Ignored for grayscale, YCbCr, and planar input.
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub enum DownsamplingMethod {
    /// Simple box filter averaging (fast, matches C++ jpegli default)
    #[default]
    Box,
    /// Gamma-aware averaging (better color accuracy at edges)
    GammaAware,
    /// Iterative optimization (SharpYUV-style, best quality, ~3× slower)
    GammaAwareIterative,
}
```

---

## 4. Encoder Config

```rust
/// JPEG encoder configuration. Dimension-independent, reusable across images.
#[derive(Clone, Debug)]
pub struct EncoderConfig {
    quality: Quality,
    quant_tables: QuantTableConfig,
    progressive: bool,
    optimize_huffman: bool,
    color_mode: ColorMode,
    downsampling_method: DownsamplingMethod,
    restart_interval: u16,
}

impl Default for EncoderConfig {
    fn default() -> Self {
        Self {
            quality: Quality::default(),
            quant_tables: QuantTableConfig::default(),
            progressive: false,
            optimize_huffman: true,
            color_mode: ColorMode::default(),
            downsampling_method: DownsamplingMethod::default(),
            restart_interval: 0,
        }
    }
}

impl EncoderConfig {
    pub fn new() -> Self { Self::default() }

    // === Quality & Quantization ===

    pub fn quality(mut self, q: impl Into<Quality>) -> Self {
        self.quality = q.into();
        self
    }

    pub fn quant_tables(mut self, config: QuantTableConfig) -> Self {
        self.quant_tables = config;
        self
    }

    // === Encoding Mode ===

    pub fn progressive(mut self, enable: bool) -> Self {
        self.progressive = enable;
        if enable {
            self.optimize_huffman = true; // required for progressive
        }
        self
    }

    pub fn optimize_huffman(mut self, enable: bool) -> Self {
        self.optimize_huffman = enable;
        self
    }

    pub fn restart_interval(mut self, interval: u16) -> Self {
        self.restart_interval = interval;
        self
    }

    // === Color Mode ===

    pub fn color_mode(mut self, mode: ColorMode) -> Self {
        self.color_mode = mode;
        self
    }

    pub fn downsampling_method(mut self, method: DownsamplingMethod) -> Self {
        self.downsampling_method = method;
        self
    }

    // === Convenience Shortcuts ===

    /// YCbCr with specified chroma subsampling.
    pub fn ycbcr(self, subsampling: ChromaSubsampling) -> Self {
        self.color_mode(ColorMode::YCbCr { subsampling })
    }

    /// XYB with B-quarter subsampling (default, perceptually optimized).
    pub fn xyb(self) -> Self {
        self.color_mode(ColorMode::Xyb { subsampling: XybSubsampling::BQuarter })
    }

    /// XYB with full resolution (no subsampling).
    pub fn xyb_full(self) -> Self {
        self.color_mode(ColorMode::Xyb { subsampling: XybSubsampling::Full })
    }

    /// Grayscale output.
    pub fn grayscale(self) -> Self {
        self.color_mode(ColorMode::Grayscale)
    }

    /// Enable SharpYUV (GammaAwareIterative) downsampling.
    pub fn sharp_yuv(self, enable: bool) -> Self {
        self.downsampling_method(if enable {
            DownsamplingMethod::GammaAwareIterative
        } else {
            DownsamplingMethod::Box
        })
    }

    // === Validation ===

    /// Validate config, error on invalid combinations.
    pub fn validate(&self) -> Result<()> {
        if self.progressive && !self.optimize_huffman {
            return Err(Error::InvalidConfig("progressive requires optimize_huffman"));
        }
        Ok(())
    }

    // === Encoder Creation ===

    /// Create encoder from raw bytes with explicit pixel layout.
    pub fn encode_from_bytes(
        &self,
        width: u32,
        height: u32,
        layout: PixelLayout,
    ) -> Result<BytesEncoder>;

    /// Create encoder from rgb crate pixel type.
    /// Layout inferred from P. For RGBA/BGRA, 4th channel is ignored.
    pub fn encode_from_rgb<P: Pixel>(
        &self,
        width: u32,
        height: u32,
    ) -> Result<RgbEncoder<P>>;

    /// Create encoder from planar YCbCr (separate Y, Cb, Cr planes).
    /// Skips RGB→YCbCr conversion. Only valid with YCbCr color mode.
    pub fn encode_from_ycbcr_planar(
        &self,
        width: u32,
        height: u32,
    ) -> Result<YCbCrPlanarEncoder>;

    // === Resource Estimation ===

    /// Estimate peak memory usage before encoding.
    pub fn estimate_memory(&self, width: u32, height: u32) -> usize;
}
```

---

## 5. Pixel Layout (for raw bytes)

```rust
/// Pixel data layout for raw byte input.
///
/// Describes channel order, bit depth, and color space interpretation.
/// Use with `encode_from_bytes()` when working with raw buffers.
///
/// For rgb crate types, use `encode_from_rgb()` which infers layout.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PixelLayout {
    // === 8-bit sRGB (gamma-encoded) ===

    /// RGB, 3 bytes/pixel, sRGB gamma
    Rgb8Srgb,
    /// BGR, 3 bytes/pixel, sRGB gamma (Windows/GDI order)
    Bgr8Srgb,
    /// RGBX, 4 bytes/pixel, sRGB gamma (4th byte ignored)
    Rgbx8Srgb,
    /// BGRX, 4 bytes/pixel, sRGB gamma (4th byte ignored)
    Bgrx8Srgb,
    /// Grayscale, 1 byte/pixel, sRGB gamma
    Gray8Srgb,

    // === 16-bit linear ===

    /// RGB, 6 bytes/pixel, linear light (0–65535)
    Rgb16Linear,
    /// RGBX, 8 bytes/pixel, linear light (4th channel ignored)
    Rgbx16Linear,
    /// Grayscale, 2 bytes/pixel, linear light
    Gray16Linear,

    // === 32-bit float linear ===

    /// RGB, 12 bytes/pixel, linear light (0.0–1.0)
    RgbF32Linear,
    /// RGBX, 16 bytes/pixel, linear light (4th channel ignored)
    RgbxF32Linear,
    /// Grayscale, 4 bytes/pixel, linear light
    GrayF32Linear,

    // === Pre-converted YCbCr (skip RGB→YCbCr conversion) ===

    /// YCbCr interleaved, 3 bytes/pixel, u8
    YCbCr8,
    /// YCbCr interleaved, 12 bytes/pixel, f32
    YCbCrF32,
}

impl PixelLayout {
    /// Bytes per pixel for this layout.
    pub const fn bytes_per_pixel(&self) -> usize {
        match self {
            Self::Gray8Srgb => 1,
            Self::Gray16Linear => 2,
            Self::Rgb8Srgb | Self::Bgr8Srgb | Self::YCbCr8 => 3,
            Self::Rgbx8Srgb | Self::Bgrx8Srgb | Self::GrayF32Linear => 4,
            Self::Rgb16Linear => 6,
            Self::Rgbx16Linear => 8,
            Self::RgbF32Linear | Self::YCbCrF32 => 12,
            Self::RgbxF32Linear => 16,
        }
    }

    /// Number of channels (including ignored channels).
    pub const fn channels(&self) -> usize {
        match self {
            Self::Gray8Srgb | Self::Gray16Linear | Self::GrayF32Linear => 1,
            Self::Rgb8Srgb | Self::Bgr8Srgb | Self::Rgb16Linear
            | Self::RgbF32Linear | Self::YCbCr8 | Self::YCbCrF32 => 3,
            Self::Rgbx8Srgb | Self::Bgrx8Srgb | Self::Rgbx16Linear
            | Self::RgbxF32Linear => 4,
        }
    }

    /// Whether this is a grayscale format.
    pub const fn is_grayscale(&self) -> bool {
        matches!(self, Self::Gray8Srgb | Self::Gray16Linear | Self::GrayF32Linear)
    }

    /// Whether this is pre-converted YCbCr.
    pub const fn is_ycbcr(&self) -> bool {
        matches!(self, Self::YCbCr8 | Self::YCbCrF32)
    }
}
```

---

## 6. Bytes Encoder

```rust
/// Encoder for raw byte input with explicit pixel layout.
pub struct BytesEncoder {
    config: EncoderConfig,
    layout: PixelLayout,
    width: u32,
    height: u32,
    rows_pushed: u32,
    // internal encoding state...
}

impl BytesEncoder {
    /// Push rows with explicit stride.
    ///
    /// - `data`: Raw pixel bytes
    /// - `rows`: Number of scanlines to push
    /// - `stride_bytes`: Bytes per row in buffer (≥ width × bytes_per_pixel)
    /// - `stop`: Cancellation token (use `enough::Never` if not needed)
    pub fn push(
        &mut self,
        data: &[u8],
        rows: usize,
        stride_bytes: usize,
        stop: impl Stop,
    ) -> Result<()>;

    /// Push contiguous (packed) data.
    ///
    /// Stride is assumed to be `width × bytes_per_pixel`.
    /// Rows inferred from `data.len() / (width × bytes_per_pixel)`.
    pub fn push_packed(&mut self, data: &[u8], stop: impl Stop) -> Result<()>;

    // === Status ===

    pub fn width(&self) -> u32 { self.width }
    pub fn height(&self) -> u32 { self.height }
    pub fn rows_pushed(&self) -> u32 { self.rows_pushed }
    pub fn rows_remaining(&self) -> u32 { self.height - self.rows_pushed }
    pub fn layout(&self) -> PixelLayout { self.layout }

    // === Finish ===

    /// Finish encoding, return JPEG bytes.
    pub fn finish(self) -> Result<Vec<u8>>;

    /// Finish encoding to Write destination.
    pub fn finish_to<W: Write>(self, output: W) -> Result<W>;
}
```

---

## 7. RGB Encoder (rgb crate types)

```rust
use rgb::{RGB, RGBA, BGR, BGRA, Gray};

/// Marker trait for supported rgb crate pixel types.
///
/// Implemented for: RGB<u8>, RGBA<u8>, BGR<u8>, BGRA<u8>, Gray<u8>,
/// RGB<u16>, RGBA<u16>, Gray<u16>, RGB<f32>, RGBA<f32>, Gray<f32>
pub trait Pixel: Copy + 'static {
    /// Equivalent PixelLayout for this type.
    const LAYOUT: PixelLayout;
}

impl Pixel for RGB<u8>  { const LAYOUT: PixelLayout = PixelLayout::Rgb8Srgb; }
impl Pixel for RGBA<u8> { const LAYOUT: PixelLayout = PixelLayout::Rgbx8Srgb; }
impl Pixel for BGR<u8>  { const LAYOUT: PixelLayout = PixelLayout::Bgr8Srgb; }
impl Pixel for BGRA<u8> { const LAYOUT: PixelLayout = PixelLayout::Bgrx8Srgb; }
impl Pixel for Gray<u8> { const LAYOUT: PixelLayout = PixelLayout::Gray8Srgb; }

impl Pixel for RGB<u16>  { const LAYOUT: PixelLayout = PixelLayout::Rgb16Linear; }
impl Pixel for RGBA<u16> { const LAYOUT: PixelLayout = PixelLayout::Rgbx16Linear; }
impl Pixel for Gray<u16> { const LAYOUT: PixelLayout = PixelLayout::Gray16Linear; }

impl Pixel for RGB<f32>  { const LAYOUT: PixelLayout = PixelLayout::RgbF32Linear; }
impl Pixel for RGBA<f32> { const LAYOUT: PixelLayout = PixelLayout::RgbxF32Linear; }
impl Pixel for Gray<f32> { const LAYOUT: PixelLayout = PixelLayout::GrayF32Linear; }

/// Encoder for rgb crate pixel types.
///
/// Type parameter P determines pixel layout at compile time.
/// For RGBA/BGRA types, 4th channel is ignored.
pub struct RgbEncoder<P: Pixel> {
    inner: BytesEncoder,
    _marker: PhantomData<P>,
}

impl<P: Pixel> RgbEncoder<P> {
    /// Push rows with explicit stride (in pixels).
    ///
    /// - `data`: Pixel slice
    /// - `rows`: Number of scanlines to push
    /// - `stride`: Pixels per row in buffer (≥ width)
    /// - `stop`: Cancellation token
    pub fn push(
        &mut self,
        data: &[P],
        rows: usize,
        stride: usize,
        stop: impl Stop,
    ) -> Result<()>;

    /// Push contiguous (packed) data.
    ///
    /// Stride assumed to be `width`. Rows inferred from `data.len() / width`.
    pub fn push_packed(&mut self, data: &[P], stop: impl Stop) -> Result<()>;

    // === Status ===

    pub fn width(&self) -> u32;
    pub fn height(&self) -> u32;
    pub fn rows_pushed(&self) -> u32;
    pub fn rows_remaining(&self) -> u32;

    // === Finish ===

    pub fn finish(self) -> Result<Vec<u8>>;
    pub fn finish_to<W: Write>(self, output: W) -> Result<W>;
}
```

---

## 8. Planar YCbCr Encoder

```rust
/// Planar YCbCr data for a strip of rows.
///
/// Each plane has its own stride. All planes are f32.
#[derive(Clone, Copy, Debug)]
pub struct YCbCrPlanes<'a> {
    pub y: &'a [f32],
    pub y_stride: usize,
    pub cb: &'a [f32],
    pub cb_stride: usize,
    pub cr: &'a [f32],
    pub cr_stride: usize,
}

/// Encoder for planar f32 YCbCr input.
///
/// Use when you have pre-converted YCbCr from video decoders, etc.
/// Skips RGB→YCbCr conversion entirely.
///
/// Only valid with `ColorMode::YCbCr`. XYB mode requires RGB input.
pub struct YCbCrPlanarEncoder {
    config: EncoderConfig,
    width: u32,
    height: u32,
    rows_pushed: u32,
    // internal state...
}

impl YCbCrPlanarEncoder {
    /// Push full-resolution planes. Encoder subsamples chroma as needed.
    ///
    /// - `planes`: Y, Cb, Cr plane data with per-plane strides
    /// - `rows`: Number of luma rows to push
    /// - `stop`: Cancellation token
    pub fn push(
        &mut self,
        planes: &YCbCrPlanes<'_>,
        rows: usize,
        stop: impl Stop,
    ) -> Result<()>;

    /// Push with pre-subsampled chroma.
    ///
    /// Cb/Cr are already at target chroma resolution.
    /// `y_rows` is luma row count; chroma rows derived from ChromaSubsampling.
    pub fn push_subsampled(
        &mut self,
        planes: &YCbCrPlanes<'_>,
        y_rows: usize,
        stop: impl Stop,
    ) -> Result<()>;

    // === Status ===

    pub fn width(&self) -> u32;
    pub fn height(&self) -> u32;
    pub fn rows_pushed(&self) -> u32;
    pub fn rows_remaining(&self) -> u32;

    // === Finish ===

    pub fn finish(self) -> Result<Vec<u8>>;
    pub fn finish_to<W: Write>(self, output: W) -> Result<W>;
}
```

---

## 9. Error Type

```rust
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
    /// Invalid encoder configuration.
    InvalidConfig(&'static str),

    /// Pixel layout incompatible with color mode.
    LayoutMismatch {
        layout: PixelLayout,
        color_mode: ColorMode,
    },

    /// Buffer too small for specified dimensions/rows.
    BufferTooSmall {
        expected: usize,
        actual: usize,
    },

    /// Stride too small for width.
    StrideTooSmall {
        width: u32,
        stride: usize,
    },

    /// Pushed more rows than image height.
    TooManyRows {
        height: u32,
        pushed: u32,
    },

    /// Encoding stopped via cancellation token.
    Stopped,

    /// Not all rows pushed before finish().
    IncompleteImage {
        height: u32,
        pushed: u32,
    },

    /// I/O error during finish_to().
    Io(std::io::Error),

    /// Internal encoding error.
    Internal(String),
}

pub type Result<T> = std::result::Result<T, Error>;
```

---

## 10. Usage Examples

```rust
use jpegli::{EncoderConfig, PixelLayout, ChromaSubsampling, Quality};
use enough::Never;
use rgb::{RGB, RGBA};

// ============================================
// Basic: raw bytes
// ============================================

let config = EncoderConfig::new().quality(85);
let mut enc = config.encode_from_bytes(1920, 1080, PixelLayout::Rgb8Srgb)?;
enc.push_packed(&rgb_bytes, Never)?;
let jpeg = enc.finish()?;

// ============================================
// Basic: rgb crate types
// ============================================

let pixels: Vec<RGB<u8>> = load_image();
let mut enc = config.encode_from_rgb::<RGB<u8>>(1920, 1080)?;
enc.push_packed(&pixels, Never)?;
let jpeg = enc.finish()?;

// ============================================
// With stride (e.g., GPU texture with row padding)
// ============================================

let stride_bytes = 2048 * 3;  // padded rows
let mut enc = config.encode_from_bytes(1920, 1080, PixelLayout::Rgb8Srgb)?;
enc.push(&padded_bytes, 1080, stride_bytes, Never)?;
let jpeg = enc.finish()?;

// rgb crate with stride (in pixels)
let stride = 2048;
let mut enc = config.encode_from_rgb::<RGB<u8>>(1920, 1080)?;
enc.push(&padded_pixels, 1080, stride, Never)?;

// ============================================
// Streaming with cancellation
// ============================================

use std::sync::atomic::AtomicBool;

let cancel = AtomicBool::new(false);
let mut enc = config.encode_from_rgb::<RGB<u8>>(w, h)?;

for chunk in pixels.chunks(64 * w as usize) {
    let rows = chunk.len() / w as usize;
    enc.push(chunk, rows, w as usize, &cancel)?;
}
let jpeg = enc.finish()?;

// ============================================
// High quality config
// ============================================

let hq = EncoderConfig::new()
    .quality(95)
    .progressive(true)
    .ycbcr(ChromaSubsampling::Full)
    .sharp_yuv(true);

// ============================================
// XYB mode
// ============================================

let xyb = EncoderConfig::new()
    .quality(90)
    .xyb();

let mut enc = xyb.encode_from_rgb::<RGB<f32>>(w, h)?;  // linear f32 input
enc.push_packed(&linear_pixels, Never)?;
let jpeg = enc.finish()?;

// ============================================
// Grayscale
// ============================================

let gray = EncoderConfig::new()
    .quality(85)
    .grayscale();

let mut enc = gray.encode_from_bytes(w, h, PixelLayout::Gray8Srgb)?;
enc.push_packed(&gray_bytes, Never)?;

// ============================================
// RGBA (4th channel ignored)
// ============================================

let mut enc = config.encode_from_rgb::<RGBA<u8>>(w, h)?;
enc.push_packed(&rgba_pixels, Never)?;  // alpha channel ignored
let jpeg = enc.finish()?;

// ============================================
// Planar YCbCr (video decoder output)
// ============================================

use jpegli::YCbCrPlanes;

let planes = YCbCrPlanes {
    y: &y_plane,
    y_stride: y_stride,
    cb: &cb_plane,
    cb_stride: cb_stride,
    cr: &cr_plane,
    cr_stride: cr_stride,
};

let mut enc = config.encode_from_ycbcr_planar(w, h)?;
enc.push(&planes, h as usize, Never)?;
let jpeg = enc.finish()?;

// ============================================
// Write to file
// ============================================

let file = File::create("output.jpg")?;
let mut enc = config.encode_from_rgb::<RGB<u8>>(w, h)?;
enc.push_packed(&pixels, Never)?;
enc.finish_to(file)?;

// ============================================
// Async with tokio
// ============================================

async fn encode_async(
    config: EncoderConfig,
    pixels: Vec<RGB<u8>>,
    w: u32,
    h: u32,
    cancel: tokio_util::sync::CancellationToken,
) -> jpegli::Result<Vec<u8>> {
    tokio::task::spawn_blocking(move || {
        let mut enc = config.encode_from_rgb::<RGB<u8>>(w, h)?;

        // Push in chunks, checking cancellation
        let chunk_rows = 64;
        for (i, chunk) in pixels.chunks(chunk_rows * w as usize).enumerate() {
            let rows = chunk.len() / w as usize;
            enc.push(chunk, rows, w as usize, &cancel)?;
        }

        enc.finish()
    }).await.map_err(|_| jpegli::Error::Internal("task join failed".into()))?
}
```

---

## 11. Migration from Current API

```rust
// Old API
JpegEncoder::new(w, h)
    .pixel_format(PixelFormat::Rgb)
    .quality(Quality::from_quality(85.0))
    .subsampling(Subsampling::S420)
    .encode(&pixels)?;

// New API
let config = EncoderConfig::new()
    .quality(85)
    .ycbcr(ChromaSubsampling::Quarter);
let mut enc = config.encode_from_rgb::<RGB<u8>>(w, h)?;
enc.push_packed(&pixels, Never)?;
enc.finish()?
```

**Key changes:**
- Config separate from dimensions (reusable)
- Pixel format from type or `PixelLayout` enum
- Stride explicit at push time
- Cancellation on every push
- `push_packed()` for contiguous data

---

## 12. Internal Implementation Notes

`RgbEncoder<P>` is a thin wrapper around `BytesEncoder`:

```rust
impl<P: Pixel> RgbEncoder<P> {
    pub fn push(&mut self, data: &[P], rows: usize, stride: usize, stop: impl Stop) -> Result<()> {
        let stride_bytes = stride * std::mem::size_of::<P>();
        let bytes = bytemuck::cast_slice(data);
        self.inner.push(bytes, rows, stride_bytes, stop)
    }

    pub fn push_packed(&mut self, data: &[P], stop: impl Stop) -> Result<()> {
        let bytes = bytemuck::cast_slice(data);
        self.inner.push_packed(bytes, stop)
    }
}
```

`BytesEncoder` dispatches to format-specific internal methods based on `PixelLayout`.
