diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index 6799c56b..abd7b7c9 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -1,6 +1,8 @@ //! The graphics state and the operators that draw: coordinates, paths, colours, painting, //! clipping and shadings. Painted paths become document objects as they are drawn ([`Out`]). +use std::collections::{HashMap, HashSet}; +use std::hash::{DefaultHasher, Hash, Hasher}; use std::rc::Rc; use std::sync::Arc; @@ -195,13 +197,16 @@ pub(crate) struct Out { pub rgb: usize, /// Spot inks used: name and colour at full tint. pub spots: Vec<(String, Color)>, + /// The color at full tint of each spot ink in [`Self::spots`], by name. + spot_colors: HashMap, Color>, /// Rectangles painted with a shading (`shfill`): a clipping group of one is the clip path /// filled with it. - pub shadings: Vec, + pub shadings: HashSet, /// Tiling patterns made into pattern swatches: the pattern's `PaintProc` (a pattern made /// again with `makepattern` shares it) and cell, the colour an uncoloured one painted in, and - /// the swatch's name. - tilings: Vec<(Shared, Rect, Option, String)>, + /// the swatch's name. Each list holds the patterns of one [`tiling_hash`]; a pattern whose + /// cell has a NaN corner is left out. + tilings: HashMap>, /// How deep art drawn apart (a pattern's cell, a Type 3 glyph) is nested. pub apart: u32, } @@ -210,6 +215,29 @@ fn sane(r: Rect) -> bool { [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite() && v.abs() <= LIMIT) } +/// A tiling pattern made into a pattern swatch, as [`Out::tilings`] lists it. +type Tiling = (Shared, Rect, Option, String); + +/// A hash of what tells tiling patterns apart in [`Out::tilings`]. Patterns that compare equal +/// hash alike: each number goes in by its bits, and −0 goes in as 0. +fn tiling_hash(proc: &Shared, tile: Rect, under: Option<&Paint>) -> u64 { + let mut h = DefaultHasher::new(); + proc.hash(&mut h); + [tile.x0, tile.y0, tile.x1, tile.y1].map(|v| (v + 0.0).to_bits()).hash(&mut h); + if let Some(Paint::Solid { color, swatch, tint }) = under { + let c = match *color { + Color::Rgb { r, g, b } => [r, g, b, 0.0], + Color::Cmyk { c, m, y, k } => [c, m, y, k], + Color::Gray { k } => [k, 0.0, 0.0, 0.0], + Color::Lab { l, a, b } => [l, a, b, 0.0], + }; + c.map(|v| (v + 0.0).to_bits()).hash(&mut h); + swatch.hash(&mut h); + (tint + 0.0).to_bits().hash(&mut h); + } + h.finish() +} + impl Out { pub fn new(doc: Document, page: Affine, frame: Rect) -> Self { Self { @@ -226,8 +254,9 @@ impl Out { cmyk: 0, rgb: 0, spots: vec![], - shadings: vec![], - tilings: vec![], + spot_colors: HashMap::new(), + shadings: HashSet::new(), + tilings: HashMap::new(), apart: 0, } } @@ -891,11 +920,14 @@ impl Interp<'_> { Paint::Solid { color: Color::cmyk(1.0, 1.0, 1.0, 1.0).tinted(t), swatch: Some(REGISTRATION.into()), tint: t } } [one] => { - let full = match self.out.spots.iter().find(|(n, _)| **n == **one) { - Some((_, c)) => *c, + let full = match self.out.spot_colors.get(one) { + Some(c) => *c, None => { let c = self.tint_color(alt, tint, &[1.0])?; self.out.spots.push((one.to_string(), c)); + // The tint transform may have listed this ink already: the color + // listed first stays. + self.out.spot_colors.entry(one.clone()).or_insert(c); c } }; @@ -986,7 +1018,9 @@ impl Interp<'_> { let xf = m * Affine::translate(tile.origin().to_vec2()); let proc = get("PaintProc").ok_or(PsError::Ps("undefined", "PaintProc".into()))?; let key = proc.items().cloned().ok_or(PsError::Ps("typecheck", "PaintProc".into()))?; - if let Some((.., name)) = self.out.tilings.iter().find(|(p, t, u, _)| p.same(&key) && *t == tile && *u == under) { + let hash = tiling_hash(&key, tile, under.as_ref()); + let made = self.out.tilings.get(&hash).and_then(|list| list.iter().find(|(p, t, u, _)| p.same(&key) && *t == tile && *u == under)); + if let Some((.., name)) = made { return Ok(Paint::Pattern { pattern: name.clone(), xf }); } if self.out.apart >= MAX_APART { @@ -1004,12 +1038,16 @@ impl Interp<'_> { if art.is_empty() { return Ok(Paint::None); } - let name = - (self.out.doc.patterns.len() + 1..).map(|i| format!("Pattern {i}")).find(|n| self.out.doc.pattern(n).is_none()).unwrap_or_default(); + // The document gets its patterns only here, so the n made so far are Pattern 1 to + // Pattern n. + let name = format!("Pattern {}", self.out.doc.patterns.len() + 1); let mut def = PatternDef::new(&name, art); def.tile = tile; self.out.doc.patterns.push(def); - self.out.tilings.push((key, tile, under, name.clone())); + // A cell with a NaN corner equals no cell, so the lookup above could never find it. + if !tile.is_nan() { + self.out.tilings.entry(hash).or_insert_with(|| Vec::with_capacity(1)).push((key, tile, under, name.clone())); + } Ok(Paint::Pattern { pattern: name, xf }) } @@ -1697,7 +1735,7 @@ impl Interp<'_> { self.out.fill(area.to_path(0.1), FillRule::NonZero, paint, self.g.overprint, &clips); if let Some((_, n)) = self.out.drawn.last() { let id = n.id; - self.out.shadings.push(id); + self.out.shadings.insert(id); } } } @@ -1756,7 +1794,7 @@ fn linear_points(f: &Obj, depth: u32, visits: &mut usize) -> Option> { /// The clip path a clipping group of one shading rectangle (`shfill` through a clip) can be /// filled with instead. -pub(crate) fn collapse(nodes: &mut [Arc], shadings: &[NodeId]) { +pub(crate) fn collapse(nodes: &mut [Arc], shadings: &HashSet) { for n in nodes.iter_mut() { let NodeKind::Group { children, clip: true } = &n.kind else { if let Some(c) = Arc::make_mut(n).children_mut() { diff --git a/crates/eps/src/import/mod.rs b/crates/eps/src/import/mod.rs index d6e9f92a..3be867a7 100644 --- a/crates/eps/src/import/mod.rs +++ b/crates/eps/src/import/mod.rs @@ -54,10 +54,11 @@ mod tests_images; #[cfg(test)] mod tests_native; +use std::collections::HashSet; use std::sync::Arc; use vectorcraft_color::Paint; -use vectorcraft_color::swatch::Swatch; +use vectorcraft_color::swatch::{REGISTRATION, Swatch}; use vectorcraft_doc::{ColorMode, Document, LayerColor, Node}; use vectorcraft_geom::{Affine, Rect}; @@ -253,14 +254,16 @@ fn finish_with(out: Out, layers: Vec>) -> Imported { let (swatches, groups) = vectorcraft_color::default_swatches(ColorMode::Cmyk.model()); (doc.swatches, doc.swatch_groups) = (swatches.into(), groups.into()); } + // `taken` holds the names `Document::swatch` finds: each swatch's name, and Registration. + let mut taken: HashSet = doc.swatches_iter().map(|s| s.name.clone()).chain([REGISTRATION.to_string()]).collect(); for (name, color) in out.spots { - if doc.swatch(&name).is_none() { + if taken.insert(name.clone()) { doc.swatches.push(Swatch { name, paint: Paint::solid(color), global: true, spot: true }); } } // Tiling patterns are pattern swatches. for name in doc.patterns.iter().map(|p| p.name.clone()).collect::>() { - if doc.swatch(&name).is_none() { + if taken.insert(name.clone()) { doc.swatches.push(Swatch { paint: Paint::Pattern { pattern: name.clone(), xf: Affine::IDENTITY }, name, global: false, spot: false }); } } diff --git a/crates/eps/src/import/obj.rs b/crates/eps/src/import/obj.rs index 2a28fff9..220f79dc 100644 --- a/crates/eps/src/import/obj.rs +++ b/crates/eps/src/import/obj.rs @@ -2,6 +2,7 @@ use std::cell::{Ref, RefCell}; use std::collections::HashMap; +use std::hash::{Hash, Hasher}; use std::rc::Rc; use super::data::Stream; @@ -196,6 +197,14 @@ impl Shared { } } +/// Hashes what [`Shared::same`] compares: the storage and the run of it. +impl Hash for Shared { + fn hash(&self, state: &mut H) { + Rc::as_ptr(&self.buf).hash(state); + (self.at, self.len).hash(state); + } +} + impl Key { pub fn name(s: &str) -> Self { Self::Name(Rc::from(s)) diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 77a02bb2..0fe18e63 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -288,6 +288,51 @@ fn shadings_become_gradients() { assert_eq!(offsets, vec![0.0, 0.25, 1.0]); } +/// Tiling patterns and spot inks are found again through a hash of what tells them apart, so a +/// program that makes 200,000 patterns and 200,001 inks reads within [`read_soon`]'s 20 seconds. +/// Patterns are named Pattern 1, Pattern 2 and so on in the order they are made. A pattern made +/// again from its prototype is the same pattern, and so is an uncolored pattern set again in a +/// color it was set in before. +#[test] +fn many_tiling_patterns_and_spot_inks_read_in_linear_time() { + let r = read( + "/pp { pop 0 0 1 1 rectfill } def \ + /a << /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep 2 /YStep 2 /PaintProc /pp load >> def \ + /b << /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep 3 /YStep 2 /PaintProc /pp load >> def \ + /u << /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 1 1] /XStep 2 /YStep 2 /PaintProc /pp load >> matrix makepattern def \ + a matrix makepattern setpattern 0 0 10 10 rectfill b matrix makepattern setpattern 10 0 10 10 rectfill \ + a [2 0 0 2 0 0] makepattern setpattern 20 0 10 10 rectfill /DeviceRGB setcolorspace \ + 1 0 0 u setpattern 30 0 10 10 rectfill 0 0 1 u setpattern 40 0 10 10 rectfill 1 0 0 u setpattern 50 0 10 10 rectfill", + ); + let d = &r.document; + let names: Vec<&str> = d.patterns.iter().map(|p| p.name.as_str()).collect(); + assert_eq!(names, ["Pattern 1", "Pattern 2", "Pattern 3", "Pattern 4"]); + let fills: Vec = objects(d) + .iter() + .map(|o| match fill(o) { + Some(Paint::Pattern { pattern, .. }) => pattern.clone(), + p => panic!("{p:?}"), + }) + .collect(); + assert_eq!(fills, ["Pattern 1", "Pattern 2", "Pattern 1", "Pattern 3", "Pattern 4", "Pattern 3"]); + assert!(names.iter().all(|n| d.swatch(n).is_some_and(|s| matches!(s.paint, Paint::Pattern { .. })))); + // 100,000 patterns whose cells differ, an uncolored pattern in 100,000 tints of an ink that is + // white at every tint (only the tint tells those patterns apart), and 200,000 more inks. + let r = read_soon( + "0 1 99999 { /i exch def << /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep i 2 add /YStep 1 \ + /PaintProc { pop 0 0 1 1 rectfill } >> matrix makepattern setpattern } for \ + [/Separation (Spot) /DeviceGray { pop 1 }] setcolorspace \ + /p << /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 1 1] /XStep 2 /YStep 2 /PaintProc { pop 0 0 1 1 rectfill } >> \ + matrix makepattern def 0 1 99999 { 100000 div p setpattern } for \ + 0 1 199999 { /i exch def [/Separation i 8 string cvs /DeviceGray {}] setcolorspace } for 0 0 1 1 rectfill", + ); + let d = &r.document; + assert_eq!(d.patterns.len(), 200_000); + assert!(d.patterns.iter().enumerate().all(|(i, p)| p.name == format!("Pattern {}", i + 1))); + assert_eq!(d.swatches.iter().filter(|s| matches!(s.paint, Paint::Pattern { .. })).count(), 200_000); + assert_eq!(d.swatches.iter().filter(|s| s.spot).count(), 200_001); +} + #[test] fn images_and_image_masks() { // 2 × 2 RGB from hex data in the file, through a procedure.