pdfluent 1.2.0

Pure-Rust PDF SDK with PDF/A, digital signatures, WASM, and experimental (feature-gated) XFA support.
Documentation
//! Options and reports for Epic 3 #1224 parity methods.
//!
//! Types that `PdfDocument::{to_images,compress,subset_fonts,embed_font,
//! insert_image}` consume or produce, separated out of `document.rs` to
//! keep the facade module scannable.

// Copyright (c) 2026 Innovation Trigger B.V.
//
// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
// the PDFluent Commercial Licence. See the LICENSE file in this repository --
// that file travels with the copy you received, which a URL does not.

use std::path::PathBuf;

use pdf_engine::render::RasterQuality;

/// Output image format for [`PdfDocument::to_images`].
///
/// [`PdfDocument::to_images`]: crate::PdfDocument::to_images
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImageFormat {
    /// Portable Network Graphics (lossless).
    Png,
    /// JPEG (lossy, smaller).
    Jpeg,
}

impl ImageFormat {
    /// File extension, no leading dot.
    pub fn extension(self) -> &'static str {
        match self {
            ImageFormat::Png => "png",
            ImageFormat::Jpeg => "jpg",
        }
    }
}

/// Options for [`PdfDocument::to_images`].
///
/// [`PdfDocument::to_images`]: crate::PdfDocument::to_images
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ToImagesOptions {
    /// Render resolution in DPI. Default: 150.
    pub dpi: u32,
    /// Output format. Default: [`ImageFormat::Png`].
    pub format: ImageFormat,
    /// Optional page range (1-based, inclusive). `None` = all pages.
    pub pages: Option<(usize, usize)>,
    /// Rasterization precision/speed trade-off. Default:
    /// [`RasterQuality::Quality`] (byte-identical to historical output).
    /// [`RasterQuality::Speed`] renders ~1.4–1.6× faster on content-heavy
    /// pages with sub-perceptual rounding where blending/AA/images compose.
    pub quality: RasterQuality,
}

impl Default for ToImagesOptions {
    fn default() -> Self {
        Self {
            dpi: 150,
            format: ImageFormat::Png,
            pages: None,
            quality: RasterQuality::default(),
        }
    }
}

impl ToImagesOptions {
    /// New default options.
    pub fn new() -> Self {
        Self::default()
    }

    /// Override the DPI.
    pub fn with_dpi(mut self, dpi: u32) -> Self {
        self.dpi = dpi;
        self
    }

    /// Override the image format.
    pub fn with_format(mut self, format: ImageFormat) -> Self {
        self.format = format;
        self
    }

    /// Limit rendering to a 1-based inclusive page range.
    pub fn with_pages(mut self, from: usize, to: usize) -> Self {
        self.pages = Some((from, to));
        self
    }

    /// Override the rasterization quality/speed trade-off.
    pub fn with_quality(mut self, quality: RasterQuality) -> Self {
        self.quality = quality;
        self
    }
}

/// Options for [`PdfDocument::render_page_with_options`].
///
/// [`PdfDocument::render_page_with_options`]: crate::PdfDocument::render_page_with_options
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RenderPageOptions {
    /// Render resolution in DPI. Default: 150.
    pub dpi: u32,
    /// Output format. Default: [`ImageFormat::Png`].
    pub format: ImageFormat,
    /// Rasterization precision/speed trade-off. Default:
    /// [`RasterQuality::Quality`] (byte-identical to historical output).
    pub quality: RasterQuality,
}

impl Default for RenderPageOptions {
    fn default() -> Self {
        Self {
            dpi: 150,
            format: ImageFormat::Png,
            quality: RasterQuality::default(),
        }
    }
}

impl RenderPageOptions {
    /// New default options.
    pub fn new() -> Self {
        Self::default()
    }

    /// Override the DPI.
    pub fn with_dpi(mut self, dpi: u32) -> Self {
        self.dpi = dpi;
        self
    }

    /// Override the image format.
    pub fn with_format(mut self, format: ImageFormat) -> Self {
        self.format = format;
        self
    }

    /// Override the rasterization quality/speed trade-off.
    pub fn with_quality(mut self, quality: RasterQuality) -> Self {
        self.quality = quality;
        self
    }
}

/// Options for [`PdfDocument::compress`].
///
/// The default preset runs the full optimisation stack (font subsetting,
/// stream compression, duplicate-object deduplication, unused-object
/// removal). Individual passes can be toggled.
///
/// [`PdfDocument::compress`]: crate::PdfDocument::compress
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CompressOptions {
    /// Subset embedded fonts to only their used glyphs.
    pub subset_fonts: bool,
    /// Compress uncompressed streams.
    pub compress_streams: bool,
    /// Deduplicate content streams with identical bytes.
    pub deduplicate_streams: bool,
    /// Remove objects unreachable from the catalog.
    pub remove_unused: bool,
}

impl Default for CompressOptions {
    fn default() -> Self {
        Self {
            subset_fonts: true,
            compress_streams: true,
            deduplicate_streams: true,
            remove_unused: true,
        }
    }
}

impl CompressOptions {
    /// New default options (full stack enabled).
    pub fn new() -> Self {
        Self::default()
    }

    /// Strict preset — full stack, no lossy passes.
    ///
    /// Identical to [`Self::default`] today; the explicit preset exists
    /// so calling code reads intent-first and is stable against future
    /// additions to lossy-only passes.
    pub fn strict() -> Self {
        Self {
            subset_fonts: true,
            compress_streams: true,
            deduplicate_streams: true,
            remove_unused: true,
        }
    }

    /// Lossy preset — enables every pass the strict preset enables.
    ///
    /// Today this is the same pass set as `strict()`. Lossy image
    /// downsampling lands in the 1.1 compress stack; reserving the
    /// preset now keeps callers forward-compatible.
    pub fn lossy() -> Self {
        Self::strict()
    }

    /// Archival preset — maximally safe: skip lossy / non-reversible
    /// passes, keep unused objects that might be referenced by future
    /// incremental updates.
    pub fn archival() -> Self {
        Self {
            subset_fonts: true,
            compress_streams: true,
            deduplicate_streams: true,
            // Archival preserves unused objects that might be referenced
            // by future incremental updates or signed appearance streams.
            remove_unused: false,
        }
    }
}

/// Report returned by [`PdfDocument::subset_fonts`] and included in the
/// combined [`CompressReport`].
///
/// [`PdfDocument::subset_fonts`]: crate::PdfDocument::subset_fonts
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FontSubsetReport {
    /// Embedded fonts visited.
    pub fonts_processed: usize,
    /// Embedded fonts whose `FontFile*` stream was replaced with a
    /// subset.
    pub fonts_subsetted: usize,
    /// Total bytes saved across all subsetted font streams.
    pub bytes_saved: usize,
}

/// Report returned by [`PdfDocument::compress`].
///
/// [`PdfDocument::compress`]: crate::PdfDocument::compress
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct CompressReport {
    /// Result of font subsetting, or `None` if the pass was disabled.
    pub font_subset: Option<FontSubsetReport>,
    /// Number of streams compressed.
    pub streams_compressed: usize,
    /// Number of duplicate streams merged.
    pub streams_deduplicated: usize,
    /// Number of unused objects removed.
    pub unused_removed: usize,
}

/// Supported [`PdfDocument::insert_image`] formats.
///
/// [`PdfDocument::insert_image`]: crate::PdfDocument::insert_image
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InsertImageFormat {
    /// JPEG — inserted as raw DCTDecode stream (no re-encoding).
    Jpeg,
    /// PNG — decoded to raw pixels, FlateDecode with optional alpha SMask.
    Png,
}

/// Description of an image to be inserted into a PDF page.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ImageInsert {
    /// Image bytes (JPEG or PNG).
    pub bytes: Vec<u8>,
    /// Format of `bytes`.
    pub format: InsertImageFormat,
    /// Target page (1-based).
    pub page: usize,
    /// X position in PDF points from the page's bottom-left.
    pub x: f64,
    /// Y position in PDF points from the page's bottom-left.
    pub y: f64,
    /// Display width in PDF points.
    pub width: f64,
    /// Display height in PDF points.
    pub height: f64,
    /// Optional opacity, `0.0..=1.0`. `None` = fully opaque.
    pub opacity: Option<f64>,
}

impl ImageInsert {
    /// Construct from raw bytes + format + destination.
    pub fn new(
        bytes: impl Into<Vec<u8>>,
        format: InsertImageFormat,
        page: usize,
        x: f64,
        y: f64,
        width: f64,
        height: f64,
    ) -> Self {
        Self {
            bytes: bytes.into(),
            format,
            page,
            x,
            y,
            width,
            height,
            opacity: None,
        }
    }

    /// Set the image opacity, clamped to `[0.0, 1.0]`.
    pub fn with_opacity(mut self, opacity: f64) -> Self {
        self.opacity = Some(opacity.clamp(0.0, 1.0));
        self
    }
}

/// Result of a successful [`PdfDocument::insert_image`] call.
///
/// [`PdfDocument::insert_image`]: crate::PdfDocument::insert_image
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ImageInsertReport {
    /// Pixel width of the decoded image.
    pub pixel_width: u32,
    /// Pixel height of the decoded image.
    pub pixel_height: u32,
    /// Resource name used in the page's content stream (e.g. `"Im1"`).
    pub resource_name: String,
}

/// Result of a successful [`PdfDocument::to_images`] call.
///
/// [`PdfDocument::to_images`]: crate::PdfDocument::to_images
#[derive(Debug, Clone)]
pub struct ToImagesReport {
    /// Paths the images were written to, in page order.
    pub paths: Vec<PathBuf>,
}

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

    #[test]
    fn to_images_options_default_is_quality() {
        let opts = ToImagesOptions::default();
        assert_eq!(opts.dpi, 150);
        assert_eq!(opts.quality, RasterQuality::Quality);
        assert_eq!(opts.quality, RasterQuality::default());
    }

    #[test]
    fn to_images_options_builder_carries_quality() {
        let opts = ToImagesOptions::new().with_quality(RasterQuality::Speed);
        assert_eq!(opts.quality, RasterQuality::Speed);
    }

    #[test]
    fn render_page_options_default_is_quality() {
        let opts = RenderPageOptions::default();
        assert_eq!(opts.dpi, 150);
        assert_eq!(opts.quality, RasterQuality::Quality);
    }

    #[test]
    fn render_page_options_builder_carries_quality() {
        let opts = RenderPageOptions::new()
            .with_dpi(72)
            .with_format(ImageFormat::Jpeg)
            .with_quality(RasterQuality::Speed);
        assert_eq!(opts.dpi, 72);
        assert_eq!(opts.format, ImageFormat::Jpeg);
        assert_eq!(opts.quality, RasterQuality::Speed);
    }
}