PDF.W1: reconcile shared writer with current main

This commit is contained in:
skylar taylor-barrick
2026-10-14 01:23:19 -06:00
121 changed files with 3694 additions and 524 deletions
+1 -1
View File
@@ -1330,7 +1330,7 @@ impl Exporter<'_> {
let doc = self.doc;
let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc));
let Some(b) = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name)) else { return false };
let art = vectorcraft_brush::stroke_pieces(b, bp, st);
let art = vectorcraft_brush::stroke_pieces(doc, b, bp, st);
let mut pushes = 0;
if st.blend != BlendMode::Normal {
s.push_blend_mode(blend(st.blend));
+112 -66
View File
@@ -3,12 +3,12 @@
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use hayro_interpret::font::Glyph;
use hayro_interpret::font::{Glyph, GlyphRun as ShownGlyphs};
use hayro_interpret::hayro_cmap::BfString;
use hayro_interpret::pattern::{Pattern, TilingPattern};
use hayro_interpret::{
CacheKey, ClipPath, Context, Device, GlyphDrawMode, Image, ImageData, InterpreterCache, InterpreterSettings, InterpreterWarning, LumaData,
MaskType, PathDrawMode, SoftMask, StrokeProps, interpret_page,
CacheKey, ClipPath, Context, Device, DrawMode, DrawProps, Image, ImageData, ImageDrawProps, InterpreterCache, InterpreterSettings,
InterpreterWarning, LumaData, MaskType, SoftMask, StrokeProps, interpret_page,
};
use hayro_syntax::object::Name;
use kurbo::{Affine, BezPath, PathEl, Rect, Shape};
@@ -147,9 +147,15 @@ pub const PAST_PAGE_NOTE: &str = "art reaching past the page was clipped to it,
/// Import a PDF, returning the document plus warnings about content that was approximated or skipped.
pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
// The interpreter draws forms, patterns, glyphs and masks inside one another by recursion, in
// large stack frames: it caps that (50 deep), and gets room for the cap whatever thread asks.
vectorcraft_doc::depth::with_deep_stack(|| read(bytes, opts))
}
/// [`import_with_report`] on the thread that asks.
pub(crate) fn read(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
let original = crate::pages::open(bytes, opts.password.as_deref())?;
let picked = crate::pages::picked(opts, original.pages().len())?;
picked.iter().filter_map(|&n| original.pages().get(n)).try_for_each(crate::pages::content_too_deep)?;
let mut ocgs = Ocgs::read(&original);
let route = opts.layers && !ocgs.list.is_empty();
// Content that is off imports as hidden layers: an update of the file turns every group on
@@ -167,7 +173,6 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
let all_on = turned_on.is_some();
let pdf = turned_on.unwrap_or(original);
let pages = pdf.pages();
picked.iter().filter_map(|&n| pages.get(n)).try_for_each(crate::pages::content_too_deep)?;
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
let sink2 = sink.clone();
@@ -176,6 +181,9 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
let msg = match w {
InterpreterWarning::UnsupportedFont => "a font could not be read; its text was skipped",
InterpreterWarning::ImageDecodeFailure => "an image could not be decoded and was skipped",
// Annotations aren't drawn (`render_annotations` below), and a kind of warning
// added later has no message yet.
_ => return,
};
if let Ok(mut v) = sink2.lock()
&& !v.iter().any(|x| x == msg)
@@ -1262,8 +1270,8 @@ impl<'p> Builder<'p> {
/// before it on the open line was stroked so too, is held with the line instead of ending it
/// (an editor writes type on a path glyph by glyph, each followed by the stroke added to the
/// type). `false`: it isn't one.
fn hold_glyph_stroke(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'_>, draw_mode: &PathDrawMode) -> bool {
let PathDrawMode::Stroke(props) = draw_mode else { return false };
fn hold_glyph_stroke(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'_>, draw_mode: &DrawMode) -> bool {
let DrawMode::Stroke(props) = draw_mode else { return false };
if self.mask.is_some() || self.blend != BlendMode::Normal || matches!(paint, hayro_interpret::Paint::Pattern(_)) {
return false;
}
@@ -1355,7 +1363,11 @@ impl<'p> Builder<'p> {
Some(art) => {
let [r, g, b, _] = m.background_color().to_rgba().components();
let backdrop = luminance(&Color::rgb(r, g, b));
let inverted = m.transfer_function().is_some_and(|tf| tf.apply(0.0) > tf.apply(1.0));
let inverted = m.transfer_function().is_some_and(|tf| {
let mut ends = [0, 255];
tf.apply_to(&mut ends);
ends[0] > ends[1]
});
let id = self.id();
mask_spec(art, m.mask_type() == MaskType::Alpha, backdrop, inverted, page, |all| Node::group(id, all))
}
@@ -1684,7 +1696,14 @@ fn resize_alpha(a: &LumaData, w: u32, h: u32) -> Vec<u8> {
}
}
impl<'a> Device<'a> for Builder<'_> {
/// What the interpreter draws, as the device below gets it.
impl<'a> Builder<'_> {
/// The soft mask and blend mode of what is drawn next.
fn set_state(&mut self, mask: Option<SoftMask<'a>>, blend_mode: hayro_interpret::BlendMode) {
self.set_soft_mask(mask);
self.set_blend_mode(blend_mode);
}
fn set_soft_mask(&mut self, mask: Option<SoftMask<'a>>) {
let spec = mask.map(|m| self.soft_mask(&m));
let same = match (&spec, &self.mask) {
@@ -1707,7 +1726,7 @@ impl<'a> Device<'a> for Builder<'_> {
}
}
fn draw_path(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'a>, draw_mode: &PathDrawMode) {
fn path(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'a>, draw_mode: &DrawMode) {
self.flush_glyphs();
if self.hold_glyph_stroke(path, transform, paint, draw_mode) {
self.pending = false;
@@ -1729,19 +1748,19 @@ impl<'a> Device<'a> for Builder<'_> {
hayro_interpret::Paint::Color(_) => None,
};
// Patch and triangle meshes are gradient meshes.
if let (PathDrawMode::Fill(_), Some(sp)) = (draw_mode, shading)
if let (DrawMode::Fill(_), Some(sp)) = (draw_mode, shading)
&& let Some(meshes) = mesh_shading(sp, &mut self.colors)
{
self.draw_meshes(meshes, &bp, sp.opacity);
return;
}
let pd = PathData::from_bezpath(&bp);
let stroke = matches!(draw_mode, PathDrawMode::Stroke(_));
let stroke = matches!(draw_mode, DrawMode::Stroke(_));
let (paint, opacity) = self.paint(paint, stroke);
// A gradient that doesn't extend past its ends paints only between them.
let width = match draw_mode {
PathDrawMode::Stroke(p) => p.line_width as f64 * mean_scale(transform),
PathDrawMode::Fill(_) => 0.0,
DrawMode::Stroke(p) => p.line_width as f64 * mean_scale(transform),
_ => 0.0,
};
let band = shading.and_then(|sp| extend_clip(sp, bp.bounding_box().inflate(width, width)));
let wrap = |b: &mut Self, n: Node| match &band {
@@ -1753,7 +1772,7 @@ impl<'a> Device<'a> for Builder<'_> {
None => n,
};
match draw_mode {
PathDrawMode::Fill(rule) => {
DrawMode::Fill(rule) => {
let mut f = FillLayer::new(paint);
(f.opacity, f.overprint) = (opacity, overprint);
let mut n = Node::path(self.id(), pd, Appearance { items: vec![AppearanceItem::Fill(f)], ..Default::default() });
@@ -1764,7 +1783,7 @@ impl<'a> Device<'a> for Builder<'_> {
let n = wrap(self, n);
self.push_node(n);
}
PathDrawMode::Stroke(props) => {
DrawMode::Stroke(props) => {
let mut st = stroke_layer(paint, opacity, props, mean_scale(transform));
st.overprint = overprint;
// Fill-then-stroke of the same path (the `B` operator, or a fill and then a stroke
@@ -1799,6 +1818,71 @@ impl<'a> Device<'a> for Builder<'_> {
let n = wrap(self, n);
self.push_node(n);
}
// (`draw_path` gives a fill and a stroke one after the other, and no path is invisible.)
DrawMode::FillAndStroke(..) | DrawMode::Invisible => {}
}
}
/// One glyph: filled, or stroked with `stroke`.
fn glyph(
&mut self,
glyph: &Glyph<'a>,
transform: Affine,
glyph_transform: Affine,
paint: &hayro_interpret::Paint<'a>,
stroke: Option<&StrokeProps>,
) {
self.pending = false;
match glyph {
Glyph::Outline(o) => {
let scale = mean_scale(transform);
let overprint = self.glyph_overprint(stroke.is_some());
if self.type_glyph(o, transform * glyph_transform, scale, paint, stroke, overprint) {
return;
}
self.flush_text();
let mut bp = o.outline();
bp.apply_affine(transform * glyph_transform);
let (paint, opacity) = self.paint(paint, stroke.is_some());
let same = self.glyphs.as_ref().is_some_and(|r| {
r.paint == paint
&& r.opacity == opacity
&& r.overprint == overprint
&& r.stroke.as_ref().map(|s| s.line_width) == stroke.map(|s| s.line_width)
&& (r.scale - scale).abs() < 1e-9
});
if !same {
self.flush_glyphs();
self.glyphs = Some(GlyphRun { path: BezPath::new(), paint, opacity, stroke: stroke.cloned(), scale, overprint });
}
if let Some(r) = &mut self.glyphs {
r.path.extend(bp.iter());
}
self.warn("text was converted to outlines");
}
Glyph::Type3(t3) => {
if self.nested >= MAX_NESTED {
return;
}
self.flush();
self.nested += 1;
t3.interpret(self, transform, glyph_transform, paint);
self.nested -= 1;
}
}
}
}
impl<'a> Device<'a> for Builder<'_> {
fn draw_path(&mut self, path: &BezPath, props: DrawProps<'a>, draw_mode: &DrawMode) {
self.set_state(props.soft_mask, props.blend_mode);
match draw_mode {
DrawMode::Invisible => {}
DrawMode::FillAndStroke(rule, stroke) => {
self.path(path, props.transform, &props.paint, &DrawMode::Fill(*rule));
self.path(path, props.transform, &props.paint, &DrawMode::Stroke(stroke.clone()));
}
one => self.path(path, props.transform, &props.paint, one),
}
}
@@ -1834,60 +1918,22 @@ impl<'a> Device<'a> for Builder<'_> {
self.blend = BlendMode::Normal;
}
fn draw_glyph(
&mut self,
glyph: &Glyph<'a>,
transform: Affine,
glyph_transform: Affine,
paint: &hayro_interpret::Paint<'a>,
draw_mode: &GlyphDrawMode,
) {
self.pending = false;
fn draw_glyph_run(&mut self, glyph_run: &ShownGlyphs<'_, 'a>, props: DrawProps<'a>, draw_mode: &DrawMode) {
// (A run is filled or stroked: one filled and stroked is given twice.)
let stroke = match draw_mode {
GlyphDrawMode::Invisible => return,
GlyphDrawMode::Fill => None,
GlyphDrawMode::Stroke(p) => Some(p.clone()),
DrawMode::Invisible => return,
DrawMode::Fill(_) => None,
DrawMode::Stroke(p) | DrawMode::FillAndStroke(_, p) => Some(p),
};
match glyph {
Glyph::Outline(o) => {
let scale = mean_scale(transform);
let overprint = self.glyph_overprint(stroke.is_some());
if self.type_glyph(o, transform * glyph_transform, scale, paint, stroke.as_ref(), overprint) {
return;
}
self.flush_text();
let mut bp = o.outline();
bp.apply_affine(transform * glyph_transform);
let (paint, opacity) = self.paint(paint, stroke.is_some());
let same = self.glyphs.as_ref().is_some_and(|r| {
r.paint == paint
&& r.opacity == opacity
&& r.overprint == overprint
&& r.stroke.as_ref().map(|s| s.line_width) == stroke.as_ref().map(|s| s.line_width)
&& (r.scale - scale).abs() < 1e-9
});
if !same {
self.flush_glyphs();
self.glyphs = Some(GlyphRun { path: BezPath::new(), paint, opacity, stroke, scale, overprint });
}
if let Some(r) = &mut self.glyphs {
r.path.extend(bp.iter());
}
self.warn("text was converted to outlines");
}
Glyph::Type3(t3) => {
if self.nested >= MAX_NESTED {
return;
}
self.flush();
self.nested += 1;
t3.interpret(self, transform, glyph_transform, paint);
self.nested -= 1;
}
for g in glyph_run.glyphs() {
self.set_state(props.soft_mask.clone(), props.blend_mode);
self.glyph(g, props.transform, g.transform(), &props.paint, stroke);
}
}
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
fn draw_image(&mut self, image: Image<'a, '_>, props: ImageDrawProps<'a>) {
self.set_state(props.soft_mask, props.blend_mode);
let transform = props.transform;
self.flush_glyphs();
self.flush_text();
self.pending = false;
@@ -1975,7 +2021,7 @@ impl<'a> Device<'a> for Builder<'_> {
}
}
fn pop_clip_path(&mut self) {
fn pop_clip(&mut self) {
self.flush();
self.pending = false;
self.pop_frame();
+1 -1
View File
@@ -379,7 +379,7 @@ impl<'p> Colors<'p> {
}
/// An object written in place (references are visited as objects of their own).
fn direct(m: MaybeRef<Object<'_>>) -> Option<Object<'_>> {
pub(crate) fn direct(m: MaybeRef<Object<'_>>) -> Option<Object<'_>> {
match m {
MaybeRef::NotRef(o) => Some(o),
MaybeRef::Ref(_) => None,
+2 -1
View File
@@ -485,7 +485,8 @@ impl Walker<'_, '_> {
{
self.out.groups.push((g.get::<bool>(b"I").unwrap_or(false), g.get::<bool>(b"K").unwrap_or(false)));
}
let inner = Resources::from_parent(d.get::<Dict<'_>>(b"Resources").unwrap_or_default(), res.clone());
// (Its own resources, or without any those of what draws it: as the interpreter reads them.)
let inner = d.get::<Dict<'_>>(b"Resources").map(Resources::new).unwrap_or_else(|| res.clone());
self.save();
self.walk(TypedIter::new(data.as_ref()), &inner, depth + 1);
self.restore();
+2 -2
View File
@@ -34,7 +34,7 @@ pub(crate) fn shading_gradient(sp: &ShadingPattern, colors: &mut Colors<'_>) ->
let space = colors.shading_space(cs, at(0.0)?.len());
// RGB to decide which samples are stops; the stops keep the shading's model.
let sample = |t: f32| -> Option<Color> {
let [r, g, b, _] = cs.to_rgba(&at(t)?, 1.0, false).components();
let [r, g, b, _] = cs.to_rgba(&at(t)?, 1.0).components();
Some(Color::rgb(round3(r), round3(g), round3(b)))
};
// Sample densely, then drop samples that linear interpolation reproduces.
@@ -147,7 +147,7 @@ fn mesh_color(v: &[f32], function: Option<&ShadingFunction>, sp: &ShadingPattern
};
let space = colors.shading_space(&sp.shading.color_space, comps.len());
colors.native(space, &comps).map(|n| n.color).unwrap_or_else(|| {
let [r, g, b, _] = sp.shading.color_space.to_rgba(&comps, 1.0, false).components();
let [r, g, b, _] = sp.shading.color_space.to_rgba(&comps, 1.0).components();
Color::rgb(round3(r), round3(g), round3(b))
})
}
+1
View File
@@ -64,6 +64,7 @@ mod post;
mod presets;
mod print;
mod settings;
mod streams;
mod syntax;
pub use editing::{EDITING_FILE, Editing, LEGACY_EDITING_FILE, editing, editing_with};
+121 -58
View File
@@ -1,12 +1,16 @@
//! The pages of a PDF to import: opening with a password, the pages picked, the box each page
//! is cropped to ([`CropTo`]) and [`info`].
use hayro_syntax::object::{Dict, Stream};
use std::collections::HashMap;
use hayro_syntax::object::{Array, Dict, MaybeRef, Name, Object, ObjectIdentifier, Stream, dict_or_stream};
use hayro_syntax::page::Page;
use hayro_syntax::xref::XRef;
use hayro_syntax::{DecryptionError, LoadPdfError, Pdf};
use kurbo::{Affine, Rect};
use vectorcraft_doc::setup::MAX_BLEED;
use crate::import_color::direct;
use crate::{Choice, CropTo, ImportOptions, PdfError};
/// What `info` tells about one page.
@@ -33,75 +37,134 @@ pub fn is_postscript(bytes: &[u8]) -> bool {
bytes.starts_with(b"%!PS") || bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6])
}
/// The deepest a file's arrays and dictionaries may nest. The PDF reader (hayro) reads them by
/// recursion, without a cap of its own: a file nested deeply enough overflows the stack, which is
/// an abort, not an error. Far beyond what a PDF writer makes.
const MAX_OBJECT_NESTING: usize = 256;
/// The most functions and colour spaces that may be built on one another (a function stitching
/// functions together, an indexed space on a space with an alternate). The interpreter (hayro)
/// builds them by recursion, without a cap of its own: one built on itself, or thousands in a
/// chain, overflows the stack, which is an abort, not an error. Files have two or three.
const MAX_LINKS: usize = 16;
fn too_deep() -> PdfError {
PdfError::Parse(format!("its arrays and dictionaries are nested more than {MAX_OBJECT_NESTING} deep"))
/// The deepest the procedures of a calculator function may nest: read by recursion, too.
const MAX_PROCEDURES: usize = 64;
/// What the function or colour space `o` is built on: the functions a stitching function joins;
/// the base, alternate and tint of a colour space; the alternate of a colour profile. Nothing for
/// any other object.
fn links<'a>(o: &Object<'a>, xref: &'a XRef) -> Vec<MaybeRef<Object<'a>>> {
const SPACES: [&[u8]; 6] = [b"Indexed", b"I", b"Separation", b"DeviceN", b"Pattern", b"ICCBased"];
if let Object::Array(a) = o {
let mut items = a.raw_iter();
let kind = match items.next() {
Some(MaybeRef::NotRef(Object::Name(n))) => Some(n),
Some(MaybeRef::Ref(r)) => xref.get::<Name<'a>>(r.into()),
_ => None,
};
return if kind.is_some_and(|n| SPACES.contains(&n.as_ref())) { items.collect() } else { vec![] };
}
let Some((d, _)) = dict_or_stream(o) else { return vec![] };
let functions = d.get::<Array<'a>>(b"Functions").map(|a| a.raw_iter().collect::<Vec<_>>()).unwrap_or_default();
functions.into_iter().chain(d.get_raw::<Object<'a>>(b"Alternate")).collect()
}
/// Do the arrays and dictionaries in the text of `pdf` nest deeper than [`MAX_OBJECT_NESTING`]?
/// Read as tokens (strings and comments hold none), an object at a time. The data of a stream
/// with a filter is skipped: it isn't PDF text until decoded, which this doesn't do, so nesting
/// inside compressed streams is not seen here.
fn nests_too_deep(pdf: &[u8]) -> bool {
let after =
|from: usize, what: &[u8]| pdf.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at + what.len());
let word = |at: usize, w: &[u8]| pdf.get(at..).is_some_and(|rest| rest.starts_with(w));
let (mut depth, mut filtered) = (0usize, false);
let mut i = 0;
while let Some(&c) = pdf.get(i) {
i += 1;
/// How many of its kind the function or colour space `o` is built on, one on the next (0 for any
/// other object); `None` past `room`, or when it is built on itself. `known`: the objects looked
/// at so far (`None` while one is being looked at, too). One `None` refuses the file, so that an
/// object is `None` only for the room there was where it was first met doesn't matter.
fn built_on<'a>(o: MaybeRef<Object<'a>>, xref: &'a XRef, known: &mut HashMap<ObjectIdentifier, Option<usize>>, room: usize) -> Option<usize> {
let (id, o) = match o {
MaybeRef::Ref(r) => {
let id = ObjectIdentifier::from(r);
if let Some(found) = known.get(&id) {
return *found;
}
let Some(o) = xref.get::<Object<'a>>(id) else { return Some(0) };
known.insert(id, None);
(Some(id), o)
}
MaybeRef::NotRef(o) => (None, o),
};
let links = links(&o, xref);
let most = match room.checked_sub(1) {
_ if links.is_empty() => Some(0),
Some(room) => links.into_iter().try_fold(0, |most, on| built_on(on, xref, known, room).map(|n| most.max(n + 1))),
None => None,
};
// (What it is built on may be known already, and deeper than there is room for here.)
let most = most.filter(|n| *n <= room);
if let Some(id) = id {
known.insert(id, most);
}
most
}
/// Is `o` a calculator function whose procedures nest deeper than [`MAX_PROCEDURES`]?
fn procedures_too_deep(o: &Object<'_>) -> bool {
let Object::Stream(s) = o else { return false };
if s.dict().get::<u8>(b"FunctionType") != Some(4) {
return false;
}
let Ok(program) = s.decoded() else { return false };
let mut depth = 0usize;
program.iter().any(|c| {
match c {
b'%' => i += pdf.get(i..).and_then(|rest| rest.iter().position(|c| matches!(c, b'\n' | b'\r'))).unwrap_or(pdf.len()),
b'(' => {
// A string: parentheses nest in it, a backslash takes the next byte.
let mut open = 1usize;
while open > 0 {
match pdf.get(i) {
None => return false,
Some(b'\\') => i += 1,
Some(b'(') => open += 1,
Some(b')') => open -= 1,
Some(_) => {}
}
i += 1;
}
}
b'[' => depth += 1,
b']' => depth = depth.saturating_sub(1),
b'<' if pdf.get(i) == Some(&b'<') => (depth, i) = (depth + 1, i + 1),
b'>' if pdf.get(i) == Some(&b'>') => (depth, i) = (depth.saturating_sub(1), i + 1),
// A string in hexadecimal.
b'<' => i = after(i, b">").unwrap_or(pdf.len()),
b'/' if word(i - 1, b"/Filter") => filtered = true,
b'e' if word(i - 1, b"endobj") => (depth, filtered) = (0, false),
b'e' if word(i - 1, b"endstream") => i += 8,
b's' if word(i - 1, b"stream") => {
depth = 0;
if std::mem::take(&mut filtered) {
i = after(i, b"endstream").unwrap_or(pdf.len());
}
}
b'{' => depth += 1,
b'}' => depth = depth.saturating_sub(1),
_ => {}
}
if depth > MAX_OBJECT_NESTING {
depth > MAX_PROCEDURES
})
}
/// The error of a file with a stream the reader would fail on ([`crate::streams`]).
const STREAM_NOTE: &str = "a stream in it would decode to more than 256 MB, or its decode parameters can't be applied";
/// The error of a file [`nests_without_end`].
const NESTED_NOTE: &str = "its functions or colour spaces are built on one another without end";
/// Why `pdf` is refused, if it is: a stream the reader would fail on (its own checks come first:
/// the calculator functions are decoded below), or [`nests_without_end`].
fn hostile(pdf: &Pdf) -> Option<&'static str> {
if pdf.objects().into_iter().any(|o| matches!(&o, Object::Stream(s) if crate::streams::unsafe_stream(s))) {
return Some(STREAM_NOTE);
}
nests_without_end(pdf).then_some(NESTED_NOTE)
}
/// Does `pdf` hold functions or colour spaces built on one another past [`MAX_LINKS`] (or on
/// themselves), or a calculator function nested past [`MAX_PROCEDURES`]? Every object is looked
/// at, wherever it is used from: the interpreter builds them from resources, shadings, images,
/// patterns, masks and graphics states alike.
fn nests_without_end(pdf: &Pdf) -> bool {
let xref = pdf.xref();
let mut known = HashMap::new();
// The objects of the file, then what each holds directly (64 deep at most: the reader's cap).
let mut pending: Vec<Object<'_>> = pdf.objects().into_iter().collect();
while let Some(o) = pending.pop() {
if procedures_too_deep(&o) || built_on(MaybeRef::NotRef(o.clone()), xref, &mut known, MAX_LINKS).is_none() {
return true;
}
match &o {
Object::Array(a) => pending.extend(a.raw_iter().filter_map(direct)),
_ => pending.extend(dict_or_stream(&o).into_iter().flat_map(|(d, _)| d.entries()).filter_map(|(_, v)| direct(v))),
}
}
false
}
/// Read a PDF, decrypting it with `password` (none: the empty user password). The permissions
/// password opens a file too.
///
/// What a hostile file nests or loops is the reader's to refuse (hayro 0.8 reads arrays and
/// dictionaries 64 deep and no deeper, wherever they are: the file's text, an object stream, the
/// content of a page, form, pattern or glyph; and follows a page tree or a chain of object streams
/// once). What it reads beyond that is left out, as an object it can't read. What the interpreter
/// builds by recursion with no cap, and streams it would decode without a ceiling or with a
/// predictor it can't apply, are refused here ([`hostile`], [`crate::streams`]).
pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError> {
if is_postscript(bytes) {
return Err(PdfError::PostScript);
}
if nests_too_deep(bytes) {
return Err(too_deep());
if crate::streams::unsafe_file_streams(bytes) {
return Err(PdfError::Parse(STREAM_NOTE.into()));
}
let password = password.unwrap_or_default();
let read = |pw: &str| Pdf::new_with_password(bytes.to_vec(), pw);
@@ -119,12 +182,12 @@ pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError
LoadPdfError::Decryption(DecryptionError::PasswordProtected) => PdfError::WrongPassword,
e => PdfError::Parse(format!("{e:?}")),
})
}
/// The error of a page whose content, decoded, nests arrays and dictionaries too deep (see
/// [`MAX_OBJECT_NESTING`]): its operands are read the same way as the file's objects.
pub(crate) fn content_too_deep(page: &Page<'_>) -> Result<(), PdfError> {
if page.page_stream().is_some_and(nests_too_deep) { Err(too_deep()) } else { Ok(()) }
// Twice: looking at every object makes the reader repair a damaged table of objects (once
// only), and the objects are other ones after that.
.and_then(|pdf| match (0..2).find_map(|_| hostile(&pdf)) {
Some(why) => Err(PdfError::Parse(why.into())),
None => Ok(pdf),
})
}
/// The pages and their boxes. An encrypted PDF needs its `password`
+367
View File
@@ -0,0 +1,367 @@
//! Streams looked at before the PDF reader (hayro) decodes them. It inflates Flate and LZW data
//! with no ceiling, so a few megabytes can ask for gigabytes (#1700), and it applies a predictor
//! before checking its parameters: `Columns 0` divides by zero, a huge `Columns` allocates a row
//! that large whatever the stream holds (#1701). A file with such a stream is refused instead.
use std::borrow::Cow;
use std::io::Read;
use hayro_syntax::content::TypedIter;
use hayro_syntax::content::ops::TypedInstruction;
use hayro_syntax::object::{Array, Dict, FromBytes, MaybeRef, Name, Object, Stream};
/// The most a stream may decode to: 256 MB, the pixels of the largest image the app reads (64
/// million, 4 bytes each).
pub(crate) const MAX_DECODED: usize = 256 << 20;
/// The longest row a predictor may have, in bytes.
const MAX_ROW: usize = 1 << 24;
/// A stream that decodes to more than [`MAX_DECODED`].
struct TooBig;
/// The filters of the stream with dictionary `dict`, by name (`Filter` or `F`: a name or an array).
fn filters<'a>(dict: &Dict<'a>) -> Vec<Name<'a>> {
[&b"Filter"[..], b"F"]
.into_iter()
.find_map(|key| dict.get::<Name<'a>>(key).map(|n| vec![n]).or_else(|| dict.get::<Array<'a>>(key).map(|a| a.iter::<Name<'a>>().collect())))
.unwrap_or_default()
}
/// Every decode parameter dictionary of the stream with dictionary `dict`, as the reader may take
/// them (`DecodeParms` or `DP`: a dictionary, or an array with one for each filter).
fn params<'a>(dict: &Dict<'a>) -> Vec<Dict<'a>> {
let mut all = vec![];
for key in [&b"DecodeParms"[..], b"DP"] {
all.extend(dict.get::<Dict<'a>>(key));
all.extend(dict.get::<Array<'a>>(key).into_iter().flat_map(|a| a.iter::<Object<'a>>().filter_map(|o| o.into_dict()).collect::<Vec<_>>()));
}
all
}
/// Would the reader's predictor fail on `params`? It reads `Colors` and `BitsPerComponent` as
/// bytes and `Columns` as a size, multiplies the first two as bytes, divides by the length of a row
/// and allocates one before it checks anything.
fn bad_predictor(params: &Dict<'_>) -> bool {
if params.get::<u8>(b"Predictor").unwrap_or(1) == 1 {
return false;
}
let colors = usize::from(params.get::<u8>(b"Colors").unwrap_or(1));
let bits = usize::from(params.get::<u8>(b"BitsPerComponent").unwrap_or(8));
let columns = params.get::<usize>(b"Columns").unwrap_or(1);
let pixel = colors * bits;
let row = columns.checked_mul(pixel).map(|b| b.div_ceil(8));
!(matches!(bits, 1 | 2 | 4 | 8 | 16) && colors > 0 && pixel <= 255 && columns > 0 && row.is_some_and(|r| r <= MAX_ROW))
}
/// `data` inflated, up to one byte past [`MAX_DECODED`] (zlib, or raw deflate as the reader also
/// takes); `None` when it is damaged before that.
fn inflate(data: &[u8]) -> Option<Vec<u8>> {
let read = |mut r: Box<dyn Read + '_>| {
let mut out = vec![];
// (Damaged data: what came out before the damage still counts.)
let done = r.by_ref().take(MAX_DECODED as u64 + 1).read_to_end(&mut out).is_ok();
(done || out.len() > MAX_DECODED).then_some(out)
};
read(Box::new(flate2::read::ZlibDecoder::new(data))).or_else(|| read(Box::new(flate2::read::DeflateDecoder::new(data))))
}
/// `data` decoded from LZW, up to one byte past [`MAX_DECODED`]. `early`: the code length grows
/// one code early (`EarlyChange`, 1 by default).
fn lzw(data: &[u8], early: bool) -> Option<Vec<u8>> {
use weezl::{BitOrder, LzwStatus, decode::Decoder};
let mut decoder = if early { Decoder::with_tiff_size_switch(BitOrder::Msb, 8) } else { Decoder::new(BitOrder::Msb, 8) };
let (mut out, mut chunk, mut input) = (vec![], vec![0; 1 << 16], data);
while out.len() <= MAX_DECODED {
let r = decoder.decode_bytes(input, &mut chunk);
input = input.get(r.consumed_in..).unwrap_or_default();
out.extend_from_slice(chunk.get(..r.consumed_out).unwrap_or_default());
match r.status {
Ok(LzwStatus::Ok) => {}
Ok(LzwStatus::Done | LzwStatus::NoProgress) => break,
Err(_) => return (out.len() > MAX_DECODED).then_some(out),
}
}
Some(out)
}
/// `data` decoded from run-length encoding, up to one byte past [`MAX_DECODED`].
fn run_length(data: &[u8]) -> Vec<u8> {
let (mut out, mut at) = (vec![], 0);
while let Some(&n) = data.get(at)
&& n != 128
&& out.len() <= MAX_DECODED
{
if n < 128 {
let take = usize::from(n) + 1;
out.extend_from_slice(data.get(at + 1..).unwrap_or_default().get(..take).unwrap_or_default());
at += 1 + take;
} else {
out.extend(std::iter::repeat_n(data.get(at + 1).copied().unwrap_or(0), 257 - usize::from(n)));
at += 2;
}
}
out
}
/// `data` decoded from hexadecimal digits (white space skipped, up to `>`).
fn hex(data: &[u8]) -> Vec<u8> {
let digits: Vec<u8> = data.iter().take_while(|c| **c != b'>').filter_map(|c| char::from(*c).to_digit(16)).map(|d| d as u8).collect();
digits.chunks(2).map(|p| (p.first().copied().unwrap_or(0) << 4) | p.get(1).copied().unwrap_or(0)).collect()
}
/// `data` decoded from base-85 (white space skipped, `z` for four zeros, up to `~>`).
fn base85(data: &[u8]) -> Vec<u8> {
let (mut out, mut group) = (vec![], vec![]);
let flush = |group: &mut Vec<u32>, out: &mut Vec<u8>| {
let n = group.len();
if n > 1 {
group.resize(5, 84);
let v = group.iter().fold(0u32, |v, d| v.wrapping_mul(85).wrapping_add(*d));
out.extend_from_slice(v.to_be_bytes().get(..n - 1).unwrap_or_default());
}
group.clear();
};
for &c in data.iter().take_while(|c| **c != b'~') {
match c {
b'z' if group.is_empty() => out.extend([0; 4]),
b'!'..=b'u' => {
group.push(u32::from(c - b'!'));
if group.len() == 5 {
flush(&mut group, &mut out);
}
}
_ => {}
}
}
flush(&mut group, &mut out);
out
}
/// The most data of `len` bytes can grow to through `filters`: 1,032 times through Flate (its
/// largest ratio), 3,641 through LZW (4,096 bytes from a 9-bit code), 64 through run-length.
fn most_after(len: usize, filters: &[Name<'_>]) -> u128 {
filters.iter().fold(len as u128, |n, f| {
n.saturating_mul(match f.as_ref() {
b"FlateDecode" | b"Fl" => 1032,
b"LZWDecode" | b"LZW" => 3641,
b"RunLengthDecode" | b"RL" => 64,
_ => 1,
})
})
}
/// `data` (of the stream with dictionary `dict`) through its filters, as far as this reads them:
/// Flate, LZW, run-length and the ASCII encodings. Not decoded when it can't decode past
/// [`MAX_DECODED`] and `keep` doesn't ask for it, or when an image codec or damaged data comes
/// first (an image's own decoder knows its size).
fn decode(data: &[u8], dict: &Dict<'_>, keep: bool) -> Result<Option<Vec<u8>>, TooBig> {
let filters = filters(dict);
if !keep && most_after(data.len(), &filters) <= MAX_DECODED as u128 {
return Ok(None);
}
let params = params(dict);
let mut data = Cow::Borrowed(data);
for (i, f) in filters.iter().enumerate() {
let early = params.get(i).and_then(|p| p.get::<u8>(b"EarlyChange")).is_none_or(|e| e != 0);
let out = match f.as_ref() {
b"FlateDecode" | b"Fl" => inflate(&data),
b"LZWDecode" | b"LZW" => lzw(&data, early),
b"RunLengthDecode" | b"RL" => Some(run_length(&data)),
b"ASCIIHexDecode" | b"AHx" => Some(hex(&data)),
b"ASCII85Decode" | b"A85" => Some(base85(&data)),
_ => None,
};
match out {
Some(out) if out.len() > MAX_DECODED => return Err(TooBig),
Some(out) => data = Cow::Owned(out),
None => return Ok(None),
}
}
Ok(Some(data.into_owned()))
}
/// May the stream with dictionary `dict` be content (of a page, form, pattern or glyph), which can
/// hold inline images? Not an image, font, profile, function, metadata or object stream.
fn maybe_content(dict: &Dict<'_>) -> bool {
let named = |key: &[u8], ok: &[&[u8]]| dict.get::<Name<'_>>(key).is_none_or(|n| ok.contains(&n.as_ref()));
named(b"Subtype", &[b"Form"])
&& named(b"Type", &[b"XObject", b"Pattern"])
&& ![&b"Length1"[..], b"Length2", b"Length3", b"FunctionType", b"N"].iter().any(|k| dict.contains_key(k))
}
/// Would the reader fail on `stream` (or on an inline image in it, when it is content): a
/// predictor it can't apply, or more than [`MAX_DECODED`] decoded?
pub(crate) fn unsafe_stream(stream: &Stream<'_>) -> bool {
unsafe_data(stream, true)
}
/// [`unsafe_stream`], looking for inline images in it when `inside` (not in an inline image's own
/// data: it would be read again for each inline image a hostile one nests).
fn unsafe_data(stream: &Stream<'_>, inside: bool) -> bool {
let dict = stream.dict();
if params(dict).iter().any(bad_predictor) {
return true;
}
let content = inside && maybe_content(dict);
match decode(&stream.raw_data(), dict, content) {
Err(TooBig) => true,
Ok(Some(data)) if content && data.windows(2).any(|w| w == b"ID") => {
let mut ops = TypedIter::new(&data);
while let Some(op) = ops.next() {
if let TypedInstruction::InlineImage(image) = op
&& unsafe_data(image.0, false)
{
return true;
}
}
false
}
Ok(_) => false,
}
}
/// Would the reader fail on a stream it decodes while it opens `pdf`, before anything else can
/// look at it: an object stream or a cross-reference stream (read here from the file's text)?
/// Their decode parameters must be in their dictionary, as the reader can't be asked for others
/// yet. Encrypted object streams are looked at once the file is open (their data is ciphered
/// here).
pub(crate) fn unsafe_file_streams(pdf: &[u8]) -> bool {
let find = |from: usize, what: &[u8]| pdf.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at);
let mut from = 0;
while let Some(at) = find(from, b"stream") {
from = at + 6;
if at >= 3 && pdf.get(at - 3..at) == Some(b"end") {
continue;
}
// The dictionary before it: back from its `>>` to the `<<` that opens it.
let Some(close) = pdf.get(..at).and_then(|head| head.iter().rposition(|c| !c.is_ascii_whitespace())).map(|i| i + 1) else { continue };
if close < 2 || pdf.get(close - 2..close) != Some(b">>") {
continue;
}
let (mut depth, mut open, mut i) = (0usize, None, close);
while i >= 2 && close - i < 1 << 16 {
match pdf.get(i - 2..i) {
Some(b">>") => (depth, i) = (depth + 1, i - 2),
Some(b"<<") => {
depth -= 1;
i -= 2;
if depth == 0 {
open = Some(i);
break;
}
}
_ => i -= 1,
}
}
let Some(dict) = open.and_then(|o| pdf.get(o..close)).and_then(Dict::from_bytes) else { continue };
if !dict.get::<Name<'_>>(b"Type").is_some_and(|t| matches!(t.as_ref(), b"ObjStm" | b"XRef")) {
continue;
}
let indirect = [&b"DecodeParms"[..], b"DP"].iter().any(|k| matches!(dict.get_raw::<Object<'_>>(k), Some(MaybeRef::Ref(_))));
if indirect || params(&dict).iter().any(bad_predictor) {
return true;
}
let start = from
+ usize::from(pdf.get(from) == Some(&b'\r'))
+ usize::from(pdf.get(from + usize::from(pdf.get(from) == Some(&b'\r'))) == Some(&b'\n'));
let end = dict
.get::<usize>(b"Length")
.and_then(|n| start.checked_add(n))
.filter(|e| *e <= pdf.len())
.or_else(|| find(start, b"endstream"))
.unwrap_or(pdf.len());
if matches!(decode(pdf.get(start..end).unwrap_or_default(), &dict, false), Err(TooBig)) {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn dict(text: &str) -> Dict<'_> {
Dict::from_bytes(text.as_bytes()).unwrap()
}
#[test]
fn predictors_the_reader_can_apply_pass() {
for ok in [
"<< >>",
"<< /Predictor 1 /Columns 0 >>",
"<< /Predictor 12 /Columns 5 >>",
"<< /Predictor 2 /Colors 3 /BitsPerComponent 16 /Columns 1000 >>",
] {
assert!(!bad_predictor(&dict(ok)), "{ok}");
}
for bad in [
"<< /Predictor 2 /Columns 0 >>",
"<< /Predictor 12 /Colors 0 >>",
"<< /Predictor 2 /BitsPerComponent 0 >>",
"<< /Predictor 2 /BitsPerComponent 3 >>",
"<< /Predictor 12 /Colors 32 /BitsPerComponent 16 >>",
"<< /Predictor 15 /Columns 99999999999 >>",
"<< /Predictor 10 /Columns 18446744073709551615 /Colors 4 >>",
] {
assert!(bad_predictor(&dict(bad)), "{bad}");
}
}
#[test]
fn the_decoders_read_what_the_specification_writes() {
assert_eq!(hex(b"48 65 6c6C6\n>junk"), b"Hell`");
assert_eq!(base85(b"87cURD]i,\"Ebo80~>"), b"Hello World!");
assert_eq!(base85(b"z!!~>"), [0, 0, 0, 0, 0]);
assert_eq!(run_length(&[2, b'a', b'b', b'c', 254, b'x', 128, b'q']), b"abcxxx");
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
std::io::Write::write_all(&mut e, &[7; 1000]).unwrap();
assert_eq!(inflate(&e.finish().unwrap()).unwrap(), [7; 1000]);
// "-----A---B" in LZW, as the specification's example writes it (7.4.4.2).
let lzw_example = [0x80, 0x0B, 0x60, 0x50, 0x22, 0x0C, 0x0C, 0x85, 0x01];
assert_eq!(lzw(&lzw_example, true).unwrap(), b"-----A---B");
}
/// Zeros past the cap, compressed.
fn zeros_past_the_cap() -> Vec<u8> {
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
for _ in 0..(MAX_DECODED >> 20) + 1 {
std::io::Write::write_all(&mut e, &[0; 1 << 20]).unwrap();
}
e.finish().unwrap()
}
#[test]
fn a_stream_that_decodes_past_the_cap_is_found_without_holding_it() {
// Inflated in pieces: never more than one byte past the cap is held.
let bomb = zeros_past_the_cap();
assert!(bomb.len() < MAX_DECODED / 500);
assert_eq!(inflate(&bomb).map(|d| d.len()), Some(MAX_DECODED + 1));
let d = dict("<< /Filter /FlateDecode >>");
assert!(matches!(decode(&bomb, &d, false), Err(TooBig)));
// Small enough that it can't get there: not decoded at all.
assert!(matches!(decode(&[1, 2, 3], &d, false), Ok(None)));
}
#[test]
fn object_and_cross_reference_streams_are_looked_at_before_the_file_is_read() {
let stream = |dict: &str, data: &[u8]| {
[format!("1 0 obj\n<< {dict} /Length {} >>\nstream\n", data.len()).as_bytes(), data, b"\nendstream\nendobj\n"].concat()
};
for bad in [
stream("/Type /ObjStm /N 1 /First 4 /Filter /FlateDecode /DecodeParms << /Predictor 2 /Columns 0 >>", b"x"),
stream("/Type /XRef /Filter /FlateDecode /DecodeParms 9 0 R", b"x"),
stream("/Type /ObjStm /N 1 /First 4 /Filter /FlateDecode", &zeros_past_the_cap()),
] {
assert!(unsafe_file_streams(&bad));
}
for fine in [
stream("/Type /XRef /Filter /FlateDecode /DecodeParms << /Predictor 12 /Columns 5 >>", b"x"),
// Another kind of stream is looked at once the file is open, its references followed.
stream("/Filter /FlateDecode /DecodeParms << /Predictor 2 /Columns 0 >>", b"x"),
] {
assert!(!unsafe_file_streams(&fine));
}
}
}
+43
View File
@@ -753,3 +753,46 @@ fn empty_and_populated_file_info_preserve_key_presence() {
}
assert!(s.contains("Synthetic title") && s.contains("Test author") && s.contains("test keyword"));
}
/// A function or colour space built on itself is refused before the interpreter builds it (by
/// recursion, with no cap: a stack overflow), and those of real files are read.
#[test]
fn functions_and_colour_spaces_built_on_themselves_are_refused() {
use vectorcraft_testkit::pdf::{PdfPage, first_extra, pdf_with};
let first = first_extra(1);
let (me, next) = (format!("{first} 0 R"), format!("{} 0 R", first + 1));
let shaded = PdfPage {
resources: format!("/Shading << /S << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function {me} >> >>"),
..PdfPage::new(100.0, 100.0, "/S sh")
};
let filled = PdfPage { resources: format!("/ColorSpace << /C {me} >>"), ..PdfPage::new(100.0, 100.0, "/C cs 0 sc 0 0 50 50 re f") };
let stitch = |on: &str| format!("<< /FunctionType 3 /Domain [0 1] /Functions [{on}] /Bounds [] /Encode [0 1] >>");
let indexed = |on: &str| format!("[/Indexed {on} 1 <000000ffffff>]");
let ramp = "<< /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >>";
// `n` functions, each stitching the next, then a plain one.
let chain = |n: usize| (1..=n).map(|i| stitch(&format!("{} 0 R", first + i))).chain([ramp.to_string()]).collect::<Vec<_>>();
let calculator =
|program: &str| format!("<< /FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1] /Length {} >>\nstream\n{program}\nendstream", program.len());
for (what, page, objects) in [
("a function stitching itself", &shaded, vec![stitch(&me)]),
("two functions stitching each other", &shaded, vec![stitch(&next), stitch(&me)]),
("twenty functions, each stitching the next", &shaded, chain(20)),
("procedures in procedures", &shaded, vec![calculator(&format!("{}{}", "{".repeat(100), "}".repeat(100)))]),
("a colour space on itself", &filled, vec![indexed(&me)]),
("two colour spaces on each other", &filled, vec![indexed(&next), indexed(&me)]),
] {
let objects: Vec<&str> = objects.iter().map(String::as_str).collect();
let e = import(&pdf_with(std::slice::from_ref(page), &objects, None)).err().map(|e| e.to_string());
assert!(e.as_deref().is_some_and(|e| e.contains("built on one another without end")), "{what}: {e:?}");
}
for (what, page, objects) in [
("a function stitching one", &shaded, vec![stitch(&next), ramp.to_string()]),
("three functions, each stitching the next", &shaded, chain(3)),
("a calculator function", &shaded, vec![calculator("{ dup 1 exch sub 0 }")]),
("an indexed colour space", &filled, vec![indexed("/DeviceRGB")]),
] {
let objects: Vec<&str> = objects.iter().map(String::as_str).collect();
let d = import(&pdf_with(std::slice::from_ref(page), &objects, None)).unwrap_or_else(|e| panic!("{what}: {e}"));
assert!(d.layers.iter().any(|l| l.children().is_some_and(|c| !c.is_empty())), "{what}: its art is read");
}
}
+3 -2
View File
@@ -287,7 +287,7 @@ fn a_sublayer_that_paints_first_brings_its_parent_layer() {
#[test]
fn hostile_layer_orders_stay_bounded() {
// A group listed twice, under itself, and an order nested far deeper than layers go.
let deep = format!("{}{{2}}{}", "[".repeat(100), "]".repeat(100));
let deep = format!("{}{{2}}{}", "[".repeat(40), "]".repeat(40));
let order = format!("[{{0}} [{{0}} {{1}}] {{1}} [{{0}}] {deep}]");
let d = import(&three_groups([&ocg("A", ""), &ocg("B", ""), &ocg("C", "")], &[], &format!("/Order {order}"))).unwrap();
assert_eq!(names(&d), ["A", "C"], "C, listed too deep, stays a top-level layer");
@@ -317,7 +317,8 @@ fn thousands_of_layers_hidden_ones_included_all_come_in() {
/// Import `bytes` with the scan's operator limit lowered to `n`.
fn import_with_max_ops(bytes: &[u8], n: usize) -> ImportReport {
let old = crate::import_scan::TEST_MAX_OPS.replace(n);
let r = import_with_report(bytes, &ImportOptions::default());
// (On this thread: the limit is this thread's.)
let r = crate::import::read(bytes, &ImportOptions::default());
crate::import_scan::TEST_MAX_OPS.set(old);
r.unwrap()
}