pdfox 0.1.0

A pure-Rust PDF library — create, parse, and render PDF documents with zero C dependencies
Documentation
/// Image embedding support for PDF documents.
/// Supports JPEG (DCTDecode) and raw RGB pixel data.

use crate::object::{PdfDict, PdfObject, PdfStream};

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ColorSpace {
    Rgb,
    Grayscale,
    Cmyk,
}

impl ColorSpace {
    pub fn pdf_name(&self) -> &'static str {
        match self {
            ColorSpace::Rgb => "DeviceRGB",
            ColorSpace::Grayscale => "DeviceGray",
            ColorSpace::Cmyk => "DeviceCMYK",
        }
    }

    pub fn components(&self) -> usize {
        match self {
            ColorSpace::Rgb => 3,
            ColorSpace::Grayscale => 1,
            ColorSpace::Cmyk => 4,
        }
    }
}

/// An image ready to be embedded in a PDF
#[derive(Debug, Clone)]
pub struct PdfImage {
    pub width: u32,
    pub height: u32,
    pub color_space: ColorSpace,
    pub bits_per_component: u8,
    pub data: Vec<u8>,
    pub encoding: ImageEncoding,
}

#[derive(Debug, Clone, PartialEq)]
pub enum ImageEncoding {
    /// Raw (uncompressed) pixel data
    Raw,
    /// JPEG data (DCTDecode)
    Jpeg,
    /// Flate compressed
    Flate,
}

impl PdfImage {
    /// Create from raw RGB bytes (3 bytes per pixel, row-major)
    pub fn from_rgb(width: u32, height: u32, data: Vec<u8>) -> Self {
        Self {
            width,
            height,
            color_space: ColorSpace::Rgb,
            bits_per_component: 8,
            data,
            encoding: ImageEncoding::Raw,
        }
    }

    /// Create from raw grayscale bytes (1 byte per pixel)
    pub fn from_gray(width: u32, height: u32, data: Vec<u8>) -> Self {
        Self {
            width,
            height,
            color_space: ColorSpace::Grayscale,
            bits_per_component: 8,
            data,
            encoding: ImageEncoding::Raw,
        }
    }

    /// Create from JPEG bytes (the raw JPEG file data)
    /// Width, height, and colorspace should be parsed from the JPEG header.
    pub fn from_jpeg(width: u32, height: u32, color_space: ColorSpace, data: Vec<u8>) -> Self {
        Self {
            width,
            height,
            color_space,
            bits_per_component: 8,
            data,
            encoding: ImageEncoding::Jpeg,
        }
    }

    /// Parse a JPEG file's dimensions and color space from its raw bytes.
    /// Returns (width, height, color_space) or an error string.
    pub fn parse_jpeg_header(data: &[u8]) -> Result<(u32, u32, ColorSpace), String> {
        if data.len() < 4 || data[0] != 0xFF || data[1] != 0xD8 {
            return Err("Not a valid JPEG file".into());
        }

        let mut i = 2;
        while i + 4 <= data.len() {
            if data[i] != 0xFF {
                return Err(format!("Invalid JPEG marker at offset {}", i));
            }
            let marker = data[i + 1];
            let length = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;

            // SOF0, SOF1, SOF2 markers contain image dimensions
            if matches!(marker, 0xC0 | 0xC1 | 0xC2) && i + 9 <= data.len() {
                let height = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
                let width = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
                let components = data[i + 9];
                let cs = match components {
                    1 => ColorSpace::Grayscale,
                    3 => ColorSpace::Rgb,
                    4 => ColorSpace::Cmyk,
                    n => return Err(format!("Unsupported JPEG component count: {}", n)),
                };
                return Ok((width, height, cs));
            }

            i += 2 + length;
        }

        Err("Could not find JPEG SOF marker".into())
    }

    /// Convenience: load a JPEG from bytes, auto-parsing dimensions
    pub fn from_jpeg_bytes(data: Vec<u8>) -> Result<Self, String> {
        let (width, height, cs) = Self::parse_jpeg_header(&data)?;
        Ok(Self::from_jpeg(width, height, cs, data))
    }

    /// Convert to a PDF XObject stream
    pub fn to_xobject_stream(&self) -> PdfStream {
        let mut dict = PdfDict::new();
        dict.set("Type", PdfObject::name("XObject"));
        dict.set("Subtype", PdfObject::name("Image"));
        dict.set("Width", PdfObject::Integer(self.width as i64));
        dict.set("Height", PdfObject::Integer(self.height as i64));
        dict.set("ColorSpace", PdfObject::name(self.color_space.pdf_name()));
        dict.set("BitsPerComponent", PdfObject::Integer(self.bits_per_component as i64));
        dict.set("Length", PdfObject::Integer(self.data.len() as i64));

        match self.encoding {
            ImageEncoding::Jpeg => {
                dict.set("Filter", PdfObject::name("DCTDecode"));
            }
            ImageEncoding::Flate => {
                dict.set("Filter", PdfObject::name("FlateDecode"));
            }
            ImageEncoding::Raw => {
                // No filter needed
            }
        }

        PdfStream { dict, data: self.data.clone() }
    }
}

// ── Image crop, border, and drop-shadow ──────────────────────────────────────

/// Crop rectangle in pixel coordinates (within the image's natural dimensions)
#[derive(Debug, Clone, Copy)]
pub struct CropRect {
    /// Left edge in pixels (0 = left side of image)
    pub x: u32,
    /// Bottom edge in pixels (0 = bottom of image, PDF convention)
    pub y: u32,
    pub width: u32,
    pub height: u32,
}

impl CropRect {
    pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
        Self { x, y, width, height }
    }
}

/// Border style to draw around an image
#[derive(Debug, Clone)]
pub struct ImageBorder {
    pub color: crate::color::Color,
    pub width: f64,
    /// Corner radius for rounded borders (0 = square corners)
    pub radius: f64,
}

impl ImageBorder {
    pub fn solid(color: crate::color::Color, width: f64) -> Self {
        Self { color, width, radius: 0.0 }
    }
    pub fn rounded(color: crate::color::Color, width: f64, radius: f64) -> Self {
        Self { color, width, radius }
    }
}

/// Drop shadow configuration for an image
#[derive(Debug, Clone)]
pub struct DropShadow {
    pub offset_x: f64,
    pub offset_y: f64,
    pub blur_spread: f64,   // approximated as a filled rect with lighter colour
    pub color: crate::color::Color,
    pub opacity: f64,
}

impl DropShadow {
    pub fn new(offset_x: f64, offset_y: f64) -> Self {
        Self {
            offset_x,
            offset_y,
            blur_spread: 4.0,
            color: crate::color::Color::rgb_u8(0, 0, 0),
            opacity: 0.3,
        }
    }
    pub fn color(mut self, c: crate::color::Color) -> Self { self.color = c; self }
    pub fn spread(mut self, s: f64) -> Self { self.blur_spread = s; self }
    pub fn opacity(mut self, o: f64) -> Self { self.opacity = o.clamp(0.0, 1.0); self }
}

/// Render parameters that decorate how an image is placed on the page.
/// Used by `PageBuilder::image_ex()`.
#[derive(Debug, Clone, Default)]
pub struct ImageOptions {
    pub crop:        Option<CropRect>,
    pub border:      Option<ImageBorder>,
    pub shadow:      Option<DropShadow>,
    /// Opacity 0.0–1.0 (requires ExtGState support in document)
    pub opacity:     Option<f64>,
}

impl ImageOptions {
    pub fn new() -> Self { Self::default() }
    pub fn crop(mut self, c: CropRect) -> Self { self.crop = Some(c); self }
    pub fn border(mut self, b: ImageBorder) -> Self { self.border = Some(b); self }
    pub fn shadow(mut self, s: DropShadow) -> Self { self.shadow = Some(s); self }
    pub fn opacity(mut self, o: f64) -> Self { self.opacity = Some(o.clamp(0.0, 1.0)); self }
}