mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
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:
@@ -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