jpegli-rs 0.12.0

Pure Rust JPEG encoder/decoder - port of Google's jpegli with perceptual optimizations
Documentation
//! JPEG encoder implementation.
//!
//! # Quick Start
//!
//! ```rust,ignore
//! use jpegli::{EncoderConfig, ChromaSubsampling, PixelLayout};
//!
//! // Create reusable config
//! let config = EncoderConfig::ycbcr(85.0, ChromaSubsampling::Quarter)
//!     .progressive(true);
//!
//! // Encode from raw bytes
//! let jpeg = config.encode_one(1920, 1080, PixelLayout::Rgb8Srgb, &rgb_bytes)?;
//! ```
//!
//! # Streaming Encoding
//!
//! For large images or when you want to process rows incrementally:
//!
//! ```rust,ignore
//! use jpegli::{EncoderConfig, ChromaSubsampling, PixelLayout};
//! use enough::Unstoppable;
//!
//! let config = EncoderConfig::ycbcr(85.0, ChromaSubsampling::Quarter);
//! let mut enc = config.encode_from_bytes(1920, 1080, PixelLayout::Rgb8Srgb)?;
//!
//! // Push rows (or use push_packed for all at once)
//! enc.push_packed(&rgb_bytes, Unstoppable)?;
//! let jpeg = enc.finish()?;
//! ```

#![allow(dead_code)]

// Internal implementation modules (pub for internal crate re-exports)
mod blocks;
#[doc(hidden)]
pub mod chroma;
#[doc(hidden)]
pub mod dct;
mod progressive;
#[doc(hidden)]
pub mod scan_script;
mod serialize;

#[doc(hidden)]
pub mod config;
#[cfg(feature = "experimental-hybrid-trellis")]
mod hybrid;
pub(crate) mod linear_lut;

// Archmage-based SIMD (token-based safe intrinsics)
#[cfg(all(feature = "archmage-simd", target_arch = "x86_64"))]
#[doc(hidden)]
pub mod mage_simd;

/// Overshoot deringing for reducing ringing artifacts on white backgrounds.
///
/// This module implements the deringing algorithm pioneered by @kornel in mozjpeg.
/// It smooths hard edges to reduce visible ringing artifacts, especially on white
/// backgrounds. Enabled by default with no quality penalty for photographic content.
pub mod deringing;
#[cfg(feature = "parallel")]
#[doc(hidden)]
pub mod parallel;
#[doc(hidden)]
pub mod streaming;
#[doc(hidden)]
pub mod strip;

// v2 types moved to encode root (v2/mod.rs re-exports these for compatibility)
pub mod byte_encoders;
pub mod encoder_config;
pub mod encoder_types;
pub mod exif;
pub mod extras;

/// Default quantization and zero-bias tables for customization.
///
/// This module exposes the internal default tables so users can modify them
/// rather than creating tables from scratch. Used with `tuning::EncodingTables`.
pub mod tables;

/// Encoding table tuning for optimization experiments.
///
/// This module provides fine-grained control over quantization and zero-bias
/// tables for researching better encoding parameters. See the README
/// "Table Optimization" section for research methodology.
pub mod tuning;

/// mozjpeg-compatible quantization table presets.
///
/// This module provides access to the 9 quantization table variants used by mozjpeg
/// (Robidoux, MSSIM, Klein, etc.) with the standard quality scaling formula.
///
/// Requires the `mozjpeg-tables` feature flag.
#[cfg(feature = "mozjpeg-tables")]
pub mod mozjpeg_tables;

/// mozjpeg-compatible API types.
///
/// This module provides types that mirror mozjpeg-rs's API for easier migration
/// and familiarity, including [`TrellisConfig`](mozjpeg_compat::TrellisConfig).
///
/// Requires the `experimental-hybrid-trellis` feature.
#[cfg(feature = "experimental-hybrid-trellis")]
pub mod mozjpeg_compat;

// v2 is the primary public API (types re-exported below)
#[doc(hidden)]
pub mod v2;

// Re-export v2 types at encode:: level for cleaner imports
// (Now from encoder_types, encoder_config, byte_encoders - v2 re-exports for compatibility)
#[allow(unused_imports)] // Public API re-exports
pub use byte_encoders::{BytesEncoder, Pixel, RgbEncoder, YCbCrPlanarEncoder};
#[allow(unused_imports)] // Public API re-export
pub use encoder_config::EncoderConfig;
#[cfg(feature = "parallel")]
#[allow(unused_imports)] // Public API re-export
pub use encoder_types::ParallelEncoding;
#[allow(unused_imports)] // Public API re-exports
pub use encoder_types::{
    ChromaSubsampling, ColorMode, DownsamplingMethod, PixelLayout, Quality, XybSubsampling,
    YCbCrPlanes,
};
pub use enough::Stop;
#[allow(unused_imports)] // Public API re-exports
pub use exif::{Exif, ExifFields, Orientation};
#[allow(unused_imports)] // Public API re-exports
pub use extras::{EncoderSegment, EncoderSegments, MpfImage};
#[cfg(feature = "mozjpeg-tables")]
#[allow(unused_imports)] // Public API re-exports
pub use mozjpeg_tables::{MozjpegTables, QuantTablePreset};

use crate::error::Result;

// Internal config types
pub(crate) use config::ProgressiveScan;

use crate::foundation::alloc::{try_alloc_zeroed_f32, try_clone_slice};
#[cfg(feature = "experimental-hybrid-trellis")]
use crate::foundation::consts::{DCT_BLOCK_SIZE, JPEG_ZIGZAG_ORDER};
use crate::types::{EdgePadding, EdgePaddingConfig};

// NOTE: The Encoder wrapper struct has been removed.
// All encoding methods are now implemented directly on ComputedConfig
// (see serialize.rs, blocks.rs, progressive.rs).
// StreamingEncoder stores ComputedConfig directly.

/// Converts coefficients from natural order to zigzag order, writing directly to destination.
/// Avoids allocation when writing to pre-allocated block arrays.
#[cfg(feature = "experimental-hybrid-trellis")]
#[inline]
fn natural_to_zigzag_into(natural: &[i16; DCT_BLOCK_SIZE], dest: &mut [i16; DCT_BLOCK_SIZE]) {
    for i in 0..DCT_BLOCK_SIZE {
        dest[JPEG_ZIGZAG_ORDER[i] as usize] = natural[i];
    }
}

// ============================================================================
// Edge Padding Helpers
// ============================================================================

/// Compute the source coordinate for a padded pixel using the specified strategy.
///
/// For coordinates within the original image, returns the coordinate unchanged.
/// For coordinates beyond the edge, applies the padding strategy.
#[inline]
fn get_padded_coord(coord: usize, size: usize, strategy: EdgePadding) -> usize {
    if coord < size {
        return coord;
    }

    match strategy {
        EdgePadding::Replicate => size - 1,
        EdgePadding::Mirror => {
            // Reflect: coord beyond edge mirrors back
            // For coord = size + d, return size - 1 - d
            let d = coord - size;
            size.saturating_sub(1).saturating_sub(d)
        }
        EdgePadding::Wrap => coord % size,
    }
}

/// Pad a single-channel f32 plane to MCU-aligned dimensions.
///
/// Returns (padded_plane, padded_width, padded_height).
/// If no padding is needed, returns a clone of the input.
pub(crate) fn pad_plane_f32(
    plane: &[f32],
    width: usize,
    height: usize,
    mcu_size: usize,
    strategy: EdgePadding,
) -> Result<(Vec<f32>, usize, usize)> {
    let padded_w = (width + mcu_size - 1) / mcu_size * mcu_size;
    let padded_h = (height + mcu_size - 1) / mcu_size * mcu_size;

    // No padding needed
    if padded_w == width && padded_h == height {
        return Ok((
            try_clone_slice(plane, "pad_plane_f32 clone")?,
            width,
            height,
        ));
    }

    let mut out = try_alloc_zeroed_f32(padded_w * padded_h, "pad_plane_f32 output")?;

    for y in 0..padded_h {
        let src_y = get_padded_coord(y, height, strategy);
        for x in 0..padded_w {
            let src_x = get_padded_coord(x, width, strategy);
            out[y * padded_w + x] = plane[src_y * width + src_x];
        }
    }

    Ok((out, padded_w, padded_h))
}

/// Pad YCbCr f32 planes to MCU-aligned dimensions with per-channel strategies.
///
/// Y plane uses the luma strategy, Cb/Cr planes use the chroma strategy.
/// Handles subsampled chroma planes correctly (cb/cr may have different dimensions than y).
///
/// Returns ((y, cb, cr), padded_luma_w, padded_luma_h, padded_chroma_w, padded_chroma_h).
#[allow(clippy::type_complexity)]
#[allow(dead_code)]
pub(crate) fn pad_ycbcr_planes_subsampled(
    y: &[f32],
    width: usize,
    height: usize,
    cb: &[f32],
    cr: &[f32],
    c_width: usize,
    c_height: usize,
    mcu_size: usize,
    config: EdgePaddingConfig,
) -> Result<((Vec<f32>, Vec<f32>, Vec<f32>), usize, usize, usize, usize)> {
    // Pad luma to MCU-aligned dimensions
    let (y_padded, padded_w, padded_h) = pad_plane_f32(y, width, height, mcu_size, config.luma)?;

    // Chroma blocks are always 8x8. Padding chroma to multiples of 8 aligns with
    // the MCU grid because c_width = ceil(width / h_factor) and:
    // ceil(ceil(width / h_factor) / 8) * 8 == ceil(width / mcu_size) * (mcu_size / h_factor)
    let (cb_padded, padded_cw, padded_ch) = pad_plane_f32(cb, c_width, c_height, 8, config.chroma)?;
    let (cr_padded, _, _) = pad_plane_f32(cr, c_width, c_height, 8, config.chroma)?;

    Ok((
        (y_padded, cb_padded, cr_padded),
        padded_w,
        padded_h,
        padded_cw,
        padded_ch,
    ))
}

/// Pad grayscale f32 plane to MCU-aligned dimensions.
///
/// Returns (padded_plane, padded_width, padded_height).
#[allow(dead_code)] // Kept for future grayscale encoding support
pub(crate) fn pad_gray_plane(
    y: &[f32],
    width: usize,
    height: usize,
    mcu_size: usize,
    config: EdgePaddingConfig,
) -> Result<(Vec<f32>, usize, usize)> {
    pad_plane_f32(y, width, height, mcu_size, config.luma)
}

// Tests are in the old module (old/tests.rs)