mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
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:
1 parent
04a305367e
commit
be9d05b274
4 files changed
+111
-16
No files matched your search
@@ -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() {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user