diff --git a/crates/pdf/src/export/entry.rs b/crates/pdf/src/export/entry.rs index 46fb58d58..6a4294c69 100644 --- a/crates/pdf/src/export/entry.rs +++ b/crates/pdf/src/export/entry.rs @@ -1,5 +1,5 @@ use super::writer::{Sheet, Writer, date_time}; -use super::*; +use super::{Affine, Document, ExportReport, Exporter, HashSet, PageBoxes, PdfError, PdfOptions, Rect, write}; /// Export `doc` as PDF bytes: one page per artboard (or the artboards chosen in `opts`). pub fn export(doc: &Document, opts: &PdfOptions) -> Result, PdfError> { diff --git a/crates/pdf/src/export/exporter.rs b/crates/pdf/src/export/exporter.rs index 156ad49e4..c7d21793d 100644 --- a/crates/pdf/src/export/exporter.rs +++ b/crates/pdf/src/export/exporter.rs @@ -1,5 +1,8 @@ use super::entry::RgbInCmyk; -use super::*; +use super::{ + Affine, Arc, BezPath, BlendMode, Color, Compatibility, Document, Exporter, Fill, FillRule, HashMap, HashSet, Node, NormalizedF32, Path, + PathBuilder, PathEl, Rect, Shape, Standard, Surface, Transform, cmyk, krilla, luma, rgb, +}; /// How deep inline graphics nest before they are left out. pub(super) const MAX_INLINE_DEPTH: u32 = 4; diff --git a/crates/pdf/src/export/images.rs b/crates/pdf/src/export/images.rs index 946cb686e..fbbf6c71c 100644 --- a/crates/pdf/src/export/images.rs +++ b/crates/pdf/src/export/images.rs @@ -1,5 +1,5 @@ use super::exporter::{UNDECODED, writable, xf}; -use super::*; +use super::{BlendMode, Exporter, Image, NodeKind, Rect, Size, Surface}; impl Exporter<'_> { /// Image `im` placed `size` points wide and high, resampled and compressed as the Compression diff --git a/crates/pdf/src/export/nodes.rs b/crates/pdf/src/export/nodes.rs index 80b1dab7c..b4e64c58f 100644 --- a/crates/pdf/src/export/nodes.rs +++ b/crates/pdf/src/export/nodes.rs @@ -1,5 +1,8 @@ use super::exporter::{address, blend, constant_mask, cover, in_range, norm, rects_overlap, rule, to_path, writable}; -use super::*; +use super::{ + Affine, AppearanceItem, BezPath, BlendMode, Exporter, Fill, FillRule, LineCap, LineJoin, Mark, Node, NodeKind, NormalizedF32, Paint, Path, Rect, + Shape, Stroke, StrokeAlign, StrokeDash, StrokeLayer, Surface, WrittenShape, krilla, stroke, +}; impl Exporter<'_> { /// Opacity mask → luminosity soft mask. Outside the art the backdrop is black (clip) or white; diff --git a/crates/pdf/src/export/paint.rs b/crates/pdf/src/export/paint.rs index 2c0316086..348f2d803 100644 --- a/crates/pdf/src/export/paint.rs +++ b/crates/pdf/src/export/paint.rs @@ -1,5 +1,8 @@ use super::exporter::{MAX_FIELD_PX, MAX_GRADIENT_PX, MAX_PATTERN_DEPTH, color, ctm, in_range, norm, q, to_path, writable, xf}; -use super::*; +use super::{ + Affine, Color, Exporter, GradientKind, LinearGradient, Paint, RadialGradient, Rect, SepColor, SeparationColorant, SeparationSpace, Shape, Size, + SpreadMethod, Stop, Surface, Vec2, cmyk, krilla, rgb, +}; impl Exporter<'_> { /// A colour for the page, as the Output settings write it ([`ColorOut::color`]): without diff --git a/crates/pdf/src/export/text.rs b/crates/pdf/src/export/text.rs index c8214127a..d7670a389 100644 --- a/crates/pdf/src/export/text.rs +++ b/crates/pdf/src/export/text.rs @@ -1,6 +1,9 @@ use super::exporter::{GlyphLine, MAX_INLINE_DEPTH, in_range, norm, to_path, writable, xf}; use super::images::embed; -use super::*; +use super::{ + Affine, AppearanceItem, BezPath, BlendMode, Color, Cow, Embedding, Exporter, Fill, FillRule, Glyph, GlyphId, KrillaGlyph, ListMarker, Mark, Node, + NormalizedF32, Paint, Path, Rect, Shape, Size, Surface, TextLayout, TextObject, krilla, write, +}; impl Exporter<'_> { /// diff --git a/crates/pdf/src/export/writer.rs b/crates/pdf/src/export/writer.rs index 372e1f927..b2ca788eb 100644 --- a/crates/pdf/src/export/writer.rs +++ b/crates/pdf/src/export/writer.rs @@ -1,6 +1,9 @@ use super::entry::RgbInCmyk; use super::exporter::{constant_mask, cover, to_path, writable, xf}; -use super::*; +use super::{ + Affine, Arc, Archival, ColorOut, Compatibility, Document, Exporter, HashSet, Image, Metadata, Node, PageSettings, PdfError, PdfVersion, Rect, + Shape, Size, Standard, find, krilla, rfind, write, +}; /// A PDF being written, shared by export and print: the file's configuration and metadata, then /// its pages ([`Self::page`]), each drawn by an [`Exporter`]. diff --git a/crates/pdf/src/import/builder.rs b/crates/pdf/src/import/builder.rs index 4c9428dd9..28e855ca1 100644 --- a/crates/pdf/src/import/builder.rs +++ b/crates/pdf/src/import/builder.rs @@ -1,7 +1,12 @@ use super::cid::{CidText, FontInfo, comparable, font_chars, identify_glyph, matching_face, replacement, unify_radical}; use super::entry::{MAX_NESTED, OVERPRINT_UNREAD, OVERPRINT_ZERO_MODE, PATTERN_REUSE, TEXT_OUTLINES, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE}; use super::state::{Builder, Frame, FrameKind, Saved, bounds, grouped, line_width, mean_scale, paints, round3, stroke_layer}; -use super::*; +use super::{ + Affine, Appearance, AppearanceItem, Arc, BezPath, BfString, BlendMode, CacheKey, Color, Colors, Families, FillLayer, FillRule, FunctionBudget, + FunctionPaint, HashMap, HashSet, ImageBlob, ImageObject, Knockout, Look, MaskSpec, MaskType, Native, Node, NodeId, NodeKind, OpacityMask, Paint, + PathData, PathDrawMode, Pattern, PatternDef, Placement, Rect, Scan, Shape, SoftMask, StrokeProps, TextAs, TextLine, TextOverprint, TilingPattern, + Upright, Walked, clipped, contains, fold_stop_opacity, function_paint, gradient_shape, is_rectangle, luminance, mask_spec, shading_gradient, +}; impl<'p> Builder<'p> { pub(super) fn new(next: u64, colors: Colors<'p>, text_as: TextAs, route: bool) -> Self { diff --git a/crates/pdf/src/import/cid.rs b/crates/pdf/src/import/cid.rs index ee6684d22..60a28d15e 100644 --- a/crates/pdf/src/import/cid.rs +++ b/crates/pdf/src/import/cid.rs @@ -1,4 +1,6 @@ -use super::*; +use super::{ + Affine, Arc, BezPath, ClipPath, Context, Device, Glyph, GlyphDrawMode, HashMap, Image, Page, PathDrawMode, PathEl, Rect, Shape, SoftMask, +}; /// How many paths the interpreter draws on `page` (each fill and each stroke), leaving out those /// of soft masks, pattern cells and type 3 glyphs, which [`Builder`] reads nested. `None` when the diff --git a/crates/pdf/src/import/device.rs b/crates/pdf/src/import/device.rs index d9721f01f..60b00fb2e 100644 --- a/crates/pdf/src/import/device.rs +++ b/crates/pdf/src/import/device.rs @@ -2,7 +2,10 @@ use super::entry::{INVISIBLE_TEXT_NOTE, MAX_NESTED, OUTLINED_TEXT_NOTE, UNSORTED use super::state::{ Builder, Decoded, Frame, FrameKind, GlyphRun, blend, fill_rule, line_width, mean_scale, resize_alpha, rgba_png, same_outline, stroke_layer, }; -use super::*; +use super::{ + Affine, Appearance, AppearanceItem, Arc, BezPath, BlendMode, ClipPath, Device, FillLayer, FillRule, Glyph, GlyphDrawMode, Image, ImageBlob, + ImageData, Name, Node, NodeKind, PathData, PathDrawMode, Pattern, Shape, SoftMask, clipped, extend_clip, mesh_shading, tag_key, +}; impl<'a> Device<'a> for Builder<'_> { fn set_soft_mask(&mut self, mask: Option>) { diff --git a/crates/pdf/src/import/entry.rs b/crates/pdf/src/import/entry.rs index d52cd576e..d128b2956 100644 --- a/crates/pdf/src/import/entry.rs +++ b/crates/pdf/src/import/entry.rs @@ -1,6 +1,10 @@ use super::cid::drawn_paths; use super::state::Builder; -use super::*; +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; @@ -182,54 +186,12 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p let picked = crate::pages::picked(opts, original.pages().len())?; 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 - // (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, &mut 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(); - let pdf = turned_on.unwrap_or(original); + let (pdf, all_on, notes) = reopen_with_groups_on(bytes, opts, reader, original, &picked, &mut ocgs, route)?; let pages = pdf.pages(); let sink: Arc>> = Arc::default(); - let sink2 = sink.clone(); let incomplete = Arc::new(AtomicBool::new(false)); - let incomplete_sink = incomplete.clone(); - let settings = 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_sink.store(true, Ordering::Relaxed); - return; - } - }; - if let Ok(mut v) = sink2.lock() - && !v.iter().any(|x| x == msg) - { - v.push(msg.to_string()); - } - }), - render_annotations: false, - ..Default::default() - }; + let settings = interpreter_settings(sink.clone(), incomplete.clone()); let mut doc = Document::new(1.0, 1.0); doc.title = "Imported PDF".into(); @@ -316,17 +278,7 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p // 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); - let mut clipped = false; - for (_, art) in parts.iter_mut().filter(|_| !ai) { - 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; - } - } - } - if clipped { + 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 @@ -336,24 +288,8 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p // 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)) { - // 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 &mut parts { - 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); - } - } - ab = Rect::new(ab.x0 + dx, ab.y0, ab.x1 + dx, ab.y1); - } + 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> = parts.iter().flat_map(|(_, v)| v.iter().cloned()).collect(); @@ -416,15 +352,7 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p } // 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. - let rank: HashMap = order.iter().enumerate().map(|(i, &g)| (g, i)).collect(); - let at: Vec = slots.iter().enumerate().filter(|(_, s)| matches!(s, Slot::Group(_))).map(|(i, _)| i).collect(); - let mut stacked: Vec = 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); - } - } + stack_groups(&mut slots, &order); for (n, slot) in slots.into_iter().enumerate() { let mut layer = match slot { Slot::Page(l) => *l, @@ -528,3 +456,114 @@ fn is_only_text(nodes: &[Arc]) -> bool { 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), 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>>, incomplete: Arc) -> 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, Vec>)], 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, Vec>)], 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 = order.iter().enumerate().map(|(i, &g)| (g, i)).collect(); + let at: Vec = slots.iter().enumerate().filter(|(_, s)| matches!(s, Slot::Group(_))).map(|(i, _)| i).collect(); + let mut stacked: Vec = 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); + } + } +} diff --git a/crates/pdf/src/import/state.rs b/crates/pdf/src/import/state.rs index cbe9f7a02..13803c98d 100644 --- a/crates/pdf/src/import/state.rs +++ b/crates/pdf/src/import/state.rs @@ -1,5 +1,8 @@ use super::cid::{CidText, FontInfo}; -use super::*; +use super::{ + Affine, Arc, BezPath, BlendMode, Colors, Dash, Families, FillRule, FontNames, FunctionBudget, HashMap, HashSet, ImageBlob, LineCap, LineFacts, + LineJoin, LumaData, MaskSpec, Node, NodeId, Paint, PathEl, PatternDef, Rect, Scan, StrokeLayer, StrokeProps, TextAs, TextLine, +}; /// Decoded RGBA pixels, width, height and hayro's scale factors. pub(super) type Decoded = (Vec, u32, u32, (f32, f32));