use super::cid::{CidText, FontInfo}; 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)); pub(super) enum FrameKind { Root, /// A clip group whose clip path node is stored here. Clip(Box), /// A clip that was redundant (covers the page); children go straight to the parent. Skip, Group { opacity: f32, blend: BlendMode, mask: Option>, isolate: bool, knockout: bool, }, } pub(super) struct Frame { pub(super) kind: FrameKind, pub(super) children: Vec>, /// The first optional content group entered inside (where the frame's art goes). pub(super) group: Option, /// The group each child was drawn in, where the frame's art goes when the frame dissolves /// into its parent (a layer's form drawing its sublayers' forms). pub(super) child_groups: Vec>, } impl Frame { pub(super) fn new(kind: FrameKind) -> Self { Self { kind, children: vec![], group: None, child_groups: vec![] } } } /// A dissolving frame's `children`, each with the group it was drawn in (`groups`, else the /// frame's `group`). pub(super) fn grouped( children: Vec>, groups: Vec>, group: Option, ) -> impl Iterator, Option)> { children.into_iter().zip(groups.into_iter().map(move |g| g.or(group)).chain(std::iter::repeat(group))) } /// Glyphs drawn consecutively with the same paint, merged into one path. pub(super) struct GlyphRun { pub(super) path: BezPath, pub(super) paint: Paint, pub(super) opacity: f32, pub(super) stroke: Option, pub(super) scale: f64, pub(super) overprint: bool, } /// How often a tiling pattern was read, and the art (as its paints) each reading drew with the /// swatch made for it. pub(super) type PatternReadings = (u32, Vec<(Vec, String)>); /// Builder state a nested interpretation (soft mask, pattern cell) starts afresh and gives back. pub(super) struct Saved { pub(super) stack: Vec, pub(super) root_groups: Vec>, pub(super) blend: BlendMode, pub(super) mask: Option>, pub(super) page: Rect, } pub(super) struct Builder<'p> { pub(super) colors: Colors<'p>, pub(super) next: u64, pub(super) stack: Vec, /// The optional content group of each child of the root frame. pub(super) root_groups: Vec>, pub(super) blend: BlendMode, pub(super) page: Rect, pub(super) glyphs: Option, pub(super) text: Option, pub(super) text_as: TextAs, pub(super) images: HashMap, pub(super) image_keys: HashMap, /// The images kept with their CMYK samples ([`crate::import_image`]). pub(super) cmyk_keys: HashSet, /// Mask keys of CMYK images whose mask turned out to hide nothing. pub(super) opaque: HashSet, pub(super) warnings: Vec, /// Fonts by cache key → base font name and whether the file carries its glyphs (from /// [`scan_page`]). pub(super) fonts: FontNames, pub(super) font_names: HashMap, pub(super) families: Option, pub(super) missing_fonts: Vec, /// Font (cache key) → its characters by glyph, for CID-keyed fonts embedded without a /// ToUnicode map. pub(super) cid_text: HashMap>>, /// Font (cache key) → the character of each glyph its embedded font program maps /// ([`font_chars`]), for glyphs the file's ToUnicode map doesn't name (#708). pub(super) font_chars: HashMap>>>, /// Font (cache key) → the installed face that draws its glyphs, decided from its first glyph /// that can be compared ([`matching_face`]); `None`: its glyphs differ, so it stays outlines. pub(super) matched: HashMap>>, /// (Font, glyph) → the character its outline matches in the installed face, for glyphs no /// mapping names ([`Builder::identified_char`]). pub(super) identified: HashMap<(u128, u32), Option>, /// Per font: the installed version its type names, when it isn't the one its family and style /// resolve to (see `CharStyle::font_version`). pub(super) versions: HashMap>, /// The soft mask of the graphics state, the art drawn through it so far, and the masks read. pub(super) mask: Option>, pub(super) masked: Vec>, pub(super) masks: HashMap>, /// Pattern swatches made, and per (pattern, stroke) the art signatures read and their names. pub(super) patterns: Vec, pub(super) pattern_keys: HashMap<(u128, bool), PatternReadings>, /// Per function-based shading (and the inks it paints with): what it comes in as, read once. pub(super) function_paints: HashMap<(u128, Option>), Paint>, /// What is left of the import's function evaluations for those shadings. pub(super) function_budget: FunctionBudget, /// What is left of the import's objects ([`super::entry::max_objects`]). pub(super) objects_left: usize, /// Swatch names in use. pub(super) taken: Vec, /// The page as walked by [`scan_page`], and how far the interpreter is through it. pub(super) scan: Scan, pub(super) tag_at: usize, /// The fills and strokes of paths drawn so far ([`Scan::paints`]). pub(super) paint_at: usize, pub(super) aligned: bool, /// Whether optional content groups become layers. pub(super) route: bool, /// Some groups are hidden or don't print: art in marked content whose group can't be told /// (once the scan and the interpreter part) is [`UNSORTED`] rather than the page's. pub(super) unsure: bool, /// The marked-content sequences open: the group each marks. pub(super) marked: Vec>, pub(super) nested: u32, /// Reading an alpha soft mask's art: images are read white with their own alpha, so the /// mask's luminance is their alpha, as whitened vector art's is (cairo, and so Inkscape, /// rasterizes masks into such images). pub(super) white_images: bool, /// The last filled path: its node and its outline (a stroke of the same outline joins it). pub(super) last_fill: Option<(NodeId, BezPath)>, /// What each line of type made tells beyond its text object ([`crate::import_lines`]). pub(super) lines: HashMap, } pub(super) fn blend(b: craft_pdf::interpret::BlendMode) -> BlendMode { use craft_pdf::interpret::BlendMode as H; match b { H::Normal => BlendMode::Normal, H::Multiply => BlendMode::Multiply, H::Screen => BlendMode::Screen, H::Overlay => BlendMode::Overlay, H::Darken => BlendMode::Darken, H::Lighten => BlendMode::Lighten, H::ColorDodge => BlendMode::ColorDodge, H::ColorBurn => BlendMode::ColorBurn, H::HardLight => BlendMode::HardLight, H::SoftLight => BlendMode::SoftLight, H::Difference => BlendMode::Difference, H::Exclusion => BlendMode::Exclusion, H::Hue => BlendMode::Hue, H::Saturation => BlendMode::Saturation, H::Color => BlendMode::Color, H::Luminosity => BlendMode::Luminosity, } } pub(super) fn fill_rule(r: craft_pdf::interpret::FillRule) -> FillRule { match r { craft_pdf::interpret::FillRule::NonZero => FillRule::NonZero, craft_pdf::interpret::FillRule::EvenOdd => FillRule::EvenOdd, } } /// `p` with every subpath closed: how a fill reads it. pub(crate) fn closed_subpaths(p: &BezPath) -> BezPath { let mut out = BezPath::new(); let mut open = false; for el in p.elements() { if open && matches!(el, PathEl::MoveTo(_)) { out.push(PathEl::ClosePath); } out.push(*el); open = !matches!(el, PathEl::ClosePath); } if open { out.push(PathEl::ClosePath); } out } /// Do a fill of `fill` and a stroke of `stroke` paint one object's outline? Applications write the /// fill without closing the path (filling closes it) and the stroke closed. pub(super) fn same_outline(fill: &BezPath, stroke: &BezPath) -> bool { fill.elements() == stroke.elements() || closed_subpaths(fill).elements() == closed_subpaths(stroke).elements() } pub(super) fn mean_scale(a: Affine) -> f64 { a.determinant().abs().sqrt() } /// Width a PDF line width of 0 imports as, points: a print hairline. In PDF `0 w` is the thinnest /// line the device can draw (ISO 32000-1, 8.4.3.2), not no line. const HAIRLINE: f64 = 0.25; /// Document width of the stroke `p` drawn at `scale`: `0 w` becomes a [`HAIRLINE`]. pub(super) fn line_width(p: &StrokeProps, scale: f64) -> f64 { if p.line_width == 0.0 { HAIRLINE } else { p.line_width as f64 * scale } } pub(super) fn stroke_layer(paint: Paint, opacity: f32, p: &StrokeProps, scale: f64) -> StrokeLayer { let mut st = StrokeLayer::new(paint, line_width(p, scale)); st.opacity = opacity; st.cap = match p.line_cap { kurbo::Cap::Butt => LineCap::Butt, kurbo::Cap::Round => LineCap::Round, kurbo::Cap::Square => LineCap::Square, }; st.join = match p.line_join { kurbo::Join::Miter => LineJoin::Miter, kurbo::Join::Round => LineJoin::Round, kurbo::Join::Bevel => LineJoin::Bevel, }; st.miter_limit = p.miter_limit as f64; // (Every stroke keeps its own copy: a dash array has a few entries, and one with thousands is cut.) let dash = Dash { pattern: p.dash_array.iter().take(MAX_DASH_ENTRIES).map(|v| *v as f64 * scale).collect(), offset: p.dash_offset as f64 * scale, align_corners: false, }; // An invalid dash array (a negative value, or all zeros) strokes solid. st.dash = dash.is_dashed().then_some(dash); st } /// The most entries of a dash array kept (an even count keeps it a pattern of dashes and gaps). const MAX_DASH_ENTRIES: usize = 64; /// Most dash periods [`stroke_outline`] cuts a stroke into. const MAX_OUTLINE_DASHES: f64 = 1e5; /// The area stroke `p` paints along `path` (in its own space, before `transform`), in the document: /// where a shading painting the stroke shows. Width, caps, joins and dashes as [`stroke_layer`] /// takes them. `None` when it paints nothing. pub(super) fn stroke_outline(path: &BezPath, transform: Affine, p: &StrokeProps) -> Option { let scale = mean_scale(transform); if !(scale.is_finite() && scale > 1e-9) { return None; } let mut style = kurbo::Stroke::new(line_width(p, scale) / scale).with_join(p.line_join).with_caps(p.line_cap).with_miter_limit(p.miter_limit as f64); let dashes: Vec = p.dash_array.iter().take(MAX_DASH_ENTRIES).map(|v| *v as f64).collect(); let dash = Dash { pattern: dashes.clone(), offset: p.dash_offset as f64, align_corners: false }; // A dash pattern far finer than the path would cut it into countless pieces: it strokes solid. let period: f64 = dashes.iter().sum(); if dash.is_dashed() && period > 0.0 && kurbo::Shape::perimeter(path, 1e-3) / period < MAX_OUTLINE_DASHES { style = style.with_dashes(dash.offset, dashes); } // To a hundredth of a point in the document, within reason at extreme scales. let tolerance = (0.01 / scale).clamp(1e-4, 1.0); let mut out = kurbo::stroke(path.iter(), &style, &kurbo::StrokeOpts::default(), tolerance); out.apply_affine(transform); (!out.elements().is_empty()).then_some(out) } pub(crate) fn round3(v: f32) -> f32 { (v * 1000.0).round() / 1000.0 } /// The paints of `nodes`' leaves, in order: what tells two readings of a pattern cell apart. pub(super) fn paints(nodes: &[Arc]) -> Vec { let mut out = vec![]; for n in nodes { n.walk(&mut |c| out.extend(c.appearance.items.iter().map(|i| i.paint().clone()))); } out } /// Visual bounds of `nodes`; `None` when one has none. pub(super) fn bounds(nodes: &[Arc]) -> Option { nodes.iter().try_fold(None, |acc, n| n.visual_bounds().map(|b| Some(acc.map_or(b, |a: Rect| a.union(b)))))? } pub(super) fn rgba_png(rgba: Vec, w: u32, h: u32) -> Option> { let img = image::RgbaImage::from_raw(w, h, rgba)?; let mut out = Vec::new(); img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?; Some(out) } pub(super) fn resize_alpha(a: &LumaData, w: u32, h: u32) -> Vec { if a.width == w && a.height == h { return a.data.clone(); } match image::GrayImage::from_raw(a.width, a.height, a.data.clone()) { Some(g) => image::imageops::resize(&g, w, h, image::imageops::FilterType::Triangle).into_raw(), None => vec![255; (w * h) as usize], } } #[cfg(test)] mod stroke_outline_tests { use super::*; /// A dash pattern far finer than the path strokes it solid instead of cutting it into /// millions of pieces; one in reason dashes it. #[test] fn a_dash_far_finer_than_the_path_strokes_solid() { let mut path = BezPath::new(); path.move_to((0.0, 0.0)); path.line_to((1000.0, 0.0)); let props = |dash: f32| StrokeProps { line_width: 2.0, dash_array: [dash; 4].into(), ..StrokeProps::default() }; let solid = stroke_outline(&path, Affine::IDENTITY, &props(1e-6)).unwrap(); assert!(solid.elements().len() < 20, "{} elements", solid.elements().len()); let dashed = stroke_outline(&path, Affine::IDENTITY, &props(10.0)).unwrap(); assert!(dashed.elements().len() > 100, "dashed: {} elements", dashed.elements().len()); } }