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 super::*;
use crate::layout::elements::{LayoutNode, LayoutVisitor, TextBlock};
use crate::render::pdf_syntax::format_pdf_number;

#[derive(Debug, Clone, Copy)]
struct ProjectedPoint {
    x: f64,
    y: f64,
}

impl ProjectedPoint {
    const fn new(x: f64, y: f64) -> Self {
        Self { x, y }
    }

    fn in_device_space(self, device: transforms::PdfDeviceSpace) -> Self {
        Self::new(
            f64::from((self.x * PageContentTransform::POINT_TO_DEVICE) as f32),
            f64::from(
                ((device.page_height() - self.y) * PageContentTransform::POINT_TO_DEVICE) as f32,
            ),
        )
    }

    fn quantized(self) -> Self {
        Self::new(f64::from(self.x as f32), f64::from(self.y as f32))
    }
}

fn uses_direct_device_projection(transform: &crate::style::computed::Transform) -> bool {
    let matrix = match transform {
        crate::style::computed::Transform::Matrix3d(matrix)
        | crate::style::computed::Transform::Project3d { matrix, .. } => matrix,
        _ => return false,
    };
    matrix[1] != 0.0 && matrix[4] != 0.0
}

pub(super) fn is_projected_transform(t: &crate::style::computed::Transform) -> bool {
    matches!(
        t,
        crate::style::computed::Transform::Matrix3d(_)
            | crate::style::computed::Transform::Project3d { .. }
    )
}

pub(super) fn projected_solid_children_are_empty(children: &[LayoutNode]) -> bool {
    children.iter().all(|child| {
        #[derive(Default)]
        struct EmptyText(bool);

        impl LayoutVisitor for EmptyText {
            fn visit_text_block(&mut self, element: &TextBlock) {
                self.0 = element.lines.is_empty()
                    && element.paint.background.color.is_none()
                    && !element.paint.background.layers.has_image()
                    && !element.box_model.border.has_any()
                    && element.paint.shadows.is_empty()
                    && element.paint.group.transform.value.is_none()
                    && element.paint.outline.width == 0.0;
            }
        }

        let mut empty = EmptyText::default();
        child.accept(&mut empty);
        empty.0
    })
}

fn project_transform_point(
    t: &crate::style::computed::Transform,
    origin: crate::style::computed::TransformOrigin,
    point: ProjectedPoint,
) -> ProjectedPoint {
    let (matrix, parent_perspective) = match *t {
        crate::style::computed::Transform::Matrix3d(m) => (m, None),
        crate::style::computed::Transform::Project3d {
            matrix,
            perspective,
            perspective_origin,
        } => (matrix, Some((perspective, perspective_origin))),
        _ => return point,
    };
    let lx = point.x - f64::from(origin.x_length);
    let ly = point.y - f64::from(origin.y_length);
    let lz = -f64::from(origin.z_length);
    let tx = matrix[0] * lx + matrix[4] * ly + matrix[8] * lz + matrix[12];
    let ty = matrix[1] * lx + matrix[5] * ly + matrix[9] * lz + matrix[13];
    let tz = matrix[2] * lx + matrix[6] * ly + matrix[10] * lz + matrix[14];
    let tw = matrix[3] * lx + matrix[7] * ly + matrix[11] * lz + matrix[15];
    let mut px = if tw != 0.0 { tx / tw } else { tx } + f64::from(origin.x_length);
    let mut py = if tw != 0.0 { ty / tw } else { ty } + f64::from(origin.y_length);
    let pz = if tw != 0.0 { tz / tw } else { tz } + f64::from(origin.z_length);
    if let Some((d, perspective_origin)) = parent_perspective {
        let denom = d - pz;
        if denom != 0.0 {
            let scale = d / denom;
            px = perspective_origin.x + (px - perspective_origin.x) * scale;
            py = perspective_origin.y + (py - perspective_origin.y) * scale;
        }
    }
    ProjectedPoint::new(px, py)
}

fn projected_quad(
    t: &crate::style::computed::Transform,
    origin: crate::style::computed::TransformOrigin,
    box_rect: PdfRect,
    inset: f32,
) -> [ProjectedPoint; 4] {
    let local = [
        ProjectedPoint::new(f64::from(inset), f64::from(inset)),
        ProjectedPoint::new(f64::from(box_rect.width - inset), f64::from(inset)),
        ProjectedPoint::new(
            f64::from(box_rect.width - inset),
            f64::from(box_rect.height - inset),
        ),
        ProjectedPoint::new(f64::from(inset), f64::from(box_rect.height - inset)),
    ];
    local.map(|point| {
        let projected = project_transform_point(t, origin, point);
        ProjectedPoint::new(
            f64::from(box_rect.left) + projected.x,
            f64::from(box_rect.top()) - projected.y,
        )
    })
}

fn projected_quad_path(points: [ProjectedPoint; 4]) -> String {
    let values = points.map(|point| {
        [
            format_pdf_number(point.x as f32),
            format_pdf_number(point.y as f32),
        ]
    });
    format!(
        "{} {} m\n{} {} l\n{} {} l\n{} {} l\n{} {} l\nh\n",
        values[0][0],
        values[0][1],
        values[1][0],
        values[1][1],
        values[2][0],
        values[2][1],
        values[3][0],
        values[3][1],
        values[0][0],
        values[0][1],
    )
}

pub(super) fn render_projected_solid_box(
    content: &mut String,
    page_content: PageContentTransform,
    box_transform: &crate::layout::elements::BoxTransform,
    geometry: PaintBoxGeometry,
    background_color: Option<crate::types::Color>,
    border: &crate::layout::engine::LayoutBorder,
) {
    let Some(t) = box_transform.value.as_ref() else {
        return;
    };
    let reference = geometry.transform_reference(box_transform);
    let local_pivot = reference.local_pivot();
    let origin = crate::style::computed::TransformOrigin {
        x_fraction: 0.0,
        x_length: local_pivot.x,
        y_fraction: 0.0,
        y_length: local_pivot.y,
        z_length: reference.z_origin(),
    };
    let box_rect = reference.border_box();
    let device = page_content
        .device_space()
        .filter(|_| uses_direct_device_projection(t));
    if let Some(device) = device {
        content.push_str("q\n");
        content.push_str(&device.enter_operator());
    }
    let path = |inset| {
        let points = projected_quad(t, origin, box_rect, inset);
        projected_quad_path(device.map_or_else(
            || points.map(ProjectedPoint::quantized),
            |device| points.map(|point| point.in_device_space(device)),
        ))
    };
    if let Some(color) = background_color {
        let (r, g, b) = color.to_f32_rgb();
        content.push_str(&PdfRgb::from((r, g, b)).fill_operator());
        content.push_str(&path(0.0));
        content.push_str("f\n");
    }
    let bw = border
        .top
        .width
        .max(border.right.width)
        .max(border.bottom.width)
        .max(border.left.width);
    if bw > 0.0 {
        let (r, g, b) = border.top.color.to_f32_rgb();
        content.push_str(&PdfRgb::from((r, g, b)).fill_operator());
        content.push_str(&path(0.0));
        content.push_str(&path(bw));
        content.push_str("f*\n");
    }
    if device.is_some() {
        content.push_str("Q\n");
    }
}