Files
vectorcraft/crates/pdf/src/import/entry.rs
T
bflatastic a7125f9e9f PDF import: bound the objects an import makes and the form content a page walks (#3569)
A page drawing one form over and over made its art again for every drawing
with no limit (a 1 KB file took 877 MB), and a form decoding to 64 MiB drawn
a thousand times hung. The import now makes at most half the objects a saved
document may hold (2M, 256K on the web), leaving the rest out with a warning,
and the scan reads at most 2 GiB of form content per page before refusing it
as past the reader's limits.
2026-10-29 21:48:36 +07:00

599 lines
28 KiB
Rust

use super::cid::drawn_paths;
use super::state::Builder;
use super::{
Affine, Arc, Artboard, AtomicBool, BlendMode, ColorMode, Colors, Context, CropTo, Document, FillRule, HashMap, HashSet, ImportOptions,
ImportReport, InterpreterCache, InterpreterSettings, InterpreterWarning, LayerColor, MAX_NESTING, Mutex, Node, NodeKind, Ocgs, Ordering, Paint,
PdfError, Rect, Shape, Swatch, all_on, contains, hides_forms, scan_page, white_cover,
};
/// Mixed into an image's cache key when it is read white inside an alpha soft mask.
pub(super) const WHITE_KEY: u128 = 0x7717_e000_a1fa;
/// The name of the paths text imports as.
pub(super) const TEXT_OUTLINES: &str = "<Text Outlines>";
/// A page whose overprint (ISO 32000-1 §8.6.7) is left out rather than given to the wrong objects.
pub(super) const OVERPRINT_UNREAD: &str = "the overprint of a page's objects couldn't be read: they knock out";
/// The document overprints as overprint mode 1 does.
pub(super) const OVERPRINT_ZERO_MODE: &str =
"overprint in the mode where a process colour's zero tints knock out (/OPM 0) is imported as overprint, where they don't";
/// The deepest soft masks, patterns and type 3 glyphs are read within each other.
pub(super) const MAX_NESTED: u32 = 8;
/// A tiling pattern read this many times with the same art is taken to always draw it.
pub(super) const PATTERN_REUSE: u32 = 16;
/// The most objects one import makes (paths, images, lines of type, and the copies of mask art
/// each masked object takes): half of what a saved document may hold to open again
/// ([`vectorcraft_doc::node::MAX_READ_NODES`]: 4M, 512K in the web build), leaving room for the
/// groups, clips and layers around them. A page of a million paths still comes in whole, as an
/// SVG or CAD drawing of two million objects does. What it stops is a small file drawing one form
/// over and over, which neither the scan's operator limit nor the interpreter's budgets bound
/// (#3569: 1 KB drawing 25 squares 32,000 times took 877 MB, about 1 KB an object, so this is
/// about 2 GB).
const MAX_OBJECTS: usize = vectorcraft_doc::node::MAX_READ_NODES / 2;
/// Warning: the import reached [`MAX_OBJECTS`].
pub(super) const TOO_MANY_OBJECTS: &str = "the file draws more objects than can be imported: the rest were left out";
/// [`MAX_OBJECTS`], lowered by tests that need an import past it.
#[cfg(not(test))]
pub(super) fn max_objects() -> usize {
MAX_OBJECTS
}
#[cfg(test)]
thread_local! {
pub(crate) static TEST_MAX_OBJECTS: std::cell::Cell<usize> = const { std::cell::Cell::new(MAX_OBJECTS) };
}
#[cfg(test)]
pub(super) fn max_objects() -> usize {
TEST_MAX_OBJECTS.with(std::cell::Cell::get)
}
/// Stands for the group of art whose group couldn't be told (see [`Builder::unsure`]): it goes
/// to a hidden, non-printing layer of its page.
pub(super) const UNSORTED: usize = usize::MAX;
/// Import a PDF (or PDF-compatible `.ai`) with default options.
pub fn import(bytes: &[u8]) -> Result<Document, PdfError> {
import_with_report(bytes, &ImportOptions::default()).map(|r| r.document)
}
/// A layer of the imported document, in paint order.
enum Slot {
/// A page's art outside optional content groups.
Page(Box<Node>),
/// An optional content group (index into [`Ocgs::list`]).
Group(usize),
}
/// What each group's layer holds, in paint order: its sublayers (their groups) and, where its
/// own group is listed, its art.
type Nesting = HashMap<usize, Vec<usize>>;
/// Group `g` paints (its first art): note where its art goes in its layer, and where that layer
/// goes when it is new: in its parent's layer (placed the same way) or, for a top-level group, in
/// `slots`.
fn place_group(g: usize, ocgs: &Ocgs, slots: &mut Vec<Slot>, nesting: &mut Nesting) {
let placed = nesting.contains_key(&g);
nesting.entry(g).or_default().push(g);
if placed {
return;
}
let mut child = g;
// Each parent is placed before its sublayers, so the chain ends (within the nesting depth).
for _ in 0..=MAX_NESTING {
let Some(parent) = ocgs.list.get(child).and_then(|o| o.parent) else { break };
let placed = nesting.contains_key(&parent);
nesting.entry(parent).or_default().push(child);
if placed {
return;
}
child = parent;
}
slots.push(Slot::Group(child));
}
/// The top-level group that group `g` is listed under (`g` itself when it isn't a sublayer).
fn top_group(g: usize, ocgs: &Ocgs) -> usize {
let mut top = g;
for _ in 0..=MAX_NESTING {
match ocgs.list.get(top).and_then(|o| o.parent) {
Some(parent) => top = parent,
None => break,
}
}
top
}
/// Merge the top-level groups a page marks (`page`, in the order it first marks each, its empty
/// ones too) into the stacking order of the pages before (`order`, bottom first; `known`: its
/// groups). A group new to it goes right below the first group the page marks after it that it
/// has: a layer whose art starts on a later page keeps its place among the others (#508).
fn merge_order(order: &mut Vec<usize>, known: &mut HashSet<usize>, page: &[usize]) {
let mut above = None;
for &g in page.iter().rev() {
if known.insert(g) {
let at = above.and_then(|a| order.iter().position(|&x| x == a)).unwrap_or(order.len());
order.insert(at, g);
}
above = Some(g);
}
}
/// The layer of group `g`, in `color`: its art and sublayers. `None` for a group that isn't read.
fn group_layer(
b: &mut Builder<'_>,
g: usize,
ocgs: &Ocgs,
nesting: &Nesting,
art: &mut HashMap<usize, Vec<Arc<Node>>>,
color: LayerColor,
) -> Option<Node> {
let ocg = ocgs.list.get(g)?;
let mut l = Node::layer(b.id(), &ocg.name, color);
(l.visible, l.locked) = (ocg.on, ocg.locked);
let mut children = vec![];
for &c in nesting.get(&g).into_iter().flatten() {
if c == g {
children.extend(art.remove(&g).unwrap_or_default());
} else if let Some(sub) = group_layer(b, c, ocgs, nesting, art, color) {
children.push(Arc::new(sub));
}
}
if let NodeKind::Layer { children: c, printable, .. } = &mut l.kind {
*c = children;
*printable = ocg.print;
}
Some(l)
}
/// The note on a file carrying an editor's private data (#472): its PDF part, the one read, holds
/// only the art on its artboards (art on the pasteboard is in the private data alone).
pub const OFF_ARTBOARD_NOTE: &str =
"only the PDF-compatible part of this file was read: art outside its artboards is kept in the editor's private data alone, so it doesn't open";
/// Warning: art reaching past the page box was clipped to it.
pub const PAST_PAGE_NOTE: &str = "art reaching past the page was clipped to it, as PDF viewers show it";
/// Warning: glyphs were kept as outlines rather than read as type.
pub const OUTLINED_TEXT_NOTE: &str = "text was converted to outlines";
/// Warning: text drawn invisibly (text rendering mode 3, as a scanned page's OCR layer is) that
/// can't come in as type (text read as outlines, or a Type 3 font) was left out: as outlines it
/// would draw nothing (#2675).
pub const INVISIBLE_TEXT_NOTE: &str = "invisible text (such as a scanned page's searchable OCR layer) that couldn't be read as type was left out";
/// Warning: glyphs that differ from the installed font of their name were kept as outlines.
pub const UNMATCHED_GLYPHS_NOTE: &str = "text whose glyphs differ from the installed font of the same name was kept as outlines";
/// The end of the warning that glyphs whose characters the file doesn't name were kept as
/// outlines, after "text in" and the font's name.
pub const UNNAMED_CHARACTERS_NOTE: &str = "whose characters the file doesn't name (U+FFFD) was kept as outlines";
/// Is `note` one of the warnings that glyphs were kept as outlines ([`OUTLINED_TEXT_NOTE`],
/// [`UNMATCHED_GLYPHS_NOTE`], [`UNNAMED_CHARACTERS_NOTE`])?
pub fn is_outlines_note(note: &str) -> bool {
note == OUTLINED_TEXT_NOTE || note == UNMATCHED_GLYPHS_NOTE || note.starts_with("text in ") && note.ends_with(UNNAMED_CHARACTERS_NOTE)
}
/// Is `n` glyphs kept as outlines (a path the import names `<Text Outlines>`)?
pub fn is_glyph_outlines(n: &Node) -> bool {
matches!(n.kind, NodeKind::Path { .. }) && n.name.as_deref() == Some(TEXT_OUTLINES)
}
/// Does `doc` hold glyphs kept as outlines ([`is_glyph_outlines`])?
pub fn holds_outlines(doc: &Document) -> bool {
let mut any = false;
doc.walk(|n| any |= is_glyph_outlines(n));
any
}
/// 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> {
read_with_reader_options(bytes, opts, &crate::pages::open_options())
}
#[cfg(test)]
pub(crate) fn import_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_pdf::OpenOptions) -> Result<ImportReport, PdfError> {
vectorcraft_doc::depth::with_deep_stack(|| read_with_reader_options(bytes, opts, reader))
}
fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_pdf::OpenOptions) -> Result<ImportReport, PdfError> {
let original = crate::pages::open_with_options(bytes, opts.password.as_deref(), reader)?;
let picked = crate::pages::picked(opts, original.pages().len())?;
let mut ocgs = Ocgs::read(&original);
let route = opts.layers && !ocgs.list.is_empty();
let (pdf, all_on, notes) = reopen_with_groups_on(bytes, opts, reader, original, &picked, &mut ocgs, route)?;
let pages = pdf.pages();
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
let incomplete = Arc::new(AtomicBool::new(false));
let settings = interpreter_settings(sink.clone(), incomplete.clone());
let mut doc = Document::new(1.0, 1.0);
doc.title = "Imported PDF".into();
doc.artboards.clear();
doc.layers.clear();
let taken: Vec<String> = doc.swatches_iter().map(|s| s.name.clone()).chain(doc.swatch_groups.iter().map(|g| g.name.clone())).collect();
let mut b = Builder::new(doc.peek_next_id(), Colors::new(&pdf, taken.clone()), opts.text_as, route);
// The art of a group that is off (drawn as `all_on` turned it on) or doesn't print mustn't
// show or print on its page's layer.
b.unsure = route && ocgs.list.iter().any(|g| (all_on && !g.on) || !g.print);
b.taken = taken;
b.warnings = notes;
b.colors.uncalibrated_rgb = opts.uncalibrated_rgb && picked.iter().filter_map(|&n| pages.get(n)).any(crate::pages::has_private_data);
let cache = InterpreterCache::new();
let mut x = 0.0;
// Where the next page's art may begin (a `.ai` page's art reaches past its artboard).
let mut clear = f64::NEG_INFINITY;
// Every page only text over private data: a file saved without its PDF part shows a notice
// there, not its art. A page that states where its art is isn't that notice.
let (mut only_text, mut art_boxes) = (true, false);
let mut slots: Vec<Slot> = vec![];
let mut group_art: HashMap<usize, Vec<Arc<Node>>> = HashMap::new();
let mut nesting = Nesting::new();
// The top-level groups in the order the pages mark them, bottom first.
let (mut order, mut known) = (vec![], HashSet::new());
for (i, &number) in picked.iter().enumerate() {
let Some(page) = pages.get(number) else { continue };
// Artboards on the trim boxes have the bleed boxes round them as the document's bleed
// (the widest of the pages' on each side), so the art there shows and prints as bleed.
if opts.crop == CropTo::Trim {
for (side, b) in doc.setup.bleed.iter_mut().zip(crate::pages::bleed(page)) {
*side = side.max(b);
}
}
// The chosen box sits at (x, 0); the page draws round it.
let (init, frame) = crate::pages::frame(page, opts.crop);
let mut ab = Rect::new(x, 0.0, x + frame.width(), frame.height());
let xf = Affine::translate((x - frame.x0, -frame.y0)) * init;
let mut ctx = Context::new(xf, ab, &cache, pdf.xref(), settings.clone());
b.page = ab;
// The first id of this page's art.
let first_id = b.next;
let mut scan = scan_page(page, &mut ocgs, all_on, &mut b.fonts, pdf.decode_limits().max_decoded_stream())?;
// The walk's ink names pick between spaces of inks that look the same to the interpreter. A
// page keeps them only when it uses such an ink and the interpreter draws as many paths
// there as the walk counts.
let tied = scan.inks.iter().any(|n| b.colors.tied(n));
if !(tied && drawn_paths(page, Context::new(xf, ab, &cache, pdf.xref(), settings.clone())) == Some(scan.paints.len())) {
scan.inks.clear();
}
if route {
let mut seen = HashSet::new();
let marked: Vec<usize> = scan.tags.iter().filter_map(|(_, g)| g.map(|g| top_group(g, &ocgs))).filter(|g| seen.insert(*g)).collect();
merge_order(&mut order, &mut known, &marked);
}
b.begin_page(scan);
craft_pdf::interpret_page(page, &mut ctx, &mut b).map_err(|e| PdfError::Parse(e.to_string()))?;
if incomplete.load(Ordering::Relaxed) {
return Err(crate::import_scan::incomplete_content());
}
let mut parts = b.end_page();
// A PDF-compatible `.ai` (the editor's private data next to the PDF art).
let ai = crate::pages::has_private_data(page);
if ai {
drop_page_fill(&mut parts, xf.transform_rect_bbox(crate::pages::page_box(page, CropTo::Crop)));
b.warn(OFF_ARTBOARD_NOTE);
}
// A file with several artboards writes, on each page, the art of its neighbours that
// reaches into the page's box: art lying wholly outside this page is theirs (each page
// draws it shifted by its own artboard spacing), so keep it only where it belongs.
if picked.len() > 1 {
for (_, art) in &mut parts {
art.retain(|n| n.visual_bounds().is_none_or(|r| r.x0 <= ab.x1 && r.x1 >= ab.x0 && r.y0 <= ab.y1 && r.y1 >= ab.y0));
}
parts.retain(|(_, art)| !art.is_empty());
}
// Viewers show a page through its crop box, so art reaching past it (a mock-up backdrop
// around the page, say) never showed. A clip to the page box that the file states is
// dropped as redundant (by hayro and by `push_clip_path`), so restore the box here:
// each object reaching past it keeps its own clip, which Fit to Artwork Bounds and
// `CropTo::Bounding` then respect.
// The crop box is what viewers show, whichever box the artboard takes. Not in a
// PDF-compatible `.ai`: its PDF part stands in for the editable document, whose art
// crossing the artboard edge isn't clipped, so it opens whole (#646).
let shown = xf.transform_rect_bbox(crate::pages::page_box(page, CropTo::Crop));
let page_box = shown.inflate(0.01, 0.01);
if !ai && clip_past_page(&mut parts, &mut b, shown, page_box) {
b.warn(PAST_PAGE_NOTE);
}
// A `.ai` page keeps its art past the artboard edge, unclipped (#646), so a page whose art
// reaches far to the left would lay it over the page before it in the row: there it would
// be taken for that page's art (by the check against the editing data, which then finds
// art its layers don't have there, and by `place_pages`). Such a page moves right until its
// art clears the art before it.
let mut art_right = f64::NEG_INFINITY;
if ai && let Some(art) = parts.iter().flat_map(|(_, v)| v.iter()).filter_map(|n| n.visual_bounds()).reduce(|a, b| a.union(b)) {
let dx = shift_ai_page(&mut parts, &mut b, first_id, clear, art);
ab = Rect::new(ab.x0 + dx, ab.y0, ab.x1 + dx, ab.y1);
art_right = art.x1 + dx;
}
let children: Vec<Arc<Node>> = parts.iter().flat_map(|(_, v)| v.iter().cloned()).collect();
only_text &= ai && is_only_text(&children);
art_boxes |= crate::pages::states_art(page);
let mut right = ab.x1;
if opts.crop == CropTo::Bounding
&& let Some(art) = vectorcraft_doc::live::nodes_bounds(&children)
{
right = right.max(art.x1);
ab = art;
}
x = right + opts.artboard_gap;
clear = right.max(art_right) + opts.artboard_gap;
doc.artboards.push(Artboard {
id: i as u32 + 1,
name: format!("Artboard {}", i + 1),
rect: ab,
locked: false,
show_center_mark: false,
show_cross_hairs: false,
show_video_safe_areas: false,
video_ruler_pixel_aspect_ratio: 1.0,
color: None,
});
let page_layer = |b: &mut Builder<'_>, art: Vec<Arc<Node>>, shown: bool| {
let name = if shown { format!("Page {}", number + 1) } else { format!("Page {} (unsorted)", number + 1) };
let mut layer = Node::layer(b.id(), &name, LayerColor::Preset((i % 27) as u8));
layer.visible = shown;
if let NodeKind::Layer { children: c, printable, .. } = &mut layer.kind {
(*c, *printable) = (art, shown);
}
Slot::Page(Box::new(layer))
};
if parts.is_empty() {
slots.push(page_layer(&mut b, vec![], true));
}
for (key, art) in parts {
match key {
None => slots.push(page_layer(&mut b, art, true)),
Some(UNSORTED) => slots.push(page_layer(&mut b, art, false)),
Some(g) => {
let into = group_art.entry(g).or_insert_with(|| {
place_group(g, &ocgs, &mut slots, &mut nesting);
vec![]
});
into.extend(art);
}
}
}
}
if only_text && !art_boxes {
return Err(PdfError::PlaceholderOnly);
}
// A group the pages mark but draw nothing in is an empty layer, as the editor had it.
for &g in &order {
if !nesting.contains_key(&g) {
slots.push(Slot::Group(g));
}
}
// The groups' layers stack as the pages mark them (the art outside them stays where it was
// drawn): a group that first paints on a later page doesn't go on top of the others.
stack_groups(&mut slots, &order);
for (n, slot) in slots.into_iter().enumerate() {
let mut layer = match slot {
Slot::Page(l) => *l,
// Sublayers take their top-level layer's colour.
Slot::Group(g) => match group_layer(&mut b, g, &ocgs, &nesting, &mut group_art, LayerColor::Preset((n % 27) as u8)) {
Some(l) => l,
None => continue,
},
};
if let Some(art) = layer.children_mut() {
crate::import_lines::rebuild(art, &b.lines);
}
doc.layers.push(Arc::new(layer));
}
for (k, blob) in b.images.drain() {
doc.images.insert(k, blob);
}
// A file painted mostly in CMYK opens as a CMYK document (with CMYK default swatches).
if b.colors.cmyk_document() {
doc.color_mode = ColorMode::Cmyk;
let (swatches, groups) = vectorcraft_color::default_swatches(ColorMode::Cmyk.model());
(doc.swatches, doc.swatch_groups) = (swatches.into(), groups.into());
}
doc.swatches.extend(b.colors.swatches());
// Tiling patterns are pattern swatches.
for p in b.patterns.drain(..) {
doc.swatches.push(Swatch {
name: p.name.clone(),
paint: Paint::Pattern { pattern: p.name.clone(), xf: Affine::IDENTITY },
global: false,
spot: false,
});
doc.patterns.push(p);
}
if b.colors.mixed {
b.warn("colours mixing several inks (DeviceN) were imported as RGB");
}
if !b.missing_fonts.is_empty() {
let list = b.missing_fonts.join(", ");
b.warn(&format!("fonts that aren't available show in the fallback font until they are: {list}"));
}
// Before anything walks the tree by recursion (`fix_next_id` does): nested clips can be deeper
// than the stack goes.
let mut depth_notes = vec![];
doc.limit_depth_noting(&mut depth_notes);
doc.fix_next_id();
doc.reserve_ids(b.next);
let mut warnings = b.warnings;
if let Ok(v) = sink.lock() {
for w in v.iter() {
if !warnings.contains(w) {
warnings.push(w.clone());
}
}
}
warnings.extend(depth_notes);
let user_unit = picked.first().and_then(|&n| pages.get(n)).map_or(1.0, |page| f64::from(page.user_unit()));
Ok(ImportReport { document: doc, warnings, native: crate::editing::editing_in(&pdf), only_text, user_unit })
}
/// The page a PDF-compatible `.ai` paints under its layers is the editor's page, not art: an opaque
/// white rectangle the size of the page (`page`, document space), outside every layer and before
/// any of them. Drop it from the page's art (`parts`, as [`Builder::end_page`] gives them).
fn drop_page_fill(parts: &mut Vec<(Option<usize>, Vec<Arc<Node>>)>, page: Rect) {
let layered = parts.iter().any(|(g, _)| g.is_some());
let Some((None, art)) = parts.first_mut() else { return };
// Not a spot colour: a white ink is art.
let fill = art.first().is_some_and(|n| {
layered
&& n.blend == BlendMode::Normal
&& white_cover(n, page)
&& n.appearance.fill().is_some_and(|f| matches!(f.paint, Paint::Solid { swatch: None, .. }))
&& n.geometric_bounds().is_some_and(|b| contains(page.inflate(0.5, 0.5), b))
});
if fill {
art.remove(0);
if art.is_empty() {
parts.remove(0);
}
}
}
/// Is this art text and nothing else (in groups and clips), with some text?
fn is_only_text(nodes: &[Arc<Node>]) -> bool {
fn walk(nodes: &[Arc<Node>], text: &mut bool) -> bool {
nodes.iter().all(|n| match &n.kind {
NodeKind::Path { clipping: true, .. } => true,
NodeKind::Path { .. } if n.name.as_deref() == Some(TEXT_OUTLINES) => {
*text = true;
true
}
NodeKind::Text(_) => {
*text = true;
true
}
NodeKind::Group { children, .. } => walk(children, text),
_ => false,
})
}
let mut text = false;
walk(nodes, &mut text) && text
}
/// Opens the file again with every layer group turned on when the pages hide art in groups that are
/// off, and tells whether it did. Gives the file to read, whether it is the turned-on copy, and the
/// notes for the import's report.
fn reopen_with_groups_on(
bytes: &[u8],
opts: &ImportOptions,
reader: &craft_pdf::OpenOptions,
original: craft_pdf::syntax::Pdf,
picked: &[usize],
ocgs: &mut Ocgs,
route: bool,
) -> Result<(craft_pdf::syntax::Pdf, bool, Vec<String>), PdfError> {
// Content that is off imports as hidden layers: an update of the file turns every group on
// (unless a form is hidden by its own group: its art couldn't be told apart).
let hide = route && ocgs.skips_any();
let mut hidden_form = false;
if hide {
for page in picked.iter().filter_map(|&n| original.pages().get(n)) {
hidden_form |= hides_forms(page, ocgs, original.decode_limits().max_decoded_stream())?;
if hidden_form {
break;
}
}
}
let turned_on = (hide && !hidden_form)
.then(|| all_on(bytes, &original))
.flatten()
.and_then(|b| crate::pages::open_with_options(&b, opts.password.as_deref(), reader).ok())
.filter(|p| Ocgs::read(p).list.is_empty());
let mut notes = vec![];
if hide && turned_on.is_none() {
notes.push("the art of hidden layers couldn't be read and was left out".to_string());
}
let all_on = turned_on.is_some();
Ok((turned_on.unwrap_or(original), all_on, notes))
}
/// The interpreter's settings, its warnings noted once each in `sink` (truncated content in `incomplete`).
fn interpreter_settings(sink: Arc<Mutex<Vec<String>>>, incomplete: Arc<AtomicBool>) -> InterpreterSettings {
InterpreterSettings {
warning_sink: Arc::new(move |w| {
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",
InterpreterWarning::ContentTruncated => {
incomplete.store(true, Ordering::Relaxed);
return;
}
};
if let Ok(mut v) = sink.lock()
&& !v.iter().any(|x| x == msg)
{
v.push(msg.to_string());
}
}),
render_annotations: false,
..Default::default()
}
}
/// Moves a `.ai` page's art (`art`, its bounds) right until it clears the art before it (at
/// `clear`), and its lines of type with it. Gives how far.
fn shift_ai_page(parts: &mut [(Option<usize>, Vec<Arc<Node>>)], b: &mut Builder<'_>, first_id: u64, clear: f64, art: Rect) -> f64 {
// Art at a coordinate that isn't a number (a damaged file) moves nothing.
let dx = Some((clear - art.x0).max(0.0)).filter(|dx| dx.is_finite()).unwrap_or(0.0);
if dx > 0.0 {
let shift = Affine::translate((dx, 0.0));
for (_, v) in parts.iter_mut() {
for n in v.iter_mut() {
Arc::make_mut(n).transform(shift, false);
}
}
// What the page's lines of type were is told where their glyphs are (the outlines
// stroked on them are looked for there), so that moves with them.
for facts in b.lines.iter_mut().filter(|(id, _)| id.0 >= first_id).map(|(_, facts)| facts) {
for ink in &mut facts.ink {
*ink = *ink + kurbo::Vec2::new(dx, 0.0);
}
}
}
dx
}
/// Clips each object reaching past `page_box` to `shown`. Tells whether any did.
fn clip_past_page(parts: &mut [(Option<usize>, Vec<Arc<Node>>)], b: &mut Builder<'_>, shown: Rect, page_box: Rect) -> bool {
let mut clipped = false;
for (_, art) in parts.iter_mut() {
for n in art.iter_mut() {
if n.visual_bounds().is_some_and(|r| !contains(page_box, r)) {
let clip = Arc::new(b.clip_node(&shown.to_path(0.1), FillRule::NonZero));
*n = Arc::new(Node::new(b.id(), NodeKind::Group { children: vec![clip, n.clone()], clip: true }));
clipped = true;
}
}
}
clipped
}
/// The groups' layers stack as the pages mark them (`order`, bottom first).
fn stack_groups(slots: &mut [Slot], order: &[usize]) {
let rank: HashMap<usize, usize> = order.iter().enumerate().map(|(i, &g)| (g, i)).collect();
let at: Vec<usize> = slots.iter().enumerate().filter(|(_, s)| matches!(s, Slot::Group(_))).map(|(i, _)| i).collect();
let mut stacked: Vec<usize> = slots.iter().filter_map(|s| if let Slot::Group(g) = s { Some(*g) } else { None }).collect();
stacked.sort_by_key(|g| rank.get(g).copied().unwrap_or(usize::MAX));
for (i, g) in at.into_iter().zip(stacked) {
if let Some(s) = slots.get_mut(i) {
*s = Slot::Group(g);
}
}
}