Merge pull request #1341 from dotCooCoo/feat/eps-quadratic-tiling-registration

EPS: the PostScript reader reads many tiling patterns, spot inks and shadings in linear time
This commit is contained in:
BFlat
2026-10-12 11:28:00 +07:00
committed by GitHub
4 changed files with 111 additions and 16 deletions
+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.