EPS: the PostScript reader reads many tiling patterns, spot inks and shadings in linear time

Setting a tiling pattern went through every pattern made so far to
find one with the same PaintProc, cell and color, and went through the
document's patterns again to name a new one. Each spot ink was looked
up in the list of every ink used. When the reading ended, each ink and
each pattern became a swatch after a search of all the swatches, and
the second object of each clipping group of two was looked up in the
list of every shading painted. For a file that makes n of any of these,
the searches did work that grows with n squared: a dev-profile build of
vectorcraft-cli info took 99 user CPU seconds on a file that makes
160,000 tiling patterns, 35 on one that uses 160,000 spot inks, and 3.2
on one that paints 160,000 shadings through clips.

The patterns are now kept in a hash map, under a hash of the
PaintProc's storage, the cell, and the color, tint and swatch that an
uncolored pattern is set in. A lookup compares the patterns under the
same hash, with the comparison it made before. The inks are kept in a
map by name, and the shadings are kept in a set. The swatch names go
in a set while the swatches are made. The reader's document gets its
patterns only from tiling, so the name search always ended at Pattern
n+1 after n patterns, and that name is now made directly. A pattern
whose cell has a NaN corner never compared equal to another, and it is
no longer kept for lookups. The three files now read in 1.3, 0.6 and
0.9 user CPU seconds, and the documents read are the same as before.

Tests: a program that makes 200,000 tiling patterns and uses 200,001
spot inks reads within 20 seconds, with the patterns named in order;
a pattern made again from its prototype, and an uncolored pattern set
again in a color it was set in, are the same pattern.
This commit is contained in:
dotCooCoo authored and bflatastic committed 2026-10-12 11:25:31 +07:00
1 parent 04a305367e
commit be9d05b274
4 files changed
+111 -16

No files matched your search

+51 -13
View File
@@ -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<Rc<str>, Color>,
/// Rectangles painted with a shading (`shfill`): a clipping group of one is the clip path
/// filled with it.
pub shadings: Vec<NodeId>,
pub shadings: HashSet<NodeId>,
/// 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<Obj>, Rect, Option<Paint>, 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<u64, Vec<Tiling>>,
/// 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<Obj>, Rect, Option<Paint>, 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<Obj>, 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<Vec<f64>> {
/// 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<Node>], shadings: &[NodeId]) {
pub(crate) fn collapse(nodes: &mut [Arc<Node>], shadings: &HashSet<NodeId>) {
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() {
+6 -3
View File
@@ -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<Arc<Node>>) -> 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<String> = 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::<Vec<_>>() {
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 });
}
}
+9
View File
@@ -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<T: Clone> Shared<T> {
}
}
/// Hashes what [`Shared::same`] compares: the storage and the run of it.
impl<T> Hash for Shared<T> {
fn hash<H: Hasher>(&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))
+45
View File
@@ -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<String> = 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.