ironpress 1.4.4

Pure Rust HTML/CSS/Markdown to PDF converter with layout engine, LaTeX math, tables, images, custom fonts, and streaming output. No browser, no system dependencies.
Documentation
use crate::render::pdf::PdfResourceUsage;
use crate::render::pdf::geometry::{PdfMatrix, PdfPoint, PdfRect, PdfVector};
use crate::render::pdf_syntax::{format_pdf_number, format_pdf_number_fixed};
use crate::render::shading::{PdfGradientStops, PdfShadingKind};

#[derive(Debug, Clone, Copy, Default)]
pub(in crate::render::pdf) struct PdfTilingPattern {
    pub(in crate::render::pdf) bbox: PdfRect,
    pub(in crate::render::pdf) paint_box: PdfRect,
    pub(in crate::render::pdf) step: PdfVector,
    pub(in crate::render::pdf) transform: PdfMatrix,
}

impl PdfTilingPattern {
    pub(in crate::render::pdf) fn matrix_dictionary_entry(self) -> String {
        if self.transform == PdfMatrix::IDENTITY {
            return String::new();
        }
        let components = self
            .transform
            .components()
            .map(|value| format_pdf_number_fixed(f64::from(value), 9));
        format!(" /Matrix [{}]", components.join(" "))
    }
}

#[derive(Debug)]
pub(in crate::render::pdf) struct PdfTilingPatternEntry {
    pub(in crate::render::pdf) pattern_id: usize,
    pub(in crate::render::pdf) target: PdfTilingPatternTarget,
    pub(in crate::render::pdf) resources: PdfResourceUsage,
}

#[derive(Debug)]
pub(in crate::render::pdf) enum PdfTilingPatternTarget {
    Page { name: String },
    Form { object_id: usize },
}

#[derive(Debug)]
pub(in crate::render::pdf) struct PdfPatternEntry {
    pub(in crate::render::pdf) name: String,
    pub(in crate::render::pdf) object_id: usize,
}

#[derive(Debug, Clone, Copy)]
pub(in crate::render::pdf) enum PdfPatternGeometryFormat {
    Shortest,
    SixDecimals,
}

impl PdfPatternGeometryFormat {
    pub(in crate::render::pdf) fn number(self, value: f32) -> String {
        if matches!(self, Self::Shortest) {
            return format_pdf_number(value);
        }
        let mut text = format!("{value:.6}");
        while text.contains('.') && text.ends_with('0') {
            text.pop();
        }
        if text.ends_with('.') {
            text.pop();
        }
        if text == "-0" {
            return "0".to_string();
        }
        text
    }
}

#[derive(Debug, Clone, Copy)]
struct PdfCircle {
    center: PdfPoint,
    radius: f32,
}

impl PdfCircle {
    const fn new(center: PdfPoint, radius: f32) -> Self {
        Self { center, radius }
    }
}

#[derive(Debug, Clone, Copy)]
enum PdfShadingGeometry {
    Axial { start: PdfPoint, end: PdfPoint },
    Radial { start: PdfCircle, end: PdfCircle },
}

impl PdfShadingGeometry {
    fn pdf(self) -> (PdfShadingKind, [f32; 6], usize) {
        match self {
            Self::Axial { start, end } => (
                PdfShadingKind::Axial,
                [start.x, start.y, end.x, end.y, 0.0, 0.0],
                4,
            ),
            Self::Radial { start, end } => (
                PdfShadingKind::Radial,
                [
                    start.center.x,
                    start.center.y,
                    start.radius,
                    end.center.x,
                    end.center.y,
                    end.radius,
                ],
                6,
            ),
        }
    }
}

pub(in crate::render::pdf) struct PdfShadingPattern {
    geometry: PdfShadingGeometry,
    transform: PdfMatrix,
    stops: PdfGradientStops,
    geometry_format: PdfPatternGeometryFormat,
}

pub(in crate::render::pdf) struct PdfFunctionPattern {
    transform: PdfMatrix,
    domain: PdfRect,
    calculator: String,
}

impl PdfFunctionPattern {
    pub(in crate::render::pdf) fn new(
        transform: PdfMatrix,
        domain: PdfRect,
        calculator: String,
    ) -> Option<Self> {
        (transform.is_invertible() && !domain.is_empty()).then_some(Self {
            transform,
            domain,
            calculator,
        })
    }

    pub(super) fn into_parts(self) -> (PdfMatrix, PdfRect, String) {
        (self.transform, self.domain, self.calculator)
    }
}

impl PdfShadingPattern {
    pub(in crate::render::pdf) const fn axial(
        start: PdfPoint,
        end: PdfPoint,
        transform: PdfMatrix,
        stops: PdfGradientStops,
        geometry_format: PdfPatternGeometryFormat,
    ) -> Self {
        Self {
            geometry: PdfShadingGeometry::Axial { start, end },
            transform,
            stops,
            geometry_format,
        }
    }

    pub(in crate::render::pdf) const fn radial(
        center: PdfPoint,
        end_radius: f32,
        transform: PdfMatrix,
        stops: PdfGradientStops,
        geometry_format: PdfPatternGeometryFormat,
    ) -> Self {
        Self {
            geometry: PdfShadingGeometry::Radial {
                start: PdfCircle::new(center, 0.0),
                end: PdfCircle::new(center, end_radius),
            },
            transform,
            stops,
            geometry_format,
        }
    }

    pub(super) fn into_pdf_parts(
        self,
    ) -> (
        PdfShadingKind,
        [f32; 6],
        usize,
        PdfMatrix,
        PdfGradientStops,
        PdfPatternGeometryFormat,
    ) {
        let (kind, coordinates, coordinate_count) = self.geometry.pdf();
        (
            kind,
            coordinates,
            coordinate_count,
            self.transform,
            self.stops,
            self.geometry_format,
        )
    }
}

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

    #[test]
    fn tiling_pattern_matrix_preserves_retained_f32_geometry() {
        let pattern = PdfTilingPattern {
            transform: PdfMatrix::new(
                PdfVector::new(1.0, 0.0),
                PdfVector::new(0.0, f32::from_bits((-1.0_f32).to_bits() - 2)),
                PdfPoint::new(0.0, f32::from_bits(72.0_f32.to_bits() - 1)),
            ),
            ..Default::default()
        };

        assert_eq!(
            pattern.matrix_dictionary_entry(),
            " /Matrix [1 0 0 -0.999999881 0 71.999992371]"
        );
    }
}