use std::{fs, path::Path};
use hayro::{
hayro_interpret::InterpreterSettings,
hayro_syntax::{LoadPdfError, Pdf},
vello_cpu::color::palette::css::WHITE,
RenderCache, RenderSettings,
};
use rayon::prelude::*;
use crate::{
hayro_stack::{guard_nesting_depth, with_deep_stack, HAYRO_STACK_SIZE},
range::{dedupe_preserving_order, PageRangeGroup},
split::{render_output_pattern, validate_output_pattern},
PdfOpsError, Result,
};
const POINTS_PER_INCH: f32 = 72.0;
const MAX_PIXMAP_DIMENSION: f32 = u16::MAX as f32;
#[derive(Debug, Clone)]
pub struct RenderOptions {
pub dpi: f32,
pub pages: Option<PageRangeGroup>,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
dpi: 150.0,
pages: None,
}
}
}
pub fn render_pages(input: &Path, output_pattern: &str, options: &RenderOptions) -> Result<()> {
with_deep_stack(|| render_pages_inner(input, output_pattern, options))
}
fn render_pages_inner(input: &Path, output_pattern: &str, options: &RenderOptions) -> Result<()> {
validate_output_pattern(output_pattern)?;
if !options.dpi.is_finite() || options.dpi <= 0.0 {
return Err(PdfOpsError::InvalidStructure(format!(
"render dpi must be a positive number, got {}",
options.dpi
)));
}
let scale = options.dpi / POINTS_PER_INCH;
let data = fs::read(input)?;
guard_nesting_depth(&data, input)?;
let pdf = Pdf::new(data).map_err(|err| load_error(err, input))?;
let pages = pdf.pages();
let selected = match &options.pages {
Some(range) => dedupe_preserving_order(&range.resolve(pages.len())?),
None => (1..=pages.len()).collect(),
};
let width = pages.len().to_string().len();
let interpreter_settings = InterpreterSettings::default();
let render_settings = RenderSettings {
x_scale: scale,
y_scale: scale,
bg_color: WHITE,
..Default::default()
};
let render_one = |&page_number: &usize| {
let page = &pages[page_number - 1];
let (page_width, page_height) = page.render_dimensions();
if page_width * scale > MAX_PIXMAP_DIMENSION || page_height * scale > MAX_PIXMAP_DIMENSION {
return Err(PdfOpsError::Unsupported(format!(
"page {page_number} is too large to render at {} dpi",
options.dpi
)));
}
let cache = RenderCache::new();
let pixmap = hayro::render(page, &cache, &interpreter_settings, &render_settings);
let png = pixmap.into_png().map_err(|err| {
PdfOpsError::InvalidStructure(format!(
"failed to encode page {page_number} as PNG: {err}"
))
})?;
fs::write(
render_output_pattern(output_pattern, page_number, width)?,
png,
)?;
Ok(())
};
match rayon::ThreadPoolBuilder::new()
.stack_size(HAYRO_STACK_SIZE)
.build()
{
Ok(pool) => pool.install(|| selected.par_iter().try_for_each(render_one)),
Err(_) => selected.iter().try_for_each(render_one),
}
}
fn load_error(err: LoadPdfError, input: &Path) -> PdfOpsError {
match err {
LoadPdfError::Decryption(_) => PdfOpsError::Unsupported(format!(
"encrypted PDFs are not supported: {}",
input.display()
)),
LoadPdfError::Invalid => PdfOpsError::InvalidStructure(format!(
"failed to parse PDF for rendering: {}",
input.display()
)),
}
}