use std::path::Path;
use anyhow::{bail, Context, Result};
use hayro::hayro_interpret::InterpreterSettings;
use hayro::hayro_syntax::Pdf;
use hayro::vello_cpu::color::palette::css::WHITE;
use hayro::{render, RenderCache, RenderSettings};
#[derive(Debug, Clone)]
pub struct RenderedPage {
pub index: usize,
pub width: usize,
pub height: usize,
pub rgba: Vec<u8>,
pub ink_coverage: f64,
}
pub struct PdfDocument<'a> {
pdf: Pdf,
cache: RenderCache<'a>,
settings: InterpreterSettings,
}
impl<'a> PdfDocument<'a> {
pub fn open(path: &Path) -> Result<Self> {
let data =
std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?;
Self::from_bytes(data).with_context(|| format!("{} is not a readable PDF", path.display()))
}
pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
let pdf = Pdf::new(data).map_err(|_| anyhow::anyhow!("invalid or encrypted PDF"))?;
Ok(Self {
pdf,
cache: RenderCache::new(),
settings: InterpreterSettings::default(),
})
}
pub fn page_count(&self) -> usize {
self.pdf.pages().len()
}
#[allow(dead_code)]
pub fn page_dimensions(&self, index: usize) -> Result<(usize, usize)> {
let page = self
.pdf
.pages()
.get(index)
.with_context(|| self.out_of_range(index))?;
let (w, h) = page.render_dimensions();
Ok((w.round() as usize, h.round() as usize))
}
#[allow(dead_code)]
pub fn scale_for_width(page_width: f32, target_width: usize) -> f32 {
target_width as f32 / page_width.max(1.0)
}
pub fn render_page(&'a self, index: usize, scale: f32) -> Result<RenderedPage> {
let page = self
.pdf
.pages()
.get(index)
.with_context(|| self.out_of_range(index))?;
let (w, h) = page.render_dimensions();
if w <= 0.0 || h <= 0.0 {
bail!("page {} has invalid dimensions", index + 1);
}
let pixmap = render(
page,
&self.cache,
&self.settings,
&RenderSettings {
x_scale: scale,
y_scale: scale,
bg_color: WHITE,
..Default::default()
},
);
let (width, height) = (pixmap.width() as usize, pixmap.height() as usize);
let mut rgba = pixmap.data_as_u8_slice().to_vec();
for px in rgba.chunks_exact_mut(4) {
px[3] = 255;
}
let ink_coverage = ink_coverage(&rgba);
Ok(RenderedPage {
index,
width,
height,
rgba,
ink_coverage,
})
}
#[allow(dead_code)]
pub fn render_page_to_width(
&'a self,
index: usize,
target_width: usize,
) -> Result<RenderedPage> {
let (page_width, _) = self.page_dimensions(index)?;
let scale = Self::scale_for_width(page_width as f32, target_width);
self.render_page(index, scale)
}
#[allow(dead_code)]
pub fn render_all(&'a self, scale: f32) -> Result<Vec<RenderedPage>> {
(0..self.page_count())
.map(|index| self.render_page(index, scale))
.collect()
}
fn out_of_range(&self, index: usize) -> String {
format!(
"page {} does not exist (document has {} pages)",
index + 1,
self.page_count()
)
}
}
pub fn ink_coverage(rgba: &[u8]) -> f64 {
let mut ink = 0.0;
for px in rgba.chunks_exact(4) {
let luminance = (px[0] as u32 + px[1] as u32 + px[2] as u32) / 3;
ink += 1.0 - luminance as f64 / 255.0;
}
ink / (rgba.len() / 4) as f64
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/two-page.pdf");
fn document<'a>() -> PdfDocument<'a> {
PdfDocument::from_bytes(FIXTURE.to_vec()).expect("fixture parses")
}
#[test]
fn fixture_renders_two_pages_with_opaque_alpha() {
let doc = document();
assert_eq!(doc.page_count(), 2);
let pages = doc.render_all(1.0).unwrap();
assert_eq!(pages.len(), 2);
for page in &pages {
assert_eq!((page.width, page.height), (612, 842));
assert_eq!(page.rgba.len(), page.width * page.height * 4);
assert!(
page.rgba.chunks_exact(4).all(|px| px[3] == 255),
"alpha must be forced to 255"
);
}
}
#[test]
fn ink_coverage_distinguishes_blank_from_inked() {
let doc = document();
let pages = doc.render_all(1.0).unwrap();
let half_page = &pages[0];
let blank = &pages[1];
assert!(
(half_page.ink_coverage - 0.5).abs() < 0.02,
"left-half fill should be ~50%, got {}",
half_page.ink_coverage
);
assert!(
blank.ink_coverage < 0.001,
"blank page should be ~0%, got {}",
blank.ink_coverage
);
}
#[test]
fn scale_derives_from_target_width_over_page_width() {
assert_eq!(PdfDocument::scale_for_width(612.0, 1224), 2.0);
assert_eq!(PdfDocument::scale_for_width(612.0, 306), 0.5);
assert_eq!(PdfDocument::scale_for_width(0.0, 100), 100.0);
}
#[test]
fn render_page_to_width_fits_the_target() {
let doc = document();
let page = doc.render_page_to_width(0, 306).unwrap();
assert_eq!(page.width, 306);
assert_eq!(page.height, 421);
}
#[test]
fn out_of_range_page_errors() {
let doc = document();
let err = doc.render_page(5, 1.0).unwrap_err();
assert!(err.to_string().contains("page 6 does not exist"));
}
#[test]
fn invalid_bytes_error() {
let err = PdfDocument::from_bytes(b"not a pdf".to_vec())
.err()
.expect("invalid bytes must fail");
assert!(!err.to_string().is_empty());
}
}