//! PDF image module - XObject images, inline images, and format conversion

use crate::fitz::error::{Error, Result};
use crate::pdf::object::{Array, Dict, Name, ObjRef, Object};
use std::collections::HashMap;
use std::convert::TryFrom;

// ============================================================================
// BPC Validation
// ============================================================================

/// Valid PDF BitsPerComponent values: 1, 2, 4, 8, or 16.
/// Returns error if bpc is invalid (avoids shift overflow for bpc >= 32).
fn validate_bpc(bpc: u32) -> Result<()> {
    if !matches!(bpc, 1 | 2 | 4 | 8 | 16) {
        return Err(Error::image("BitsPerComponent must be 1, 2, 4, 8, or 16"));
    }
    Ok(())
}

/// Safe max value for bpc-bit component: (1 << bpc) - 1.
/// Call validate_bpc before using.
fn max_val_for_bpc(bpc: u32) -> Result<u32> {
    validate_bpc(bpc)?;
    Ok((1u32 << bpc) - 1)
}

/// Parse JPEG header for dimensions and component count without full decode.
/// Validates SOI (0xFF 0xD8) and scans for SOF0 (0xFF 0xC0) or SOF2 (0xFF 0xC2).
/// Returns (width, height, num_components).
fn jpeg_dimensions_and_components_from_header(data: &[u8]) -> Result<(u32, u32, u8)> {
    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
        return Err(Error::image("Invalid JPEG: missing SOI marker (0xFF 0xD8)"));
    }
    let mut i = 2;
    while i + 2 <= data.len() {
        if data[i] != 0xFF {
            i += 1;
            continue;
        }
        let marker = data[i + 1];
        i += 2;
        if marker == 0x00 {
            continue; // Stuffing byte
        }
        if marker == 0xD8 || marker == 0xD9 {
            continue; // SOI or EOI, no length
        }
        if marker == 0xC0 || marker == 0xC2 {
            // SOF0 (baseline) or SOF2 (progressive)
            // 1. Must have 2 bytes for segment length field
            if i + 2 > data.len() {
                return Err(Error::image(
                    "Invalid JPEG: truncated SOF segment (no length)",
                ));
            }
            let seg_len = (u16::from(data[i]) << 8) | u16::from(data[i + 1]);
            // 2. Min SOF size: 2 (length) + 1 (precision) + 2 (height) + 2 (width) + 1 (components) = 8
            if seg_len < 8 {
                return Err(Error::image(
                    "Invalid JPEG: SOF segment length must be >= 8",
                ));
            }
            let seg_len_usize = seg_len as usize;
            // 3. Must have full segment in buffer (use checked add to avoid overflow)
            let seg_end = i
                .checked_add(seg_len_usize)
                .ok_or_else(|| Error::image("Invalid JPEG: SOF segment length overflow"))?;
            if seg_end > data.len() {
                return Err(Error::image("Invalid JPEG: truncated SOF segment"));
            }
            let height = (u32::from(data[i + 3]) << 8) | u32::from(data[i + 4]);
            let width = (u32::from(data[i + 5]) << 8) | u32::from(data[i + 6]);
            let num_components = data[i + 7];
            let components = match num_components {
                1 => 1u8,
                3 => 3u8,
                4 => 4u8,
                n => {
                    return Err(Error::image(format!(
                        "Unsupported JPEG component count: {} (expected 1, 3, or 4)",
                        n
                    )));
                }
            };
            return Ok((width, height, components));
        }
        if i + 2 > data.len() {
            return Err(Error::image("Invalid JPEG: truncated marker"));
        }
        let len = (u16::from(data[i]) << 8) | u16::from(data[i + 1]);
        if len < 2 {
            return Err(Error::image("Invalid JPEG: marker length must be >= 2"));
        }
        let skip = usize::from(len)
            .checked_sub(2)
            .ok_or_else(|| Error::image("Invalid JPEG: marker length underflow"))?;
        let new_i = i
            .checked_add(2)
            .and_then(|n| n.checked_add(skip))
            .ok_or_else(|| Error::image("Invalid JPEG: marker offset overflow"))?;
        if new_i > data.len() {
            return Err(Error::image("Invalid JPEG: marker extends past end"));
        }
        i = new_i;
    }
    Err(Error::image("Invalid JPEG: no SOF0/SOF2 marker found"))
}

/// Parse JPEG header for dimensions without full decode (legacy wrapper).
fn jpeg_dimensions_from_header(data: &[u8]) -> Result<(u32, u32)> {
    let (w, h, _) = jpeg_dimensions_and_components_from_header(data)?;
    Ok((w, h))
}

// ============================================================================
// Image Type
// ============================================================================

/// PDF image encoding/compression type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageType {
    /// DCTDecode - JPEG
    Jpeg,
    /// FlateDecode with predictor - PNG-like
    Png,
    /// JPXDecode - JPEG 2000
    Jpeg2000,
    /// JBIG2Decode
    Jbig2,
    /// CCITTFaxDecode
    CcittFax,
    /// Uncompressed raw
    Raw,
}

impl ImageType {
    /// Returns true for compression filters we do not decode (must error).
    fn is_unsupported_filter(name: &str) -> bool {
        matches!(
            name,
            "ASCIIHexDecode"
                | "AHx"
                | "ASCII85Decode"
                | "A85"
                | "LZWDecode"
                | "LZW"
                | "RunLengthDecode"
                | "RL"
        )
    }

    /// Returns Some for recognized filter names, None for unknown.
    fn from_filter_name(name: &str) -> Option<Self> {
        match name {
            "DCTDecode" | "DCT" => Some(Self::Jpeg),
            "FlateDecode" | "Fl" => Some(Self::Png),
            "JPXDecode" | "JPX" => Some(Self::Jpeg2000),
            "JBIG2Decode" | "JBIG2" => Some(Self::Jbig2),
            "CCITTFaxDecode" | "CCF" => Some(Self::CcittFax),
            "ASCIIHexDecode" | "AHx" | "ASCII85Decode" | "A85" | "LZWDecode" | "LZW"
            | "RunLengthDecode" | "RL" => None, // Unsupported - need decoding
            _ => None,
        }
    }

    fn to_filter_name(&self) -> Option<&'static str> {
        match self {
            Self::Jpeg => Some("DCTDecode"),
            Self::Png => Some("FlateDecode"),
            Self::Jpeg2000 => Some("JPXDecode"),
            Self::Jbig2 => Some("JBIG2Decode"),
            Self::CcittFax => Some("CCITTFaxDecode"),
            Self::Raw => None,
        }
    }
}

// ============================================================================
// Image Mask
// ============================================================================

/// PDF image mask type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImageMask {
    /// No mask
    None,
    /// 1-bit stencil mask (/ImageMask true)
    Stencil,
    /// Soft mask reference (/SMask)
    SoftMask { obj_ref: ObjRef },
    /// Hard mask reference (/Mask - image)
    HardMask { obj_ref: ObjRef },
    /// Color key mask (/Mask [lo hi ...])
    ColorKeyMask { ranges: Vec<(u32, u32)> },
}

// ============================================================================
// PdfImage
// ============================================================================

/// PDF image XObject or inline image representation
#[derive(Debug, Clone)]
pub struct PdfImage {
    pub width: u32,
    pub height: u32,
    pub bits_per_component: u8,
    pub image_type: ImageType,
    pub colorspace_name: Option<String>,
    pub num_components: u8,
    pub mask: ImageMask,
    pub interpolate: bool,
    pub decode: Option<Vec<f64>>,
    pub data: Vec<u8>,
    pub is_inline: bool,
}

impl PdfImage {
    /// Parse image from XObject dictionary and decoded stream data
    pub fn from_pdf_dict(dict: &Dict, data: Vec<u8>) -> Result<Self> {
        let width = dict
            .get(&Name::new("Width"))
            .and_then(|o| o.as_int())
            .ok_or_else(|| Error::image("Missing /Width"))?;
        let width = u32::try_from(width).map_err(|_| Error::image("Width out of range for u32"))?;

        let height = dict
            .get(&Name::new("Height"))
            .and_then(|o| o.as_int())
            .ok_or_else(|| Error::image("Missing /Height"))?;
        let height =
            u32::try_from(height).map_err(|_| Error::image("Height out of range for u32"))?;

        let bits_per_component = match dict
            .get(&Name::new("BitsPerComponent"))
            .and_then(|o| o.as_int())
        {
            Some(n) => {
                u8::try_from(n).map_err(|_| Error::image("BitsPerComponent out of range"))?
            }
            None => {
                if dict.get(&Name::new("ImageMask")).and_then(|o| o.as_bool()) == Some(true) {
                    1
                } else {
                    return Err(Error::image(
                        "Missing BitsPerComponent for non-ImageMask image",
                    ));
                }
            }
        };
        validate_bpc(u32::from(bits_per_component))?;

        let is_image_mask =
            dict.get(&Name::new("ImageMask")).and_then(|o| o.as_bool()) == Some(true);
        let (colorspace_name, num_components) = Self::parse_colorspace(dict, is_image_mask)?;

        let image_type = match dict.get(&Name::new("Filter")) {
            Some(Object::Name(n)) => {
                let name = n.as_str();
                if ImageType::is_unsupported_filter(name) {
                    return Err(Error::image(format!("Unsupported image filter: {}", name)));
                }
                ImageType::from_filter_name(name)
                    .ok_or_else(|| Error::image(format!("Unknown image filter: {}", name)))?
            }
            Some(Object::Array(arr)) => {
                if arr.len() > 1 {
                    return Err(Error::image(
                        "Filter chains (multiple filters) are not supported",
                    ));
                }
                let first = arr
                    .get(0)
                    .and_then(|o| o.as_name())
                    .map(|n| n.as_str().to_string());
                match first.as_deref() {
                    Some(name) => {
                        if ImageType::is_unsupported_filter(name) {
                            return Err(Error::image(format!(
                                "Unsupported image filter: {}",
                                name
                            )));
                        }
                        ImageType::from_filter_name(name).ok_or_else(|| {
                            Error::image(format!("Unknown image filter: {}", name))
                        })?
                    }
                    None => ImageType::Raw,
                }
            }
            _ => ImageType::Raw,
        };

        let mask = Self::parse_mask(dict, num_components, bits_per_component)?;

        let interpolate = dict
            .get(&Name::new("Interpolate"))
            .and_then(|o| o.as_bool())
            .unwrap_or(false);

        let decode = dict
            .get(&Name::new("Decode"))
            .and_then(|o| o.as_array())
            .map(|arr| arr.iter().filter_map(|o| o.as_real()).collect::<Vec<f64>>())
            .filter(|v| !v.is_empty());

        let img = Self {
            width,
            height,
            bits_per_component,
            image_type,
            colorspace_name,
            num_components,
            mask,
            interpolate,
            decode,
            data,
            is_inline: false,
        };
        if let Ok(expected) = img.data_size() {
            if img.data.len() < expected {
                return Err(Error::image(format!(
                    "Image data too short: expected {}, got {}",
                    expected,
                    img.data.len()
                )));
            }
        }
        Ok(img)
    }

    fn parse_colorspace(dict: &Dict, is_image_mask: bool) -> Result<(Option<String>, u8)> {
        let cs_obj = dict.get(&Name::new("ColorSpace"));
        let (name, components) = match cs_obj {
            Some(Object::Name(n)) => {
                let s = n.as_str();
                let comps = match s {
                    "DeviceGray" | "G" => 1,
                    "DeviceRGB" | "RGB" => 3,
                    "DeviceCMYK" | "CMYK" => 4,
                    other => {
                        return Err(Error::image(format!("Unknown colorspace: {}", other)));
                    }
                };
                (Some(s.to_string()), comps)
            }
            Some(Object::Array(arr)) => {
                let first = arr
                    .first()
                    .and_then(|o| o.as_name())
                    .map(|n| n.as_str().to_string());
                let comps = match first.as_deref() {
                    Some("DeviceGray") | Some("G") => 1,
                    Some("DeviceRGB") | Some("RGB") => 3,
                    Some("DeviceCMYK") | Some("CMYK") => 4,
                    Some("Indexed") => 1,
                    Some(other) => {
                        return Err(Error::image(format!("Unknown colorspace: {}", other)));
                    }
                    None => {
                        return Err(Error::image("ColorSpace array has no valid name"));
                    }
                };
                (first, comps)
            }
            None => {
                if is_image_mask {
                    (None, 1)
                } else {
                    return Err(Error::image("Missing ColorSpace for non-ImageMask image"));
                }
            }
            _ => {
                if is_image_mask {
                    (None, 1)
                } else {
                    return Err(Error::image("Missing ColorSpace for non-ImageMask image"));
                }
            }
        };
        Ok((name, components))
    }

    fn parse_mask(dict: &Dict, num_components: u8, bits_per_component: u8) -> Result<ImageMask> {
        if let Some(Object::Bool(true)) = dict.get(&Name::new("ImageMask")) {
            return Ok(ImageMask::Stencil);
        }

        if let Some(Object::Ref(r)) = dict.get(&Name::new("SMask")) {
            return Ok(ImageMask::SoftMask {
                obj_ref: ObjRef::new(r.num, r.generation),
            });
        }

        if let Some(obj) = dict.get(&Name::new("Mask")) {
            if let Object::Ref(r) = obj {
                return Ok(ImageMask::HardMask {
                    obj_ref: ObjRef::new(r.num, r.generation),
                });
            }
            if let Object::Array(arr) = obj {
                if arr.len() % 2 != 0 {
                    return Err(Error::image("Color key mask array must have even length"));
                }
                let comps = num_components.max(1) as usize;
                let expected_len = comps
                    .checked_mul(2)
                    .ok_or_else(|| Error::image("Color key mask size overflow"))?;
                if arr.len() != expected_len {
                    return Err(Error::image(format!(
                        "Color key mask array length {} does not match 2×{} color components (expected {})",
                        arr.len(),
                        comps,
                        expected_len
                    )));
                }
                let max_val = max_val_for_bpc(u32::from(bits_per_component))?;
                let mut ranges = Vec::with_capacity(comps);
                let mut i = 0;
                while i + 1 < arr.len() {
                    let lo = match arr.get(i).and_then(|o| o.as_int()) {
                        Some(n) if n < 0 => {
                            return Err(Error::image(format!(
                                "Color key mask value must be non-negative, got {}",
                                n
                            )));
                        }
                        Some(n) => n as u32,
                        None => {
                            return Err(Error::image("Color key mask values must be integers"));
                        }
                    };
                    let hi = match arr.get(i + 1).and_then(|o| o.as_int()) {
                        Some(n) if n < 0 => {
                            return Err(Error::image(format!(
                                "Color key mask value must be non-negative, got {}",
                                n
                            )));
                        }
                        Some(n) => n as u32,
                        None => {
                            return Err(Error::image("Color key mask values must be integers"));
                        }
                    };
                    if lo > max_val || hi > max_val {
                        return Err(Error::image(format!(
                            "Color key mask value out of range: lo={}, hi={} (max for {}-bit is {})",
                            lo, hi, bits_per_component, max_val
                        )));
                    }
                    // Per PDF spec, lo > hi means no pixels match; valid (though unusual).
                    ranges.push((lo, hi));
                    i += 2;
                }
                return Ok(ImageMask::ColorKeyMask { ranges });
            }
        }

        Ok(ImageMask::None)
    }

    /// Convert image data to RGBA bytes.
    ///
    /// **Limitations:** Masks (stencil, soft mask, hard mask, color key) are not applied.
    /// Decode arrays are not applied. Use a higher-level renderer for full PDF image semantics.
    pub fn to_rgba(&self) -> Result<Vec<u8>> {
        validate_bpc(u32::from(self.bits_per_component))?;
        let bpc = self.bits_per_component as usize;

        let components = self.num_components.max(1) as usize;
        let pixel_count = (self.width as usize)
            .checked_mul(self.height as usize)
            .ok_or_else(|| Error::image("Image dimensions overflow"))?;
        let expected_data_len = self.data_size()?;
        if self.data.len() < expected_data_len {
            let short_by = expected_data_len - self.data.len();
            let row_bytes = if self.height > 0 {
                expected_data_len / (self.height as usize)
            } else {
                expected_data_len
            };
            // More than 10% short: error
            if short_by > expected_data_len / 10 {
                return Err(Error::image(format!(
                    "Image data too short: expected {} bytes, got {}",
                    expected_data_len,
                    self.data.len()
                )));
            }
            // More than 1 row short: error
            if short_by > row_bytes {
                return Err(Error::image(format!(
                    "Image data too short: expected {} bytes, got {}",
                    expected_data_len,
                    self.data.len()
                )));
            }
            // Within 1 row short: allow and pad silently below
        }
        let max_val = max_val_for_bpc(u32::from(self.bits_per_component))? as f64;

        let rgba_capacity = pixel_count
            .checked_mul(4)
            .ok_or_else(|| Error::image("RGBA buffer size overflow (pixel_count × 4)"))?;

        let cs_name = self.colorspace_name.as_deref().unwrap_or("DeviceRGB");

        let mut rgba = match (cs_name, bpc) {
            ("DeviceGray" | "G", 8) => {
                let mut out = Vec::with_capacity(rgba_capacity);
                let limit = self.data.len().min(pixel_count);
                for i in 0..limit {
                    let g = self.data.get(i).copied().unwrap_or(0);
                    out.extend_from_slice(&[g, g, g, 255]);
                }
                out
            }
            ("DeviceGray" | "G", 1) => {
                let mut out = Vec::with_capacity(rgba_capacity);
                let bits = &self.data;
                for i in 0..pixel_count {
                    let byte_idx = i / 8;
                    // Big-endian bit order per PDF spec 8.9.3: high bit first within each byte
                    let bit_idx = 7 - (i % 8);
                    let bit = bits.get(byte_idx).map(|b| (b >> bit_idx) & 1).unwrap_or(0);
                    let g = if bit != 0 { 255 } else { 0 };
                    out.extend_from_slice(&[g, g, g, 255]);
                }
                out
            }
            ("DeviceRGB" | "RGB", 8) => {
                let mut out = Vec::with_capacity(rgba_capacity);
                let stride = components.min(3);
                let required = pixel_count
                    .checked_mul(stride)
                    .ok_or_else(|| Error::image("RGB data size overflow"))?;
                let available_pixels = if self.data.len() >= required {
                    pixel_count
                } else {
                    self.data.len() / stride
                };
                for chunk in self.data.chunks(stride).take(available_pixels) {
                    let r = chunk.get(0).copied().unwrap_or(0);
                    let g = chunk.get(1).copied().unwrap_or(0);
                    let b = chunk.get(2).copied().unwrap_or(0);
                    out.extend_from_slice(&[r, g, b, 255]);
                }
                out
            }
            ("DeviceCMYK" | "CMYK", 8) => {
                let mut out = Vec::with_capacity(rgba_capacity);
                let stride = 4.min(components);
                for chunk in self.data.chunks(stride).take(pixel_count) {
                    let c = chunk.get(0).copied().unwrap_or(0) as f64 / 255.0;
                    let m = chunk.get(1).copied().unwrap_or(0) as f64 / 255.0;
                    let y = chunk.get(2).copied().unwrap_or(0) as f64 / 255.0;
                    let k = chunk.get(3).copied().unwrap_or(0) as f64 / 255.0;
                    let r = (1.0 - c) * (1.0 - k);
                    let g = (1.0 - m) * (1.0 - k);
                    let b = (1.0 - y) * (1.0 - k);
                    out.extend_from_slice(&[
                        (r * 255.0).clamp(0.0, 255.0) as u8,
                        (g * 255.0).clamp(0.0, 255.0) as u8,
                        (b * 255.0).clamp(0.0, 255.0) as u8,
                        255,
                    ]);
                }
                out
            }
            _ => {
                let mut out = Vec::with_capacity(rgba_capacity);
                let bytes_per_component = (bpc + 7) / 8;
                let bytes_per_pixel = components
                    .checked_mul(bytes_per_component)
                    .ok_or_else(|| Error::image("Bytes per pixel overflow"))?;
                for chunk in self.data.chunks(bytes_per_pixel).take(pixel_count) {
                    let mut vals = [0.0f64; 4];
                    for (i, v) in vals.iter_mut().take(components.min(4)).enumerate() {
                        let start = i
                            .checked_mul(bytes_per_component)
                            .ok_or_else(|| Error::image("Component offset overflow"))?;
                        let end = start
                            .checked_add(bytes_per_component)
                            .ok_or_else(|| Error::image("Component end offset overflow"))?;
                        if end <= chunk.len() {
                            let mut n = 0u32;
                            for j in start..end {
                                if let Some(&b) = chunk.get(j) {
                                    n = (n << 8) | u32::from(b);
                                }
                            }
                            *v = n as f64 / max_val;
                        }
                    }
                    let (r, g, b) = match cs_name {
                        "DeviceGray" | "G" => (vals[0], vals[0], vals[0]),
                        "DeviceRGB" | "RGB" => (vals[0], vals[1], vals[2]),
                        "DeviceCMYK" | "CMYK" => {
                            let (c, m, y, k) = (vals[0], vals[1], vals[2], vals[3]);
                            (
                                (1.0 - c) * (1.0 - k),
                                (1.0 - m) * (1.0 - k),
                                (1.0 - y) * (1.0 - k),
                            )
                        }
                        _ => (vals[0], vals[1], vals[2]),
                    };
                    out.extend_from_slice(&[
                        (r * 255.0).clamp(0.0, 255.0) as u8,
                        (g * 255.0).clamp(0.0, 255.0) as u8,
                        (b * 255.0).clamp(0.0, 255.0) as u8,
                        255,
                    ]);
                }
                out
            }
        };

        // Ensure we wrote all pixels; fill any remaining with black + full alpha
        if rgba.len() < rgba_capacity {
            for _ in 0..((rgba_capacity - rgba.len()) / 4) {
                rgba.extend_from_slice(&[0, 0, 0, 255]);
            }
        }

        Ok(rgba)
    }

    /// Return (width, height)
    pub fn dimensions(&self) -> (u32, u32) {
        (self.width, self.height)
    }

    /// Total pixel count
    pub fn pixel_count(&self) -> Result<u64> {
        let w = u64::from(self.width);
        let h = u64::from(self.height);
        w.checked_mul(h)
            .ok_or_else(|| Error::image("Pixel count overflow"))
    }

    /// Expected raw data size in bytes based on dimensions and bit depth
    pub fn data_size(&self) -> Result<usize> {
        let components = self.num_components.max(1) as usize;
        let bpc = self.bits_per_component.max(1) as usize;
        let bits_per_pixel = components
            .checked_mul(bpc)
            .ok_or_else(|| Error::image("Bits per pixel overflow"))?;
        let row_bits = (self.width as usize)
            .checked_mul(bits_per_pixel)
            .ok_or_else(|| Error::image("Row bits overflow"))?;
        let row_bytes = (row_bits + 7) / 8;
        row_bytes
            .checked_mul(self.height as usize)
            .ok_or_else(|| Error::image("Image data size overflow"))
    }

    /// Create JPEG image for embedding (validates header and extracts dimensions).
    /// Uses header-only parsing (SOI + SOF0/SOF2) instead of full decode for performance.
    pub fn create_jpeg(data: &[u8], width: u32, height: u32) -> Result<Self> {
        let (w, h, components) = jpeg_dimensions_and_components_from_header(data)?;
        if width != w || height != h {
            return Err(Error::image(format!(
                "Dimension mismatch: expected {}x{}, got {}x{}",
                width, height, w, h
            )));
        }
        let colorspace_name = match components {
            1 => "DeviceGray",
            3 => "DeviceRGB",
            4 => "DeviceCMYK",
            _ => unreachable!(),
        };
        Ok(Self {
            width: w,
            height: h,
            bits_per_component: 8,
            image_type: ImageType::Jpeg,
            colorspace_name: Some(colorspace_name.to_string()),
            num_components: components,
            mask: ImageMask::None,
            interpolate: false,
            decode: None,
            data: data.to_vec(),
            is_inline: false,
        })
    }

    /// Create raw RGB image for embedding
    pub fn create_rgb(data: &[u8], width: u32, height: u32) -> Result<Self> {
        let expected = (width as usize)
            .checked_mul(height as usize)
            .and_then(|n| n.checked_mul(3))
            .ok_or_else(|| Error::image("RGB image dimensions overflow"))?;
        if data.len() < expected {
            return Err(Error::image(format!(
                "RGB data too short: need {} bytes, got {}",
                expected,
                data.len()
            )));
        }
        let data = data[..expected].to_vec();
        Ok(Self {
            width,
            height,
            bits_per_component: 8,
            image_type: ImageType::Raw,
            colorspace_name: Some("DeviceRGB".to_string()),
            num_components: 3,
            mask: ImageMask::None,
            interpolate: false,
            decode: None,
            data,
            is_inline: false,
        })
    }

    /// Generate XObject Image dictionary for embedding
    pub fn to_pdf_dict(&self) -> Dict {
        let mut dict = HashMap::new();
        dict.insert(Name::new("Type"), Object::Name(Name::new("XObject")));
        dict.insert(Name::new("Subtype"), Object::Name(Name::new("Image")));
        dict.insert(Name::new("Width"), Object::Int(self.width as i64));
        dict.insert(Name::new("Height"), Object::Int(self.height as i64));
        dict.insert(
            Name::new("BitsPerComponent"),
            Object::Int(self.bits_per_component as i64),
        );
        if let Some(ref cs) = self.colorspace_name {
            dict.insert(
                Name::new("ColorSpace"),
                Object::Name(Name::new(cs.as_str())),
            );
        }
        if let Some(filter) = self.image_type.to_filter_name() {
            dict.insert(Name::new("Filter"), Object::Name(Name::new(filter)));
        }
        if self.interpolate {
            dict.insert(Name::new("Interpolate"), Object::Bool(true));
        }
        if let Some(ref decode) = self.decode {
            dict.insert(
                Name::new("Decode"),
                Object::Array(decode.iter().map(|&f| Object::Real(f)).collect::<Array>()),
            );
        }
        dict
    }
}

// ============================================================================
// InlineImage
// ============================================================================

/// Inline image with abbreviated key dictionary
#[derive(Debug, Clone)]
pub struct InlineImage {
    pub dict: Dict,
    pub data: Vec<u8>,
}

impl InlineImage {
    /// Parse inline image from content stream bytes starting after BI
    /// Returns (InlineImage, total bytes consumed including ID, data, EI)
    pub fn parse(data: &[u8]) -> Result<(Self, usize)> {
        const MAX_DICT_ENTRIES: usize = 100;
        const MAX_DICT_BYTES: usize = 4096;

        let mut pos = 0;
        let mut dict = Dict::new();
        let mut dict_entry_count = 0usize;

        while pos <= MAX_DICT_BYTES && pos < data.len() {
            pos = skip_whitespace(data, pos);
            if pos >= data.len() {
                break;
            }
            if matches_keyword(data, pos, "ID") {
                pos += 2;
                pos = skip_whitespace(data, pos);
                break;
            }
            if dict_entry_count >= MAX_DICT_ENTRIES {
                return Err(Error::limit(format!(
                    "Inline image dictionary exceeds maximum entries ({})",
                    MAX_DICT_ENTRIES
                )));
            }
            if data[pos] == b'/' {
                pos += 1;
                let key_start = pos;
                while pos < data.len() && !is_whitespace(data[pos]) && data[pos] != b'/' {
                    pos += 1;
                }
                let key = std::str::from_utf8(&data[key_start..pos])
                    .map_err(|_| Error::syntax("Invalid inline image key"))?
                    .to_string();
                pos = skip_whitespace(data, pos);
                if pos >= data.len() {
                    break;
                }
                let (val_obj, consumed) = parse_inline_value(data, pos)?;
                dict.insert(Name::new(expand_inline_key(&key).as_str()), val_obj);
                dict_entry_count += 1;
                pos += consumed;
            } else {
                pos += 1;
            }
        }

        if pos > MAX_DICT_BYTES {
            return Err(Error::limit(format!(
                "Inline image dictionary exceeds maximum size ({} bytes)",
                MAX_DICT_BYTES
            )));
        }

        const INLINE_IMAGE_MAX_SIZE: usize = 102_400; // 100KB DoS limit
        let data_start = pos;
        let search_limit = data_start
            .checked_add(INLINE_IMAGE_MAX_SIZE)
            .ok_or_else(|| Error::image("Inline image offset overflow"))?
            .min(data.len());
        let data_end = find_ei_marker(data, pos, search_limit)
            .ok_or_else(|| Error::syntax("Inline image: EI marker not found"))?;
        let image_data_len = data_end.saturating_sub(data_start);
        if image_data_len > INLINE_IMAGE_MAX_SIZE {
            return Err(Error::limit(format!(
                "Inline image data exceeds maximum size ({} bytes)",
                INLINE_IMAGE_MAX_SIZE
            )));
        }
        let image_data = data[data_start..data_end].to_vec();
        // Consume EI marker: "EI" (empty image) or " EI" (space + EI)
        let ei_len = if data_end + 2 <= data.len() && data[data_end..].starts_with(b"EI") {
            2
        } else if data_end + 3 <= data.len()
            && is_pdf_delimiter_whitespace(data[data_end])
            && data[data_end + 1..].starts_with(b"EI")
        {
            3
        } else {
            0
        };
        let total = data_end + ei_len;

        Ok((
            Self {
                dict,
                data: image_data,
            },
            total,
        ))
    }

    /// Convert inline image to PdfImage by expanding abbreviated keys
    pub fn to_pdf_image(&self) -> Result<PdfImage> {
        let mut full_dict = Dict::new();
        for (k, v) in &self.dict {
            full_dict.insert(k.clone(), v.clone());
        }
        let as_int_or_real =
            |o: &Object| -> Option<i64> { o.as_int().or_else(|| o.as_real().map(|r| r as i64)) };
        let width = self
            .dict
            .get(&Name::new("Width"))
            .or_else(|| self.dict.get(&Name::new("W")))
            .and_then(as_int_or_real)
            .ok_or_else(|| Error::image("Inline image missing Width"))?;
        let width = u32::try_from(width)
            .map_err(|_| Error::image("Inline image Width out of range for u32"))?;
        let height = self
            .dict
            .get(&Name::new("Height"))
            .or_else(|| self.dict.get(&Name::new("H")))
            .and_then(as_int_or_real)
            .ok_or_else(|| Error::image("Inline image missing Height"))?;
        let height = u32::try_from(height)
            .map_err(|_| Error::image("Inline image Height out of range for u32"))?;
        let is_image_mask = self
            .dict
            .get(&Name::new("ImageMask"))
            .or_else(|| self.dict.get(&Name::new("IM")))
            .and_then(|o| o.as_bool())
            == Some(true);
        let bpc = match self
            .dict
            .get(&Name::new("BitsPerComponent"))
            .or_else(|| self.dict.get(&Name::new("BPC")))
            .and_then(as_int_or_real)
        {
            Some(n) => u8::try_from(n)
                .map_err(|_| Error::image("Inline image BitsPerComponent out of range"))?,
            None => {
                if is_image_mask {
                    1
                } else {
                    return Err(Error::image(
                        "Inline image missing BitsPerComponent for non-ImageMask",
                    ));
                }
            }
        };
        validate_bpc(u32::from(bpc))?;

        let cs_obj = self
            .dict
            .get(&Name::new("ColorSpace"))
            .or_else(|| self.dict.get(&Name::new("CS")));
        let (colorspace_name, num_components) = match cs_obj {
            Some(Object::Name(n)) => {
                let s = expand_inline_value("CS", n.as_str());
                let comps = match s.as_str() {
                    "DeviceGray" => 1,
                    "DeviceRGB" => 3,
                    "DeviceCMYK" => 4,
                    other => {
                        return Err(Error::image(format!(
                            "Unknown colorspace in inline image: {}",
                            other
                        )));
                    }
                };
                (Some(s), comps)
            }
            None => {
                if is_image_mask {
                    (None, 1)
                } else {
                    return Err(Error::image(
                        "Missing ColorSpace for non-ImageMask inline image",
                    ));
                }
            }
            _ => {
                if is_image_mask {
                    (None, 1)
                } else {
                    return Err(Error::image(
                        "Missing ColorSpace for non-ImageMask inline image",
                    ));
                }
            }
        };

        let image_type = match self
            .dict
            .get(&Name::new("Filter"))
            .or_else(|| self.dict.get(&Name::new("F")))
        {
            Some(Object::Name(n)) => {
                let name = expand_inline_value("F", n.as_str());
                if ImageType::is_unsupported_filter(&name) {
                    return Err(Error::image(format!("Unsupported image filter: {}", name)));
                }
                ImageType::from_filter_name(&name)
                    .ok_or_else(|| Error::image(format!("Unknown inline image filter: {}", name)))?
            }
            Some(Object::Array(arr)) => {
                if arr.len() > 1 {
                    return Err(Error::image(
                        "Filter chains (multiple filters) are not supported",
                    ));
                }
                let first_name = arr
                    .get(0)
                    .and_then(|o| o.as_name())
                    .map(|n| expand_inline_value("F", n.as_str()));
                match first_name.as_deref() {
                    Some(name) => {
                        if ImageType::is_unsupported_filter(name) {
                            return Err(Error::image(format!(
                                "Unsupported image filter: {}",
                                name
                            )));
                        }
                        match ImageType::from_filter_name(name) {
                            Some(it) => it,
                            None => {
                                return Err(Error::image(format!(
                                    "Unknown inline image filter: {}",
                                    name
                                )));
                            }
                        }
                    }
                    None => ImageType::Raw,
                }
            }
            _ => ImageType::Raw,
        };

        let mask = PdfImage::parse_mask(&self.dict, num_components, bpc)?;

        let interpolate = self
            .dict
            .get(&Name::new("Interpolate"))
            .or_else(|| self.dict.get(&Name::new("I")))
            .and_then(|o| o.as_bool())
            .unwrap_or(false);

        let decode = self
            .dict
            .get(&Name::new("Decode"))
            .or_else(|| self.dict.get(&Name::new("D")))
            .and_then(|o| o.as_array())
            .map(|arr| arr.iter().filter_map(|o| o.as_real()).collect::<Vec<f64>>())
            .filter(|v| !v.is_empty());

        Ok(PdfImage {
            width,
            height,
            bits_per_component: bpc,
            image_type,
            colorspace_name,
            num_components,
            mask,
            interpolate,
            decode,
            data: self.data.clone(),
            is_inline: true,
        })
    }
}

fn skip_whitespace(data: &[u8], pos: usize) -> usize {
    let mut p = pos;
    while p < data.len() && is_whitespace(data[p]) {
        p += 1;
    }
    p
}

/// PDF whitespace only: NUL, TAB, LF, FF, CR, SP (0x00, 0x09, 0x0A, 0x0C, 0x0D, 0x20).
/// Does not match 0x01..0x08, 0x0B, 0x0E..0x1F which are not PDF whitespace.
fn is_whitespace(b: u8) -> bool {
    matches!(b, 0x00 | 0x09 | 0x0a | 0x0c | 0x0d | 0x20)
}

fn matches_keyword(data: &[u8], pos: usize, kw: &str) -> bool {
    let kw = kw.as_bytes();
    pos + kw.len() <= data.len()
        && &data[pos..pos + kw.len()] == kw
        && (pos + kw.len() >= data.len() || is_whitespace(data[pos + kw.len()]))
}

fn parse_inline_value(data: &[u8], pos: usize) -> Result<(Object, usize)> {
    let mut p = pos;
    p = skip_whitespace(data, p);
    if p >= data.len() {
        return Err(Error::syntax("Unexpected end in inline image"));
    }
    let start = p;
    if data[p] == b'[' {
        p += 1;
        let mut arr = Array::new();
        loop {
            p = skip_whitespace(data, p);
            if p >= data.len() {
                return Err(Error::syntax("Unterminated array in inline image"));
            }
            if data[p] == b']' {
                p += 1;
                break;
            }
            let (val_obj, consumed) = parse_inline_value(data, p)?;
            arr.push(val_obj);
            p += consumed;
        }
        return Ok((Object::Array(arr), p - pos));
    }
    if data[p] == b'/' {
        p += 1;
        while p < data.len() && !is_whitespace(data[p]) && data[p] != b'/' && data[p] != b'[' {
            p += 1;
        }
        let s = std::str::from_utf8(&data[start + 1..p])
            .map_err(|_| Error::syntax("Invalid UTF-8 in inline image key"))?
            .to_string();
        return Ok((Object::Name(Name::new(&s)), p - pos));
    }
    if data[p] == b't' && p + 4 <= data.len() && &data[p..p + 4] == b"true" {
        return Ok((Object::Bool(true), p + 4 - pos));
    }
    if data[p] == b'f' && p + 5 <= data.len() && &data[p..p + 5] == b"false" {
        return Ok((Object::Bool(false), p + 5 - pos));
    }
    if data[p].is_ascii_digit() || data[p] == b'-' || data[p] == b'.' {
        let mut end = p;
        if data[p] == b'-' || data[p] == b'+' {
            end += 1;
        }
        while end < data.len() && (data[end].is_ascii_digit() || data[end] == b'.') {
            end += 1;
        }
        let s = std::str::from_utf8(&data[p..end])
            .map_err(|_| Error::syntax("Invalid UTF-8 in inline image number"))?;
        if s.contains('.') {
            if let Ok(f) = s.parse::<f64>() {
                return Ok((Object::Real(f), end - pos));
            }
        }
        if let Ok(i) = s.parse::<i64>() {
            return Ok((Object::Int(i), end - pos));
        }
    }
    Ok((Object::Null, 1))
}

/// PDF delimiter whitespace only (0x00, 0x09, 0x0a, 0x0c, 0x0d, 0x20).
/// Used for EI marker detection to avoid matching "EI" inside binary image data.
fn is_pdf_delimiter_whitespace(b: u8) -> bool {
    matches!(b, 0x00 | 0x09 | 0x0a | 0x0c | 0x0d | 0x20)
}

/// Returns the exclusive end index of the image data for data[start..end].
/// Image data is everything from start up to (but not including) the whitespace byte that precedes EI.
/// - Non-empty: end = position of whitespace before 'E' (exclusive; last image byte at end-1).
/// - Empty ("ID EI"): end = start so data[start..start] is empty.
///
/// `max_end`: do not search beyond this position (DoS protection). Typically start + INLINE_IMAGE_MAX_SIZE.
fn find_ei_marker(data: &[u8], start: usize, max_end: usize) -> Option<usize> {
    // Need at least 2 bytes for "EI"
    if start.saturating_add(2) > data.len() {
        return None;
    }
    // Do not search beyond max_end; need i+2 for "EI" so last valid i is max_end-2
    let last_i = data.len().saturating_sub(2).min(max_end.saturating_sub(2));
    if start > last_i {
        return None;
    }
    for i in start..=last_i {
        if data.get(i).copied() == Some(b'E') && data.get(i + 1).copied() == Some(b'I') {
            // Per PDF spec: EI must be preceded by whitespace (0x00, 0x09, 0x0A, 0x0C, 0x0D, 0x20)
            // and followed by whitespace or EOF. Empty image "ID EI": i==start.
            let ws_before = if i == start {
                true
            } else {
                is_pdf_delimiter_whitespace(data[i - 1])
            };
            let ws_after = (i + 2 >= data.len())
                || data
                    .get(i + 2)
                    .map_or(true, |&b| is_pdf_delimiter_whitespace(b));
            if ws_before && ws_after {
                // Exclusive end: position of whitespace before E (or start if empty)
                let end = if i > start { i - 1 } else { start };
                return Some(end);
            }
        }
    }
    None
}

// ============================================================================
// Inline Key/Value Expansion
// ============================================================================

/// Map abbreviated inline image keys to full names
pub fn expand_inline_key(key: &str) -> String {
    match key {
        "W" => "Width".to_string(),
        "H" => "Height".to_string(),
        "BPC" => "BitsPerComponent".to_string(),
        "E" => "Decode".to_string(),
        "CS" => "ColorSpace".to_string(),
        "F" => "Filter".to_string(),
        "DP" => "DecodeParms".to_string(),
        "IM" => "ImageMask".to_string(),
        "I" => "Interpolate".to_string(),
        "Intent" => "Intent".to_string(),
        _ => key.to_string(),
    }
}

/// Map abbreviated inline image values to full names
pub fn expand_inline_value(key: &str, value: &str) -> String {
    match (key, value) {
        ("CS", "G") | ("ColorSpace", "G") => "DeviceGray".to_string(),
        ("CS", "RGB") | ("ColorSpace", "RGB") => "DeviceRGB".to_string(),
        ("CS", "CMYK") | ("ColorSpace", "CMYK") => "DeviceCMYK".to_string(),
        ("F", "AHx") | ("Filter", "AHx") => "ASCIIHexDecode".to_string(),
        ("F", "A85") | ("Filter", "A85") => "ASCII85Decode".to_string(),
        ("F", "LZW") | ("Filter", "LZW") => "LZWDecode".to_string(),
        ("F", "Fl") | ("Filter", "Fl") => "FlateDecode".to_string(),
        ("F", "RL") | ("Filter", "RL") => "RunLengthDecode".to_string(),
        ("F", "CCF") | ("Filter", "CCF") => "CCITTFaxDecode".to_string(),
        ("F", "DCT") | ("Filter", "DCT") => "DCTDecode".to_string(),
        _ => value.to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_dict(entries: &[(&str, Object)]) -> Dict {
        let mut d = Dict::new();
        for (k, v) in entries {
            d.insert(Name::new(k), v.clone());
        }
        d
    }

    #[test]
    fn test_parse_image_jpeg() {
        let dict = make_dict(&[
            ("Width", Object::Int(100)),
            ("Height", Object::Int(50)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceRGB"))),
            ("Filter", Object::Name(Name::new("DCTDecode"))),
        ]);
        let data = vec![0u8; 100 * 50 * 3];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        assert_eq!(img.width, 100);
        assert_eq!(img.height, 50);
        assert_eq!(img.image_type, ImageType::Jpeg);
        assert_eq!(img.colorspace_name.as_deref(), Some("DeviceRGB"));
    }

    #[test]
    fn test_parse_image_png() {
        let dict = make_dict(&[
            ("Width", Object::Int(64)),
            ("Height", Object::Int(64)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceRGB"))),
            ("Filter", Object::Name(Name::new("FlateDecode"))),
        ]);
        let data = vec![0u8; 64 * 64 * 3];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        assert_eq!(img.image_type, ImageType::Png);
    }

    #[test]
    fn test_parse_image_unknown_filter_errors() {
        let dict = make_dict(&[
            ("Width", Object::Int(10)),
            ("Height", Object::Int(10)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceRGB"))),
            ("Filter", Object::Name(Name::new("UnknownFilter"))),
        ]);
        assert!(PdfImage::from_pdf_dict(&dict, vec![]).is_err());
    }

    #[test]
    fn test_parse_image_raw() {
        let dict = make_dict(&[
            ("Width", Object::Int(10)),
            ("Height", Object::Int(10)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceGray"))),
        ]);
        let data = vec![128u8; 100];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        assert_eq!(img.image_type, ImageType::Raw);
        assert_eq!(img.colorspace_name.as_deref(), Some("DeviceGray"));
    }

    #[test]
    fn test_parse_image_missing_colorspace_non_mask() {
        let dict = make_dict(&[
            ("Width", Object::Int(10)),
            ("Height", Object::Int(10)),
            ("BitsPerComponent", Object::Int(8)),
        ]);
        assert!(PdfImage::from_pdf_dict(&dict, vec![]).is_err());
    }

    #[test]
    fn test_parse_image_missing_width() {
        let dict = make_dict(&[("Height", Object::Int(10))]);
        assert!(PdfImage::from_pdf_dict(&dict, vec![]).is_err());
    }

    #[test]
    fn test_parse_image_missing_height() {
        let dict = make_dict(&[("Width", Object::Int(10))]);
        assert!(PdfImage::from_pdf_dict(&dict, vec![]).is_err());
    }

    #[test]
    fn test_dimensions() {
        let dict = make_dict(&[
            ("Width", Object::Int(320)),
            ("Height", Object::Int(240)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceRGB"))),
        ]);
        let data = vec![0u8; 320 * 240 * 3];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        assert_eq!(img.dimensions(), (320, 240));
    }

    #[test]
    fn test_pixel_count() {
        let dict = make_dict(&[
            ("Width", Object::Int(100)),
            ("Height", Object::Int(200)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceRGB"))),
        ]);
        let data = vec![0u8; 100 * 200 * 3];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        assert_eq!(img.pixel_count().unwrap(), 20000);
    }

    #[test]
    fn test_to_rgba_rgb() {
        let dict = make_dict(&[
            ("Width", Object::Int(2)),
            ("Height", Object::Int(2)),
            ("ColorSpace", Object::Name(Name::new("DeviceRGB"))),
            ("BitsPerComponent", Object::Int(8)),
        ]);
        let data = vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 128, 128, 128];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        let rgba = img.to_rgba().unwrap();
        assert_eq!(rgba.len(), 16);
        assert_eq!(rgba[0..4], [255, 0, 0, 255]);
        assert_eq!(rgba[4..8], [0, 255, 0, 255]);
        assert_eq!(rgba[8..12], [0, 0, 255, 255]);
    }

    #[test]
    fn test_to_rgba_gray() {
        let dict = make_dict(&[
            ("Width", Object::Int(2)),
            ("Height", Object::Int(1)),
            ("ColorSpace", Object::Name(Name::new("DeviceGray"))),
            ("BitsPerComponent", Object::Int(8)),
        ]);
        let data = vec![64, 200];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        let rgba = img.to_rgba().unwrap();
        assert_eq!(rgba, [64, 64, 64, 255, 200, 200, 200, 255]);
    }

    #[test]
    fn test_to_rgba_cmyk() {
        let dict = make_dict(&[
            ("Width", Object::Int(1)),
            ("Height", Object::Int(1)),
            ("ColorSpace", Object::Name(Name::new("DeviceCMYK"))),
            ("BitsPerComponent", Object::Int(8)),
        ]);
        let data = vec![0, 0, 0, 0];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        let rgba = img.to_rgba().unwrap();
        assert_eq!(rgba[0..3], [255, 255, 255]);
    }

    #[test]
    fn test_to_rgba_1bit() {
        let dict = make_dict(&[
            ("Width", Object::Int(8)),
            ("Height", Object::Int(1)),
            ("ColorSpace", Object::Name(Name::new("DeviceGray"))),
            ("BitsPerComponent", Object::Int(1)),
        ]);
        let data = vec![0b10101010];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        let rgba = img.to_rgba().unwrap();
        assert_eq!(rgba.len(), 32);
        assert_eq!(rgba[0], 255);
        assert_eq!(rgba[4], 0);
    }

    #[test]
    fn test_create_rgb() {
        let data: Vec<u8> = (0..300).map(|i| (i % 256) as u8).collect();
        let img = PdfImage::create_rgb(&data, 10, 10).unwrap();
        assert_eq!(img.width, 10);
        assert_eq!(img.height, 10);
        assert_eq!(img.image_type, ImageType::Raw);
        assert_eq!(img.data.len(), 300);
    }

    #[test]
    fn test_create_rgb_too_short() {
        let data = vec![0u8; 100];
        assert!(PdfImage::create_rgb(&data, 10, 10).is_err());
    }

    #[test]
    fn test_to_pdf_dict() {
        let dict = make_dict(&[
            ("Width", Object::Int(50)),
            ("Height", Object::Int(50)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceRGB"))),
        ]);
        let img = PdfImage::from_pdf_dict(&dict, vec![0; 7500]).unwrap();
        let out = img.to_pdf_dict();
        assert_eq!(
            out.get(&Name::new("Type"))
                .and_then(|o| o.as_name())
                .map(|n| n.as_str()),
            Some("XObject")
        );
        assert_eq!(
            out.get(&Name::new("Subtype"))
                .and_then(|o| o.as_name())
                .map(|n| n.as_str()),
            Some("Image")
        );
        assert_eq!(
            out.get(&Name::new("Width")).and_then(|o| o.as_int()),
            Some(50)
        );
        assert_eq!(
            out.get(&Name::new("Height")).and_then(|o| o.as_int()),
            Some(50)
        );
    }

    #[test]
    fn test_image_mask_stencil() {
        let dict = make_dict(&[
            ("Width", Object::Int(10)),
            ("Height", Object::Int(10)),
            ("ImageMask", Object::Bool(true)),
        ]);
        let data = vec![0u8; 20];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        assert!(matches!(img.mask, ImageMask::Stencil));
    }

    #[test]
    fn test_image_mask_color_key() {
        let dict = make_dict(&[
            ("Width", Object::Int(10)),
            ("Height", Object::Int(10)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceGray"))),
            (
                "Mask",
                Object::Array(vec![Object::Int(0), Object::Int(255)]),
            ),
        ]);
        let data = vec![0u8; 100];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        if let ImageMask::ColorKeyMask { ranges } = &img.mask {
            assert_eq!(ranges.len(), 1);
            assert_eq!(ranges[0], (0, 255));
        } else {
            panic!("Expected ColorKeyMask");
        }
    }

    #[test]
    fn test_image_mask_color_key_out_of_range_errors() {
        // 8-bit max is 255; 256 is out of range
        let dict = make_dict(&[
            ("Width", Object::Int(10)),
            ("Height", Object::Int(10)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceGray"))),
            (
                "Mask",
                Object::Array(vec![Object::Int(0), Object::Int(256)]),
            ),
        ]);
        let data = vec![0u8; 100];
        assert!(PdfImage::from_pdf_dict(&dict, data).is_err());
    }

    #[test]
    fn test_data_size() {
        let dict = make_dict(&[
            ("Width", Object::Int(100)),
            ("Height", Object::Int(100)),
            ("ColorSpace", Object::Name(Name::new("DeviceRGB"))),
            ("BitsPerComponent", Object::Int(8)),
        ]);
        let data = vec![0u8; 100 * 100 * 3];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        assert_eq!(img.data_size().unwrap(), 30000);
    }

    #[test]
    fn test_data_size_1bit() {
        let dict = make_dict(&[
            ("Width", Object::Int(8)),
            ("Height", Object::Int(1)),
            ("ColorSpace", Object::Name(Name::new("DeviceGray"))),
            ("BitsPerComponent", Object::Int(1)),
        ]);
        let data = vec![0u8; 1];
        let img = PdfImage::from_pdf_dict(&dict, data).unwrap();
        assert_eq!(img.data_size().unwrap(), 1);
    }

    #[test]
    fn test_expand_inline_key() {
        assert_eq!(expand_inline_key("W"), "Width");
        assert_eq!(expand_inline_key("H"), "Height");
        assert_eq!(expand_inline_key("BPC"), "BitsPerComponent");
        assert_eq!(expand_inline_key("CS"), "ColorSpace");
        assert_eq!(expand_inline_key("F"), "Filter");
        assert_eq!(expand_inline_key("IM"), "ImageMask");
        assert_eq!(expand_inline_key("Unknown"), "Unknown");
    }

    #[test]
    fn test_expand_inline_value() {
        assert_eq!(expand_inline_value("CS", "G"), "DeviceGray");
        assert_eq!(expand_inline_value("CS", "RGB"), "DeviceRGB");
        assert_eq!(expand_inline_value("CS", "CMYK"), "DeviceCMYK");
        assert_eq!(expand_inline_value("F", "DCT"), "DCTDecode");
        assert_eq!(expand_inline_value("F", "Fl"), "FlateDecode");
        assert_eq!(expand_inline_value("F", "AHx"), "ASCIIHexDecode");
    }

    #[test]
    fn test_inline_image_parse() {
        let data = b"/W 10 /H 20 /BPC 8 /CS /RGB ID \x00\x01\x02\x03 EI";
        let (inline_img, consumed) = InlineImage::parse(data).unwrap();
        assert!(consumed > 0);
        assert_eq!(
            inline_img
                .dict
                .get(&Name::new("Width"))
                .and_then(|o| o.as_int()),
            Some(10)
        );
        assert_eq!(
            inline_img
                .dict
                .get(&Name::new("Height"))
                .and_then(|o| o.as_int()),
            Some(20)
        );
        assert_eq!(
            inline_img
                .dict
                .get(&Name::new("BitsPerComponent"))
                .and_then(|o| o.as_int()),
            Some(8)
        );
    }

    #[test]
    fn test_inline_image_missing_ei_errors() {
        let data = b"/W 10 /H 10 /BPC 8 ID \x00\x01\x02";
        assert!(InlineImage::parse(data).is_err());
    }

    #[test]
    fn test_inline_image_empty_data_ei_at_start() {
        // "ID EI" - empty image data, EI immediately after ID
        let data = b"/W 0 /H 0 /BPC 8 ID EI";
        let (inline_img, consumed) = InlineImage::parse(data).unwrap();
        assert!(consumed > 0);
        assert!(inline_img.data.is_empty());
    }

    #[test]
    fn test_inline_image_empty_data_with_space_before_ei() {
        // "ID  EI" - whitespace between ID and EI, empty image data
        let data = b"/W 0 /H 0 /BPC 8 ID  EI";
        let (inline_img, _) = InlineImage::parse(data).unwrap();
        assert!(inline_img.data.is_empty());
    }

    #[test]
    fn test_find_ei_marker_empty_then_ei() {
        // data_start at 'E', empty image
        let data = b"EI";
        assert_eq!(find_ei_marker(data, 0, data.len()), Some(0));
    }

    #[test]
    fn test_find_ei_marker_one_byte_then_space_ei() {
        // One image byte, then space, then EI. Exclusive end = position of whitespace (1).
        let data = b"X EI";
        assert_eq!(find_ei_marker(data, 0, data.len()), Some(1));
        assert_eq!(&data[0..1], b"X");
    }

    #[test]
    fn test_find_ei_marker_bounded_search_no_ei() {
        // No EI within limit - should return None
        let data = vec![0x42u8; 200_000];
        let limit = 102_400;
        assert_eq!(find_ei_marker(&data, 0, limit), None);
    }

    #[test]
    fn test_inline_image_ei_at_end() {
        // EI at very end of buffer
        let data = b"/W 1 /H 1 /BPC 8 ID \x42 EI";
        let (inline_img, consumed) = InlineImage::parse(data).unwrap();
        assert_eq!(inline_img.data.len(), 1);
        assert_eq!(inline_img.data[0], 0x42);
    }

    #[test]
    fn test_inline_image_decode_array() {
        let data = b"/W 2 /H 2 /BPC 8 /E [0 1 0 1 0 1] ID \x00\x01\x02\x03 EI";
        let (inline_img, _) = InlineImage::parse(data).unwrap();
        let decode = inline_img
            .dict
            .get(&Name::new("Decode"))
            .or_else(|| inline_img.dict.get(&Name::new("E")))
            .and_then(|o| o.as_array());
        let arr = decode.expect("Decode array should be parsed");
        assert_eq!(arr.len(), 6);
    }

    #[test]
    fn test_inline_image_abbreviated_keys() {
        let data = b"/W 5 /H 5 /BPC 8 /CS /G ID ";
        let mut data_vec = data.to_vec();
        data_vec.extend_from_slice(&[0u8; 25]);
        data_vec.extend_from_slice(b" EI");
        let (inline_img, _) = InlineImage::parse(&data_vec).unwrap();
        let pdf_img = inline_img.to_pdf_image().unwrap();
        assert_eq!(pdf_img.width, 5);
        assert_eq!(pdf_img.height, 5);
        assert_eq!(pdf_img.colorspace_name.as_deref(), Some("DeviceGray"));
    }

    #[test]
    fn test_edge_case_zero_dimensions() {
        let dict = make_dict(&[
            ("Width", Object::Int(0)),
            ("Height", Object::Int(0)),
            ("BitsPerComponent", Object::Int(8)),
            ("ColorSpace", Object::Name(Name::new("DeviceRGB"))),
        ]);
        let img = PdfImage::from_pdf_dict(&dict, vec![]).unwrap();
        assert_eq!(img.dimensions(), (0, 0));
        assert_eq!(img.pixel_count().unwrap(), 0);
        assert_eq!(img.data_size().unwrap(), 0);
    }

    #[test]
    fn test_create_jpeg() {
        use image::codecs::jpeg::JpegEncoder;
        use image::{ImageBuffer, Rgb};
        use std::io::Cursor;

        let img: ImageBuffer<Rgb<u8>, _> = ImageBuffer::from_raw(10, 10, vec![128u8; 300]).unwrap();
        let mut buf = Cursor::new(Vec::new());
        let mut encoder = JpegEncoder::new_with_quality(&mut buf, 85);
        encoder
            .encode(img.as_raw(), 10, 10, image::ExtendedColorType::Rgb8)
            .unwrap();
        let jpeg_data = buf.into_inner();
        let pdf_img = PdfImage::create_jpeg(&jpeg_data, 10, 10).unwrap();
        assert_eq!(pdf_img.width, 10);
        assert_eq!(pdf_img.height, 10);
        assert_eq!(pdf_img.image_type, ImageType::Jpeg);
    }
}
