Files
designcraft/crates/render/src/pdf_bounds.rs
T
skylar taylor-barrick bc840130a4 Satisfy clippy's nonminimal_bool on rustc 1.96
Four sites already on main that `cargo xtask ci` cannot get past with the
current toolchain; each rewrite is the one clippy suggests and changes no
behaviour. `!x.is_some_and(f)` becomes `x.is_none_or(!f)`, and pdf_bounds
folds `!(a && b) || !c` into `!(a && b && c)` by De Morgan.

Unrelated to the merge they are committed beside; separated so that merge
stays a merge.
2026-10-16 20:20:36 -06:00

134 lines
5.5 KiB
Rust

//! The bounds of what a PDF page draws, found by interpreting its drawing operations: the union
//! of every painted path, glyph and image, each cut to the clip in force when it's painted.
//! Nothing is rasterized, so measuring a page costs a fraction of drawing it.
use hayro::hayro_interpret::font::Glyph;
use hayro::hayro_interpret::{
BlendMode, ClipPath, Context, Device, FillRule, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, RectExt,
SoftMask, TransformExt, interpret_page,
};
use vello_cpu::kurbo::{self, Affine, BezPath, Rect, Shape};
/// The bounds of what PDF page `page` draws, as fractions of the page as rendered (its visible
/// crop box, rotated as it displays): left, top, right, bottom. `None` when it draws nothing.
pub(crate) fn page_content_bounds(bytes: &[u8], page: usize) -> Option<[f64; 4]> {
let pdf = hayro::hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.to_vec())).ok()?;
let p = pdf.pages().get(page)?;
let (w, h) = p.render_dimensions();
let (w, h) = (f64::from(w), f64::from(h));
if !(w > 0.0 && h > 0.0) {
return None;
}
let initial = p.initial_transform(true).to_kurbo();
let page_rect = Rect::new(0.0, 0.0, w, h);
let crop = (initial * p.intersected_crop_box().to_kurbo().to_path(0.1)).bounding_box().intersect(page_rect);
let cache = InterpreterCache::new();
let mut ctx = Context::new(initial, page_rect, &cache, p.xref(), InterpreterSettings::default());
let mut dev = Bounds { clips: vec![crop], hidden: Vec::new(), out: None };
interpret_page(p, &mut ctx, &mut dev);
let r = dev.out?;
let frac = [r.x0 / w, r.y0 / h, r.x1 / w, r.y1 / h];
frac.iter().all(|v| v.is_finite()).then_some(frac)
}
/// A [`Device`] that collects bounds instead of drawing.
struct Bounds {
/// The bounding box of each clip in force, innermost last (device space); the first is the
/// page's crop box.
clips: Vec<Rect>,
/// For each open transparency group: whether nothing in it can show (opacity 0).
hidden: Vec<bool>,
out: Option<Rect>,
}
impl Bounds {
fn add(&mut self, r: Rect) {
if self.hidden.last().copied().unwrap_or(false) {
return;
}
let Some(clip) = self.clips.last() else { return };
let r = r.intersect(*clip);
if !(r.width() > 0.0 && r.height() > 0.0 && [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite())) {
return;
}
self.out = Some(self.out.map_or(r, |o| o.union(r)));
}
fn add_path(&mut self, path: &BezPath, transform: Affine, paint: &Paint<'_>, mode: &PathDrawMode) {
if let Paint::Color(c) = paint
&& c.to_rgba().to_rgba8()[3] == 0
{
return;
}
let r = match mode {
PathDrawMode::Fill(_) => (transform * path.clone()).bounding_box(),
PathDrawMode::Stroke(s) => {
// Dashes only remove parts of the line: the undashed stroke bounds them.
let stroke = kurbo::Stroke {
width: f64::from(s.line_width.max(0.0)),
join: s.line_join,
miter_limit: f64::from(s.miter_limit),
start_cap: s.line_cap,
end_cap: s.line_cap,
dash_pattern: Default::default(),
dash_offset: 0.0,
};
// A quarter point on the page, in the path's own units.
let scale = transform.determinant().abs().sqrt();
let tolerance = if scale.is_finite() && scale > 1e-9 { (0.25 / scale).min(1e6) } else { 0.25 };
let outline = kurbo::stroke(path.iter(), &stroke, &kurbo::StrokeOpts::default(), tolerance);
(transform * outline).bounding_box()
}
};
self.add(r);
}
}
impl<'a> Device<'a> for Bounds {
fn set_soft_mask(&mut self, _: Option<SoftMask<'a>>) {}
fn set_blend_mode(&mut self, _: BlendMode) {}
fn draw_path(&mut self, path: &BezPath, transform: Affine, paint: &Paint<'a>, mode: &PathDrawMode) {
self.add_path(path, transform, paint, mode);
}
fn push_clip_path(&mut self, clip: &ClipPath) {
let top = self.clips.last().copied().unwrap_or(Rect::ZERO);
self.clips.push(clip.path.bounding_box().intersect(top));
}
fn push_transparency_group(&mut self, opacity: f32, _: Option<SoftMask<'a>>, _: BlendMode) {
let parent = self.hidden.last().copied().unwrap_or(false);
self.hidden.push(parent || opacity <= 0.0);
}
fn draw_glyph(&mut self, glyph: &Glyph<'a>, transform: Affine, glyph_transform: Affine, paint: &Paint<'a>, mode: &GlyphDrawMode) {
let mode = match mode {
GlyphDrawMode::Invisible => return,
GlyphDrawMode::Fill => PathDrawMode::Fill(FillRule::NonZero),
GlyphDrawMode::Stroke(s) => PathDrawMode::Stroke(s.clone()),
};
match glyph {
Glyph::Outline(o) => self.add_path(&o.outline(), transform * glyph_transform, paint, &mode),
Glyph::Type3(g) => g.interpret(self, transform, glyph_transform, paint),
}
}
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
let r = Rect::new(0.0, 0.0, f64::from(image.width()), f64::from(image.height()));
self.add((transform * r.to_path(0.1)).bounding_box());
}
fn pop_clip_path(&mut self) {
// The crop box stays.
if self.clips.len() > 1 {
self.clips.pop();
}
}
fn pop_transparency_group(&mut self) {
self.hidden.pop();
}
}