mirror of
https://github.com/lexmount/moli.git
synced 2026-11-13 16:10:01 +00:00
209 lines
6.5 KiB
Rust
209 lines
6.5 KiB
Rust
//! Native PDF output from owned paint snapshots. Layout and shaping remain in Moli.
|
|
//!
|
|
//! PDF pages are slices of the existing continuous print layout. Unsupported
|
|
//! effects are rasterized at their own clipped bounds; ordinary text stays text.
|
|
|
|
mod image;
|
|
mod links;
|
|
mod primitives;
|
|
mod text;
|
|
mod validation;
|
|
|
|
use std::{collections::BTreeSet, fmt, ops::RangeInclusive};
|
|
|
|
use krilla::{Document, geom::Size, page::PageSettings};
|
|
use moli_layout::{PaintRect, PaintSnapshot};
|
|
use peniko::kurbo::{Affine, Rect};
|
|
|
|
use primitives::PdfPainter;
|
|
|
|
/// Page geometry in PDF points (72 per inch). Ranges are zero-based and inclusive.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct PdfOptions {
|
|
pub width: f64,
|
|
pub height: f64,
|
|
pub margin_top: f64,
|
|
pub margin_bottom: f64,
|
|
pub margin_left: f64,
|
|
pub margin_right: f64,
|
|
pub scale: f64,
|
|
pub page_ranges: Vec<RangeInclusive<usize>>,
|
|
}
|
|
|
|
impl Default for PdfOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
width: 612.0,
|
|
height: 792.0,
|
|
margin_top: 72.0 / 2.54,
|
|
margin_bottom: 72.0 / 2.54,
|
|
margin_left: 72.0 / 2.54,
|
|
margin_right: 72.0 / 2.54,
|
|
scale: 1.0,
|
|
page_ranges: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum PdfError {
|
|
InvalidParameters,
|
|
PageRangeExceeded,
|
|
Paint(crate::PaintError),
|
|
Encoding(String),
|
|
}
|
|
|
|
impl fmt::Display for PdfError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::InvalidParameters => f.write_str("invalid print parameters"),
|
|
Self::PageRangeExceeded => f.write_str("Page range exceeds page count"),
|
|
Self::Paint(error) => error.fmt(f),
|
|
Self::Encoding(message) => write!(f, "Printing failed: {message}"),
|
|
}
|
|
}
|
|
}
|
|
impl std::error::Error for PdfError {}
|
|
impl From<crate::PaintError> for PdfError {
|
|
fn from(error: crate::PaintError) -> Self {
|
|
Self::Paint(error)
|
|
}
|
|
}
|
|
|
|
/// Writes embedded, subsetted fonts and native drawing commands. Device pixel
|
|
/// ratio does not affect page geometry. This does not perform CSS fragmentation.
|
|
pub fn pdf_snapshot(snapshot: &PaintSnapshot, options: &PdfOptions) -> Result<Vec<u8>, PdfError> {
|
|
crate::validation::validate_fragment_stream(snapshot)?;
|
|
validation::validate(snapshot)?;
|
|
let width = f64::from(snapshot.surface.css_width);
|
|
let height = f64::from(snapshot.surface.css_height);
|
|
let printable_width = options.width - options.margin_left - options.margin_right;
|
|
let printable_height = options.height - options.margin_top - options.margin_bottom;
|
|
if [
|
|
width,
|
|
height,
|
|
options.width,
|
|
options.height,
|
|
printable_width,
|
|
printable_height,
|
|
options.scale,
|
|
]
|
|
.iter()
|
|
.any(|value| !value.is_finite() || *value <= 0.0)
|
|
|| [
|
|
options.margin_top,
|
|
options.margin_bottom,
|
|
options.margin_left,
|
|
options.margin_right,
|
|
]
|
|
.iter()
|
|
.any(|value| !value.is_finite() || *value < 0.0)
|
|
|| options.width > 14_400.0
|
|
|| options.height > 14_400.0
|
|
|| !(0.1..=2.0).contains(&options.scale)
|
|
{
|
|
return Err(PdfError::InvalidParameters);
|
|
}
|
|
// Preserve the existing shrink-to-printable-width contract.
|
|
let scale = printable_width * options.scale / width;
|
|
let page_height = printable_height / scale;
|
|
let page_count = (height / page_height).ceil().max(1.0);
|
|
if !page_count.is_finite() || page_count >= usize::MAX as f64 {
|
|
return Err(PdfError::InvalidParameters);
|
|
}
|
|
let page_count = page_count as usize;
|
|
let selected: BTreeSet<usize> = if options.page_ranges.is_empty() {
|
|
(0..page_count).collect()
|
|
} else {
|
|
options
|
|
.page_ranges
|
|
.iter()
|
|
.flat_map(|range| *range.start()..=(*range.end()).min(page_count - 1))
|
|
.collect()
|
|
};
|
|
if selected.is_empty() {
|
|
return Err(PdfError::PageRangeExceeded);
|
|
}
|
|
let mut document = Document::new();
|
|
let mut fonts = text::FontCache::default();
|
|
let mut images = std::collections::HashMap::new();
|
|
let scopes = primitives::scope_ends(&snapshot.fragments)?;
|
|
let regions = links::collect(snapshot)?;
|
|
let selected = selected
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(output, original)| (original, output))
|
|
.collect();
|
|
let page_links = links::PageLinks {
|
|
regions: ®ions,
|
|
selected: &selected,
|
|
page_height,
|
|
scale,
|
|
options,
|
|
};
|
|
for &page_index in selected.keys() {
|
|
let top = page_index as f64 * page_height;
|
|
let source = Rect::new(0.0, top, printable_width / scale, top + page_height);
|
|
let mut page = document.start_page_with(
|
|
PageSettings::from_wh(options.width as f32, options.height as f32)
|
|
.ok_or(PdfError::InvalidParameters)?,
|
|
);
|
|
let mut surface = page.surface();
|
|
let target = Rect::new(
|
|
options.margin_left,
|
|
options.margin_top,
|
|
options.width - options.margin_right,
|
|
options.height - options.margin_bottom,
|
|
);
|
|
surface.push_clip_path(
|
|
&primitives::path(&target).ok_or(PdfError::InvalidParameters)?,
|
|
&Default::default(),
|
|
);
|
|
surface.push_transform(&primitives::transform(
|
|
Affine::translate((options.margin_left, options.margin_top - top * scale))
|
|
* Affine::scale(scale),
|
|
));
|
|
let mut painter = PdfPainter::new(
|
|
&mut surface,
|
|
snapshot,
|
|
source,
|
|
page_height,
|
|
page_index,
|
|
&mut fonts,
|
|
&mut images,
|
|
);
|
|
painter.fill_canvas();
|
|
let result = painter.paint(&snapshot.fragments, &scopes);
|
|
drop(painter);
|
|
surface.pop();
|
|
surface.pop();
|
|
surface.finish();
|
|
result?;
|
|
page_links.append(&mut page, source)?;
|
|
page.finish();
|
|
}
|
|
let mut bytes = document
|
|
.finish()
|
|
.map_err(|error| PdfError::Encoding(format!("{error:?}")))?;
|
|
bytes.push(b'\n');
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn rect(rect: PaintRect) -> Rect {
|
|
Rect::new(
|
|
f64::from(rect.x),
|
|
f64::from(rect.y),
|
|
f64::from(rect.x + rect.width),
|
|
f64::from(rect.y + rect.height),
|
|
)
|
|
}
|
|
|
|
fn image_size(width: u32, height: u32) -> Result<Size, PdfError> {
|
|
Size::from_wh(width as f32, height as f32).ok_or(PdfError::InvalidParameters)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod links_tests;
|
|
#[cfg(test)]
|
|
mod tests;
|