mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-21 06:34:18 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
991ce2c635 | ||
|
|
0efcaeb558 |
No files matched your search
Generated
+1
@@ -4800,6 +4800,7 @@ dependencies = [
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-testkit",
|
||||
"vectorcraft-text",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+89
-5
@@ -16,6 +16,7 @@ mod defaults;
|
||||
pub mod track;
|
||||
mod warp;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -557,10 +558,11 @@ fn tolerance(scale: f64) -> f64 {
|
||||
(0.02 * scale).clamp(0.01, 0.25)
|
||||
}
|
||||
|
||||
/// Filled art for one brushed stroke along `bp`. Pieces are in document space with id 0; their
|
||||
/// appearances carry no brushes (so they never recurse). Stroke-level opacity/blend are the
|
||||
/// caller's job (`StrokeLayer::opacity`).
|
||||
pub fn stroke_pieces(brush: &Brush, bp: &BezPath, stroke: &StrokeLayer) -> Vec<Node> {
|
||||
/// Filled art for one brushed stroke along `bp`, in `doc`. Pieces are in document space with id
|
||||
/// 0, and never draw a brushed stroke (so they never recurse): their own strokes carry no brush,
|
||||
/// and what they draw by name that would draw one is left out ([`leave_out_brushed`]).
|
||||
/// Stroke-level opacity/blend are the caller's job (`StrokeLayer::opacity`).
|
||||
pub fn stroke_pieces(doc: &Document, brush: &Brush, bp: &BezPath, stroke: &StrokeLayer) -> Vec<Node> {
|
||||
let w = if stroke.width > 0.0 { stroke.width } else { 1.0 };
|
||||
let mut out = match &brush.kind {
|
||||
BrushKind::Calligraphic(c) => calli::calligraphic(c, bp, w, stroke, &brush.name),
|
||||
@@ -573,9 +575,91 @@ pub fn stroke_pieces(brush: &Brush, bp: &BezPath, stroke: &StrokeLayer) -> Vec<N
|
||||
for n in &mut out {
|
||||
colorize(n, colorization, &stroke.paint);
|
||||
}
|
||||
leave_out_brushed(doc, &mut out, &mut Known::new());
|
||||
out
|
||||
}
|
||||
|
||||
/// A symbol (`false`) or pattern (`true`) of a document, by name: what art draws by name.
|
||||
type Named<'a> = (bool, &'a str);
|
||||
|
||||
/// What [`draws_brushed`] found of each symbol and pattern so far.
|
||||
type Known = HashMap<(bool, String), bool>;
|
||||
|
||||
/// The symbol an instance draws and the patterns that paint `n` (not its children).
|
||||
fn named(n: &Node) -> impl Iterator<Item = Named<'_>> {
|
||||
let symbol = match &n.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => Some((false, symbol.as_str())),
|
||||
_ => None,
|
||||
};
|
||||
let patterns = vectorcraft_doc::swatches::node_paints(n).filter_map(|p| match p {
|
||||
Paint::Pattern { pattern, .. } => Some((true, pattern.as_str())),
|
||||
_ => None,
|
||||
});
|
||||
symbol.into_iter().chain(patterns)
|
||||
}
|
||||
|
||||
/// Does drawing the symbol or pattern `def` of `doc` draw a brushed stroke: in its art, or in
|
||||
/// the art of the symbols and patterns that uses (and so on)? `known`: the answers so far.
|
||||
fn draws_brushed(doc: &Document, def: Named<'_>, known: &mut Known) -> bool {
|
||||
let key = (def.0, def.1.to_string());
|
||||
if let Some(&answer) = known.get(&key) {
|
||||
return answer;
|
||||
}
|
||||
let art = |(pattern, name): Named<'_>| -> Vec<&Node> {
|
||||
match pattern {
|
||||
false => doc.symbols.iter().filter(|s| s.name == name).take(1).map(|s| &*s.art).collect(),
|
||||
true => doc.pattern(name).map(|p| p.art.iter().map(|n| &**n).collect()).unwrap_or_default(),
|
||||
}
|
||||
};
|
||||
// Without recursion, each one looked at once: they may use one another without end.
|
||||
let (mut seen, mut stack) = (std::collections::HashSet::from([key.clone()]), art(def));
|
||||
let mut answer = false;
|
||||
while let Some(n) = stack.pop() {
|
||||
if has_brush(n) {
|
||||
answer = true;
|
||||
break;
|
||||
}
|
||||
stack.extend(n.children().into_iter().flatten().chain(n.extras().mask.as_ref().map(|m| &m.art)).map(|c| &**c));
|
||||
for other in named(n) {
|
||||
if seen.insert((other.0, other.1.to_string())) {
|
||||
stack.extend(art(other));
|
||||
}
|
||||
}
|
||||
}
|
||||
known.insert(key, answer);
|
||||
answer
|
||||
}
|
||||
|
||||
/// Leave out of brush pieces `nodes` what they draw by name that draws a brushed stroke
|
||||
/// ([`draws_brushed`]): such an instance goes, such a pattern paint becomes no paint. A symbol or
|
||||
/// pattern holding art stroked with the brush itself would draw the brush again, without end.
|
||||
fn leave_out_brushed(doc: &Document, nodes: &mut Vec<Node>, known: &mut Known) {
|
||||
fn walk(doc: &Document, n: &mut Node, known: &mut Known) {
|
||||
for paint in vectorcraft_doc::swatches::node_paints_mut(n) {
|
||||
if let Paint::Pattern { pattern, .. } = paint
|
||||
&& draws_brushed(doc, (true, pattern.as_str()), known)
|
||||
{
|
||||
*paint = Paint::None;
|
||||
}
|
||||
}
|
||||
if let Some(children) = n.children_mut() {
|
||||
children.retain(|c| !matches!(&c.kind, NodeKind::SymbolInstance { symbol, .. } if draws_brushed(doc, (false, symbol.as_str()), known)));
|
||||
for c in children.iter_mut() {
|
||||
walk(doc, Arc::make_mut(c), known);
|
||||
}
|
||||
}
|
||||
if n.extras().mask.is_some()
|
||||
&& let Some(mask) = n.extras_mut().mask.as_mut()
|
||||
{
|
||||
walk(doc, Arc::make_mut(&mut mask.art), known);
|
||||
}
|
||||
}
|
||||
nodes.retain(|n| !matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if draws_brushed(doc, (false, symbol.as_str()), known)));
|
||||
for n in nodes.iter_mut() {
|
||||
walk(doc, n, known);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pieces for every brushed stroke of `n` (a path or compound path), in appearance order, each
|
||||
/// with the stroke's opacity folded into the piece opacity. `None` if nothing is brushed.
|
||||
pub fn node_pieces(doc: &Document, n: &Node) -> Option<Vec<Node>> {
|
||||
@@ -591,7 +675,7 @@ pub fn node_pieces(doc: &Document, n: &Node) -> Option<Vec<Node>> {
|
||||
continue;
|
||||
}
|
||||
any = true;
|
||||
for mut p in stroke_pieces(b, &bp, st) {
|
||||
for mut p in stroke_pieces(doc, b, &bp, st) {
|
||||
p.opacity *= st.opacity;
|
||||
if st.blend != vectorcraft_color::BlendMode::Normal {
|
||||
p.blend = st.blend;
|
||||
|
||||
+90
-38
@@ -37,7 +37,7 @@ fn all_points(n: &Node, out: &mut Vec<Point>) {
|
||||
|
||||
#[test]
|
||||
fn calligraphic_vertical_nib_is_full_width_across_horizontal_stroke() {
|
||||
let out = stroke_pieces(&calli(90.0, 20.0, 10.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &calli(90.0, 20.0, 10.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
assert_eq!(out.len(), 1);
|
||||
let b = bounds(&out);
|
||||
assert!(near(b.height(), 10.0, 0.05), "{b:?}");
|
||||
@@ -46,30 +46,30 @@ fn calligraphic_vertical_nib_is_full_width_across_horizontal_stroke() {
|
||||
|
||||
#[test]
|
||||
fn calligraphic_flat_nib_is_thin_along_its_angle() {
|
||||
let out = stroke_pieces(&calli(0.0, 20.0, 10.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &calli(0.0, 20.0, 10.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
assert!(near(bounds(&out).height(), 2.0, 0.05), "{:?}", bounds(&out));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calligraphic_width_at_45_degrees_matches_ellipse_support() {
|
||||
let (a, b) = (5.0_f64, 1.0_f64);
|
||||
let out = stroke_pieces(&calli(45.0, 20.0, 10.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &calli(45.0, 20.0, 10.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
let expect = 2.0 * ((a * a + b * b) / 2.0).sqrt();
|
||||
assert!(near(bounds(&out).height(), expect, 0.05), "{} vs {expect}", bounds(&out).height());
|
||||
// A vertical stroke with the same nib has the same width (45° is symmetric).
|
||||
let v = stroke_pieces(&calli(45.0, 20.0, 10.0), &line(0.0, 0.0, 0.0, 100.0), &stroke(1.0));
|
||||
let v = stroke_pieces(&Document::new(1.0, 1.0), &calli(45.0, 20.0, 10.0), &line(0.0, 0.0, 0.0, 100.0), &stroke(1.0));
|
||||
assert!(near(bounds(&v).width(), expect, 0.05));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calligraphic_scales_with_stroke_weight() {
|
||||
let out = stroke_pieces(&calli(90.0, 100.0, 3.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(2.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &calli(90.0, 100.0, 3.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(2.0));
|
||||
assert!(near(bounds(&out).height(), 6.0, 0.05));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_nib_is_direction_independent() {
|
||||
let out = stroke_pieces(&calli(0.0, 100.0, 4.0), &line(0.0, 0.0, 60.0, 80.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &calli(0.0, 100.0, 4.0), &line(0.0, 0.0, 60.0, 80.0), &stroke(1.0));
|
||||
let b = bounds(&out);
|
||||
// Round caps: the bounds grow by the radius on every side.
|
||||
assert!(near(b.x0, -2.0, 0.05) && near(b.y1, 82.0, 0.05), "{b:?}");
|
||||
@@ -78,7 +78,7 @@ fn round_nib_is_direction_independent() {
|
||||
#[test]
|
||||
fn closed_calligraphic_makes_a_ring() {
|
||||
let bp = shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 100.0)).to_bezpath();
|
||||
let out = stroke_pieces(&calli(0.0, 100.0, 6.0), &bp, &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &calli(0.0, 100.0, 6.0), &bp, &stroke(1.0));
|
||||
let pd = out[0].path_data().unwrap();
|
||||
assert_eq!(pd.subpaths.len(), 2);
|
||||
let poly_area = |sp: &vectorcraft_geom::SubPath| {
|
||||
@@ -102,12 +102,14 @@ fn rect_art(w: f64, h: f64) -> Node {
|
||||
|
||||
#[test]
|
||||
fn art_brush_stretches_to_path_length() {
|
||||
let out = stroke_pieces(&art_brush(rect_art(10.0, 4.0), ArtScale::Stretch), &line(20.0, 50.0, 220.0, 50.0), &stroke(1.0));
|
||||
let out =
|
||||
stroke_pieces(&Document::new(1.0, 1.0), &art_brush(rect_art(10.0, 4.0), ArtScale::Stretch), &line(20.0, 50.0, 220.0, 50.0), &stroke(1.0));
|
||||
let b = bounds(&out);
|
||||
assert!(near(b.x0, 20.0, 1e-6) && near(b.x1, 220.0, 1e-6), "{b:?}");
|
||||
assert!(near(b.height(), 4.0, 1e-6));
|
||||
// 3 pt stroke → 3× wider.
|
||||
let out = stroke_pieces(&art_brush(rect_art(10.0, 4.0), ArtScale::Stretch), &line(20.0, 50.0, 220.0, 50.0), &stroke(3.0));
|
||||
let out =
|
||||
stroke_pieces(&Document::new(1.0, 1.0), &art_brush(rect_art(10.0, 4.0), ArtScale::Stretch), &line(20.0, 50.0, 220.0, 50.0), &stroke(3.0));
|
||||
assert!(near(bounds(&out).height(), 12.0, 1e-6));
|
||||
}
|
||||
|
||||
@@ -120,7 +122,7 @@ fn art_brush_arc_length_mapping_is_linear() {
|
||||
let mut spacer = frame.clone();
|
||||
spacer.appearance = Appearance::default().into();
|
||||
let art = Node::group(NodeId(0), vec![std::sync::Arc::new(spacer), std::sync::Arc::new(marker)]);
|
||||
let out = stroke_pieces(&art_brush(art, ArtScale::Stretch), &line(0.0, 0.0, 300.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &art_brush(art, ArtScale::Stretch), &line(0.0, 0.0, 300.0, 0.0), &stroke(1.0));
|
||||
let g = &out[0];
|
||||
let m = g.children().unwrap()[1].geometric_bounds().unwrap();
|
||||
assert!(near(m.x0, 150.0, 1e-6) && near(m.x1, 180.0, 1e-6), "{m:?}");
|
||||
@@ -128,7 +130,8 @@ fn art_brush_arc_length_mapping_is_linear() {
|
||||
|
||||
#[test]
|
||||
fn art_brush_proportional_keeps_aspect() {
|
||||
let out = stroke_pieces(&art_brush(rect_art(100.0, 4.0), ArtScale::Proportional), &line(0.0, 0.0, 200.0, 0.0), &stroke(1.0));
|
||||
let out =
|
||||
stroke_pieces(&Document::new(1.0, 1.0), &art_brush(rect_art(100.0, 4.0), ArtScale::Proportional), &line(0.0, 0.0, 200.0, 0.0), &stroke(1.0));
|
||||
assert!(near(bounds(&out).height(), 8.0, 1e-6));
|
||||
}
|
||||
|
||||
@@ -139,7 +142,12 @@ fn art_brush_between_guides_keeps_ends() {
|
||||
Node::path(NodeId(0), shapes::rectangle(Rect::new(80.0, -4.0, 100.0, 4.0)), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
|
||||
let shaft = rect_art(80.0, 2.0);
|
||||
let art = Node::group(NodeId(0), vec![std::sync::Arc::new(shaft), std::sync::Arc::new(head)]);
|
||||
let out = stroke_pieces(&art_brush(art, ArtScale::BetweenGuides { start: 0.0, end: 0.8 }), &line(0.0, 0.0, 300.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(
|
||||
&Document::new(1.0, 1.0),
|
||||
&art_brush(art, ArtScale::BetweenGuides { start: 0.0, end: 0.8 }),
|
||||
&line(0.0, 0.0, 300.0, 0.0),
|
||||
&stroke(1.0),
|
||||
);
|
||||
let h = out[0].children().unwrap()[1].geometric_bounds().unwrap();
|
||||
assert!(near(h.x0, 280.0, 1e-6) && near(h.x1, 300.0, 1e-6), "{h:?}");
|
||||
}
|
||||
@@ -154,7 +162,7 @@ fn art_brush_follows_curves() {
|
||||
bp_open.push(*el);
|
||||
}
|
||||
}
|
||||
let out = stroke_pieces(&art_brush(rect_art(10.0, 4.0), ArtScale::Stretch), &bp_open, &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &art_brush(rect_art(10.0, 4.0), ArtScale::Stretch), &bp_open, &stroke(1.0));
|
||||
let mut pts = vec![];
|
||||
all_points(&out[0], &mut pts);
|
||||
assert!(pts.len() > 40, "long edges are subdivided so they bend: {}", pts.len());
|
||||
@@ -178,13 +186,13 @@ fn scatter_brush(spacing: f64) -> Brush {
|
||||
|
||||
#[test]
|
||||
fn scatter_count_and_spacing() {
|
||||
let out = stroke_pieces(&scatter_brush(100.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &scatter_brush(100.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
assert_eq!(out.len(), 10);
|
||||
let centres: Vec<f64> = out.iter().map(|n| n.geometric_bounds().unwrap().center().x).collect();
|
||||
for (i, c) in centres.iter().enumerate() {
|
||||
assert!(near(*c, 5.0 + 10.0 * i as f64, 1e-6), "{centres:?}");
|
||||
}
|
||||
let out = stroke_pieces(&scatter_brush(200.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &scatter_brush(200.0), &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
assert_eq!(out.len(), 5);
|
||||
}
|
||||
|
||||
@@ -201,8 +209,8 @@ fn scatter_is_deterministic_and_randomised() {
|
||||
}),
|
||||
};
|
||||
let p = line(0.0, 0.0, 300.0, 0.0);
|
||||
let a1 = stroke_pieces(&b, &p, &stroke(1.0));
|
||||
let a2 = stroke_pieces(&b, &p, &stroke(1.0));
|
||||
let a1 = stroke_pieces(&Document::new(1.0, 1.0), &b, &p, &stroke(1.0));
|
||||
let a2 = stroke_pieces(&Document::new(1.0, 1.0), &b, &p, &stroke(1.0));
|
||||
assert_eq!(a1, a2);
|
||||
let ys: Vec<f64> = a1.iter().map(|n| n.geometric_bounds().unwrap().center().y).collect();
|
||||
assert!(ys.iter().any(|y| y.abs() > 0.5), "scatter offsets copies: {ys:?}");
|
||||
@@ -228,21 +236,21 @@ fn pattern_brush(fit: PatternFit, corners: bool) -> Brush {
|
||||
#[test]
|
||||
fn pattern_tile_count_by_fit() {
|
||||
let p = line(0.0, 0.0, 100.0, 0.0);
|
||||
assert_eq!(stroke_pieces(&pattern_brush(PatternFit::Stretch, false), &p, &stroke(1.0)).len(), 10);
|
||||
assert_eq!(stroke_pieces(&Document::new(1.0, 1.0), &pattern_brush(PatternFit::Stretch, false), &p, &stroke(1.0)).len(), 10);
|
||||
let p2 = line(0.0, 0.0, 104.0, 0.0);
|
||||
let st = stroke_pieces(&pattern_brush(PatternFit::Stretch, false), &p2, &stroke(1.0));
|
||||
let st = stroke_pieces(&Document::new(1.0, 1.0), &pattern_brush(PatternFit::Stretch, false), &p2, &stroke(1.0));
|
||||
assert_eq!(st.len(), 10);
|
||||
assert!(near(bounds(&st).width(), 104.0, 1e-6), "stretch fills the path exactly");
|
||||
let sp = stroke_pieces(&pattern_brush(PatternFit::AddSpace, false), &line(0.0, 0.0, 109.0, 0.0), &stroke(1.0));
|
||||
let sp = stroke_pieces(&Document::new(1.0, 1.0), &pattern_brush(PatternFit::AddSpace, false), &line(0.0, 0.0, 109.0, 0.0), &stroke(1.0));
|
||||
assert_eq!(sp.len(), 10);
|
||||
assert!(near(sp[0].geometric_bounds().unwrap().width(), 10.0, 1e-6), "add space keeps tile size");
|
||||
assert_eq!(stroke_pieces(&pattern_brush(PatternFit::Stretch, false), &p, &stroke(2.0)).len(), 5);
|
||||
assert_eq!(stroke_pieces(&Document::new(1.0, 1.0), &pattern_brush(PatternFit::Stretch, false), &p, &stroke(2.0)).len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_places_corner_tiles() {
|
||||
let sq = shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)).to_bezpath();
|
||||
let out = stroke_pieces(&pattern_brush(PatternFit::Stretch, true), &sq, &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &pattern_brush(PatternFit::Stretch, true), &sq, &stroke(1.0));
|
||||
let corners = out
|
||||
.iter()
|
||||
.filter(
|
||||
@@ -257,7 +265,7 @@ fn pattern_places_corner_tiles() {
|
||||
#[test]
|
||||
fn bristle_makes_translucent_strands() {
|
||||
let b = Brush { name: "b".into(), kind: BrushKind::Bristle(Bristle::default()) };
|
||||
let out = stroke_pieces(&b, &line(0.0, 0.0, 200.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &b, &line(0.0, 0.0, 200.0, 0.0), &stroke(1.0));
|
||||
assert!(out.len() >= 5, "{}", out.len());
|
||||
assert!(out.iter().all(|n| n.opacity < 1.0 && n.opacity > 0.0));
|
||||
let bb = bounds(&out);
|
||||
@@ -270,7 +278,7 @@ fn tints_colorization_uses_stroke_colour() {
|
||||
let BrushKind::Art(mut a) = b.kind else { panic!("not an art brush") };
|
||||
a.colorization = Colorization::Tints;
|
||||
let b = Brush { name: "t".into(), kind: BrushKind::Art(a) };
|
||||
let out = stroke_pieces(&b, &line(0.0, 0.0, 50.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &b, &line(0.0, 0.0, 50.0, 0.0), &stroke(1.0));
|
||||
assert_eq!(out[0].appearance.fill_paint(), Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
|
||||
let mut white = rect_art(1.0, 1.0);
|
||||
white.appearance.set_fill(Paint::solid(Color::WHITE));
|
||||
@@ -310,7 +318,7 @@ fn brushes_serde_round_trip() {
|
||||
fn every_default_brush_draws_something() {
|
||||
let bp = preview_path(120.0, 24.0);
|
||||
for b in defaults() {
|
||||
let out = stroke_pieces(b, &bp, &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), b, &bp, &stroke(1.0));
|
||||
assert!(!out.is_empty(), "{}", b.name);
|
||||
let bb = bounds(&out);
|
||||
assert!(bb.width() > 50.0 && bb.width() < 200.0, "{}: {bb:?}", b.name);
|
||||
@@ -365,20 +373,20 @@ fn pressed(points: Vec<(f64, f64)>) -> StrokeLayer {
|
||||
fn pressure_maps_to_size_along_the_stroke() {
|
||||
let b = pressure_calli(10.0, 4.0);
|
||||
let light_to_heavy = pressed(vec![(0.0, 0.0), (1.0, 1.0)]);
|
||||
let out = stroke_pieces(&b, &line(0.0, 0.0, 100.0, 0.0), &light_to_heavy);
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &b, &line(0.0, 0.0, 100.0, 0.0), &light_to_heavy);
|
||||
// Lightest: 6 pt (half 3); heaviest: 14 pt (half 7); in between the pressure there.
|
||||
assert!(near(half_width(&out, 0.5, 1.5), 3.0, 0.1), "{}", half_width(&out, 0.5, 1.5));
|
||||
assert!(near(half_width(&out, 98.5, 99.5), 7.0, 0.1), "{}", half_width(&out, 98.5, 99.5));
|
||||
assert!(near(half_width(&out, 49.5, 50.5), 5.0, 0.1), "{}", half_width(&out, 49.5, 50.5));
|
||||
// The stroke weight scales it.
|
||||
let heavy = StrokeLayer { width: 2.0, ..light_to_heavy };
|
||||
let out = stroke_pieces(&b, &line(0.0, 0.0, 100.0, 0.0), &heavy);
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &b, &line(0.0, 0.0, 100.0, 0.0), &heavy);
|
||||
assert!(near(half_width(&out, 98.5, 99.5), 14.0, 0.2));
|
||||
// No pressure recorded (a mouse, or a path the brush was applied to): the size itself.
|
||||
let out = stroke_pieces(&b, &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &b, &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
assert!(near(bounds(&out).height(), 10.0, 0.05), "{:?}", bounds(&out));
|
||||
// Kept within the valid range: 2 ± 4 never goes below nothing.
|
||||
let out = stroke_pieces(&pressure_calli(2.0, 4.0), &line(0.0, 0.0, 100.0, 0.0), &pressed(vec![(0.0, 0.0), (1.0, 0.0)]));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &pressure_calli(2.0, 4.0), &line(0.0, 0.0, 100.0, 0.0), &pressed(vec![(0.0, 0.0), (1.0, 0.0)]));
|
||||
assert!(!out.is_empty() && bounds(&out).height() < 0.2, "{:?}", bounds(&out));
|
||||
}
|
||||
|
||||
@@ -389,7 +397,7 @@ fn pressure_varies_angle_and_roundness() {
|
||||
let horizontal = line(0.0, 0.0, 100.0, 0.0);
|
||||
let draw = |c: Calligraphic, p: f64| {
|
||||
let b = Brush { name: "a".into(), kind: BrushKind::Calligraphic(c) };
|
||||
bounds(&stroke_pieces(&b, &horizontal, &pressed(vec![(0.0, p), (1.0, p)]))).height()
|
||||
bounds(&stroke_pieces(&Document::new(1.0, 1.0), &b, &horizontal, &pressed(vec![(0.0, p), (1.0, p)]))).height()
|
||||
};
|
||||
use Variation::{Fixed, Pressure};
|
||||
// Angle 0 ± 90: heavy pressure stands the flat nib up across the stroke.
|
||||
@@ -399,7 +407,7 @@ fn pressure_varies_angle_and_roundness() {
|
||||
let vertical = Calligraphic { angle: 90.0, ..c([Fixed, Pressure, Fixed]) };
|
||||
let wide = |p| {
|
||||
let b = Brush { name: "r".into(), kind: BrushKind::Calligraphic(vertical.clone()) };
|
||||
bounds(&stroke_pieces(&b, &line(0.0, 0.0, 0.0, 100.0), &pressed(vec![(0.0, p), (1.0, p)]))).width()
|
||||
bounds(&stroke_pieces(&Document::new(1.0, 1.0), &b, &line(0.0, 0.0, 0.0, 100.0), &pressed(vec![(0.0, p), (1.0, p)]))).width()
|
||||
};
|
||||
assert!(near(wide(0.0), 0.1, 0.05), "{}", wide(0.0));
|
||||
assert!(near(wide(1.0), 10.0, 0.05), "{}", wide(1.0));
|
||||
@@ -421,12 +429,18 @@ fn brushes_without_modes_vary_randomly_as_before() {
|
||||
..old.clone()
|
||||
};
|
||||
let bp = preview_path(120.0, 24.0);
|
||||
assert_eq!(stroke_pieces(&old, &bp, &stroke(1.0)), stroke_pieces(&explicit, &bp, &stroke(1.0)));
|
||||
assert_eq!(
|
||||
stroke_pieces(&Document::new(1.0, 1.0), &old, &bp, &stroke(1.0)),
|
||||
stroke_pieces(&Document::new(1.0, 1.0), &explicit, &bp, &stroke(1.0))
|
||||
);
|
||||
// Fixed ignores the variation; Random still draws a different size per stroke.
|
||||
let fixed = Brush { kind: BrushKind::Calligraphic(Calligraphic { modes: Some([Variation::Fixed; 3]), ..c.clone() }), ..old.clone() };
|
||||
assert!(near(bounds(&stroke_pieces(&fixed, &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0))).height(), 8.0, 0.05));
|
||||
assert!(near(bounds(&stroke_pieces(&Document::new(1.0, 1.0), &fixed, &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0))).height(), 8.0, 0.05));
|
||||
// Recorded pressure changes nothing for a brush that doesn't use it.
|
||||
assert_eq!(stroke_pieces(&old, &bp, &stroke(1.0)), stroke_pieces(&old, &bp, &pressed(vec![(0.0, 0.0), (1.0, 1.0)])));
|
||||
assert_eq!(
|
||||
stroke_pieces(&Document::new(1.0, 1.0), &old, &bp, &stroke(1.0)),
|
||||
stroke_pieces(&Document::new(1.0, 1.0), &old, &bp, &pressed(vec![(0.0, 0.0), (1.0, 1.0)]))
|
||||
);
|
||||
// Modes are saved by name.
|
||||
assert_eq!(serde_json::to_value(Variation::ALL).unwrap(), serde_json::json!(["fixed", "random", "pressure"]));
|
||||
assert!(Variation::ALL.iter().all(|v| Variation::parse(v.id()) == Some(*v)));
|
||||
@@ -438,7 +452,7 @@ fn junk_pressure_brushes_draw_finite_art() {
|
||||
let bp = preview_path(120.0, 24.0);
|
||||
for (size, var) in [(1e308, 1e308), (-1e308, 1e308), (0.0, -1e308), (f64::MAX, f64::MAX)] {
|
||||
for profile in [vec![], vec![(0.0, 2.0), (0.0, -1.0), (5.0, 0.5)], vec![(1.0, 1.0), (0.0, 0.0)]] {
|
||||
let out = stroke_pieces(&pressure_calli(size, var), &bp, &pressed(profile));
|
||||
let out = stroke_pieces(&Document::new(1.0, 1.0), &pressure_calli(size, var), &bp, &pressed(profile));
|
||||
let mut pts = vec![];
|
||||
for n in &out {
|
||||
all_points(n, &mut pts);
|
||||
@@ -461,8 +475,9 @@ fn sized_scatter(lo: f64, hi: f64, mode: Variation) -> Brush {
|
||||
#[test]
|
||||
fn scatter_values_follow_their_modes() {
|
||||
let bp = line(0.0, 0.0, 400.0, 0.0);
|
||||
let heights =
|
||||
|b: &Brush, st: &StrokeLayer| -> Vec<f64> { stroke_pieces(b, &bp, st).iter().map(|n| n.geometric_bounds().unwrap().height()).collect() };
|
||||
let heights = |b: &Brush, st: &StrokeLayer| -> Vec<f64> {
|
||||
stroke_pieces(&Document::new(1.0, 1.0), b, &bp, st).iter().map(|n| n.geometric_bounds().unwrap().height()).collect()
|
||||
};
|
||||
// Fixed: every copy at 50 %, whatever the second value.
|
||||
assert!(heights(&sized_scatter(50.0, 300.0, Variation::Fixed), &stroke(1.0)).iter().all(|h| near(*h, 2.0, 1e-6)));
|
||||
// Pressure: light at the start (50 %: 2 pt tall), heavy at the end (300 %: 12 pt).
|
||||
@@ -478,7 +493,10 @@ fn scatter_values_follow_their_modes() {
|
||||
let BrushKind::Scatter(sc) = &old.kind else { panic!("not a scatter brush") };
|
||||
assert_eq!(Scatter { modes: None, ..sc.clone() }.modes(), [Variation::Random, Variation::Fixed, Variation::Fixed, Variation::Fixed]);
|
||||
let unsaved = Brush { kind: BrushKind::Scatter(Scatter { modes: None, ..sc.clone() }), ..old.clone() };
|
||||
assert_eq!(stroke_pieces(&old, &bp, &stroke(1.0)), stroke_pieces(&unsaved, &bp, &stroke(1.0)));
|
||||
assert_eq!(
|
||||
stroke_pieces(&Document::new(1.0, 1.0), &old, &bp, &stroke(1.0)),
|
||||
stroke_pieces(&Document::new(1.0, 1.0), &unsaved, &bp, &stroke(1.0))
|
||||
);
|
||||
assert!(serde_json::to_value(&unsaved).unwrap().get("modes").is_none(), "not saved when unset");
|
||||
}
|
||||
|
||||
@@ -521,3 +539,37 @@ fn art_colors_list_the_most_used_first() {
|
||||
assert_eq!(b.art().len(), 3, "the side and both corner tiles");
|
||||
assert!(calli(0.0, 100.0, 3.0).art().is_empty());
|
||||
}
|
||||
|
||||
/// Brush pieces draw no brushed stroke, also not by name: a symbol instance whose art is brushed
|
||||
/// is left out, a pattern paint whose tile is brushed becomes no paint, and plain ones stay.
|
||||
#[test]
|
||||
fn pieces_leave_out_what_draws_a_brushed_stroke_by_name() {
|
||||
use vectorcraft_doc::{PatternDef, Symbol};
|
||||
let mut doc = Document::new(100.0, 100.0);
|
||||
let mut brushed = rect_art(10.0, 4.0);
|
||||
brushed.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(StrokeLayer { brush: Some("s".into()), ..stroke(1.0) }));
|
||||
for (name, art) in [("plain", rect_art(10.0, 4.0)), ("brushed", brushed)] {
|
||||
doc.symbols.push(Symbol { name: name.into(), art: std::sync::Arc::new(art.clone()) });
|
||||
doc.patterns.push(PatternDef::new(name, vec![std::sync::Arc::new(art)]));
|
||||
}
|
||||
let instance = |symbol: &str| Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: symbol.into(), xf: vectorcraft_geom::Affine::IDENTITY });
|
||||
let painted = |pattern: &str| {
|
||||
let fill = Paint::Pattern { pattern: pattern.into(), xf: vectorcraft_geom::Affine::IDENTITY };
|
||||
Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 4.0)), Appearance::basic(fill, Paint::None, 0.0))
|
||||
};
|
||||
let art = Node::group(NodeId(0), [instance("plain"), instance("brushed"), painted("plain"), painted("brushed")].map(std::sync::Arc::new).into());
|
||||
let b = Brush { name: "s".into(), kind: BrushKind::Scatter(Scatter { art, colorization: Colorization::None, ..Default::default() }) };
|
||||
let out = stroke_pieces(&doc, &b, &line(0.0, 0.0, 100.0, 0.0), &stroke(1.0));
|
||||
assert!(!out.is_empty());
|
||||
for piece in &out {
|
||||
let children = piece.children().unwrap();
|
||||
let symbols: Vec<&str> = children
|
||||
.iter()
|
||||
.filter_map(|c| if let NodeKind::SymbolInstance { symbol, .. } = &c.kind { Some(symbol.as_str()) } else { None })
|
||||
.collect();
|
||||
assert_eq!(symbols, ["plain"]);
|
||||
let fills: Vec<Paint> =
|
||||
children.iter().filter(|c| c.path_data().is_some()).filter_map(|c| c.appearance.fill().map(|f| f.paint.clone())).collect();
|
||||
assert_eq!(fills, [Paint::Pattern { pattern: "plain".into(), xf: vectorcraft_geom::Affine::IDENTITY }, Paint::None]);
|
||||
}
|
||||
}
|
||||
@@ -506,7 +506,7 @@ impl<'a> Scene<'a> {
|
||||
let doc = self.doc;
|
||||
let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc));
|
||||
let b = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name))?;
|
||||
Some(vectorcraft_brush::stroke_pieces(b, bp, st))
|
||||
Some(vectorcraft_brush::stroke_pieces(doc, b, bp, st))
|
||||
}
|
||||
|
||||
/// A filled area as its outlines followed by a solid hatch (R12: the outlines alone). Laser
|
||||
|
||||
@@ -226,7 +226,7 @@ fn item_paint_mut(it: &mut AppearanceItem) -> &mut Paint {
|
||||
}
|
||||
|
||||
/// The paints a node holds itself (not its children): appearance items, then text runs.
|
||||
pub(crate) fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
|
||||
pub fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
|
||||
let runs = match &n.kind {
|
||||
NodeKind::Text(t) => t.runs.as_slice(),
|
||||
_ => &[],
|
||||
@@ -235,7 +235,7 @@ pub(crate) fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
|
||||
}
|
||||
|
||||
/// Mutable [`node_paints`], in the same order.
|
||||
pub(crate) fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
|
||||
pub fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
|
||||
let runs = match &mut n.kind {
|
||||
NodeKind::Text(t) => t.runs.as_mut_slice(),
|
||||
_ => &mut [],
|
||||
|
||||
@@ -594,7 +594,7 @@ pub(crate) fn outlined_stroke(
|
||||
st: &StrokeLayer,
|
||||
) -> Option<Node> {
|
||||
if let Some(b) = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name)) {
|
||||
let mut pieces = vectorcraft_brush::stroke_pieces(b, &path.to_bezpath(), st);
|
||||
let mut pieces = vectorcraft_brush::stroke_pieces(d, b, &path.to_bezpath(), st);
|
||||
let art = match pieces.len() {
|
||||
0 => return None,
|
||||
1 => pieces.remove(0),
|
||||
|
||||
@@ -2552,10 +2552,14 @@ fn deep_pdf(content: &str, resources: &str) -> Vec<u8> {
|
||||
|
||||
/// A stream object with the entries `dict`, its `data` compressed.
|
||||
fn packed(dict: &str, data: &str) -> Vec<u8> {
|
||||
let z = deflated(data.as_bytes());
|
||||
packed_as_is(dict, &deflated(data.as_bytes()))
|
||||
}
|
||||
|
||||
/// A stream object with the entries `dict`, its data `z` compressed already.
|
||||
fn packed_as_is(dict: &str, z: &[u8]) -> Vec<u8> {
|
||||
// (A length the test gives stands.)
|
||||
let length = if dict.contains("/Length") { String::new() } else { format!(" /Length {}", z.len()) };
|
||||
[format!("<< {dict} /Filter /FlateDecode{length} >>\nstream\n").as_bytes(), &z, b"\nendstream"].concat()
|
||||
[format!("<< {dict} /Filter /FlateDecode{length} >>\nstream\n").as_bytes(), z, b"\nendstream"].concat()
|
||||
}
|
||||
|
||||
/// A page whose content is `content`, with `resources` and the objects `more` (numbered from 5).
|
||||
@@ -2801,6 +2805,54 @@ fn pdf_color_spaces_that_hold_themselves() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Spaces past what a stream may decode to (256 MB), compressed (about 260 kB).
|
||||
fn spaces_past_the_cap() -> Vec<u8> {
|
||||
use std::io::Write as _;
|
||||
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
|
||||
for _ in 0..257 {
|
||||
e.write_all(&[b' '; 1 << 20]).unwrap();
|
||||
}
|
||||
e.finish().unwrap()
|
||||
}
|
||||
|
||||
// Streams the reader would fail on (#1700, #1701): it applies a predictor before checking its
|
||||
// parameters (`Columns 0` divides by zero, a huge `Columns` allocates a row that large), and
|
||||
// inflates without a ceiling.
|
||||
|
||||
#[test]
|
||||
fn pdf_streams_the_reader_would_fail_on_are_refused() {
|
||||
let zero = "/DecodeParms << /Predictor 2 /Columns 0 >>";
|
||||
let huge = "/DecodeParms << /Predictor 12 /Colors 4 /Columns 99999999999 >>";
|
||||
// An inline image whose data is deflated, written in hexadecimal so the content stays text.
|
||||
let inline = |dp: &str| {
|
||||
let data: String = deflated(&[0; 8]).iter().map(|b| format!("{b:02x}")).collect();
|
||||
format!("q 10 0 0 10 0 0 cm BI /W 2 /H 2 /CS /G /BPC 8 /F [/AHx /Fl] /DP [null {dp}] ID {data}> EI Q")
|
||||
};
|
||||
let form = |dict: &str, content: &str| pdf_with("/F Do", "<< /XObject << /F 5 0 R >> >>", &[packed(&format!("{FORM} {dict}"), content)]);
|
||||
let files = [
|
||||
("a form with Columns 0", form(zero, "0 0 5 5 re f")),
|
||||
("a form with a huge Columns", form(huge, "0 0 5 5 re f")),
|
||||
("an inline image with Columns 0", deep_pdf(&inline("<< /Predictor 2 /Columns 0 >>"), "<< >>")),
|
||||
("an inline image with a huge Columns", deep_pdf(&inline("<< /Predictor 15 /Columns 99999999999 >>"), "<< >>")),
|
||||
("an inline image in a compressed form", form("", &inline("<< /Predictor 2 /Columns 0 >>"))),
|
||||
("an object stream with Columns 0", pdf_with_object_stream("[0 0 100 100]", zero)),
|
||||
("a form of spaces past the cap", pdf_with("/F Do", "<< /XObject << /F 5 0 R >> >>", &[packed_as_is(FORM, &spaces_past_the_cap())])),
|
||||
];
|
||||
on_a_small_stack(move || {
|
||||
for (what, bytes) in &files {
|
||||
for name in ["hostile.pdf", "hostile.ai"] {
|
||||
let e = vectorcraft_engine::cmd::fileio::load(name, bytes).err().map(|e| e.to_string());
|
||||
assert!(e.as_deref().is_some_and(|e| e.contains("would decode to more than 256 MB")), "{what} as {name}: {e:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
// Within range, the same files open.
|
||||
let fine = "/DecodeParms << /Predictor 1 /Columns 0 >>";
|
||||
for bytes in [form(fine, "0 0 5 5 re f"), deep_pdf(&inline("<< /Predictor 1 >>"), "<< >>")] {
|
||||
vectorcraft_engine::cmd::fileio::load("fine.pdf", &bytes).expect("opens");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_art_that_draws_itself() {
|
||||
let own = "<< /XObject << /F 5 0 R >> /Pattern << /P 5 0 R >> /Font << /F1 5 0 R >> /ExtGState << /G << /SMask << /S /Alpha /G 5 0 R >> >> >> >>";
|
||||
@@ -2881,6 +2933,188 @@ fn deeply_nested_dxf_blocks_are_read_within_the_cap() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dxf_blocks_inserting_themselves_are_left_out() {
|
||||
on_a_small_stack(|| {
|
||||
let pair = |c: i32, v: &str| (c, v.to_string());
|
||||
// A line, then an insert of `inserts`.
|
||||
let block = |name: &str, inserts: &str| {
|
||||
let line = [pair(0, "LINE"), pair(10, "0"), pair(20, "0"), pair(11, "5"), pair(21, "5")];
|
||||
let insert = [pair(0, "INSERT"), pair(2, inserts), pair(10, "1"), pair(20, "1"), pair(0, "ENDBLK")];
|
||||
[pair(0, "BLOCK"), pair(2, name), pair(10, "0"), pair(20, "0")].into_iter().chain(line).chain(insert).collect::<Vec<_>>()
|
||||
};
|
||||
let mut pairs = vec![pair(0, "SECTION"), pair(2, "BLOCKS")];
|
||||
pairs.extend([block("SELF", "SELF"), block("A", "B"), block("B", "A")].concat());
|
||||
pairs.extend([pair(0, "ENDSEC"), pair(0, "SECTION"), pair(2, "ENTITIES")]);
|
||||
for name in ["SELF", "A"] {
|
||||
pairs.extend([pair(0, "INSERT"), pair(2, name), pair(10, "0"), pair(20, "0")]);
|
||||
}
|
||||
pairs.extend([pair(0, "ENDSEC"), pair(0, "EOF")]);
|
||||
let (_, warnings) = open_deep("self.dxf", dxf_text(&pairs).as_bytes()).expect("opens");
|
||||
assert!(warnings.iter().any(|w| w.contains("inside themselves")), "{warnings:?}");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeply_nested_affinity_groups_are_read_within_the_cap() {
|
||||
use vectorcraft_affinity::synth::{self, F, Method, tag};
|
||||
// Groups in groups: within what the reader takes, past its cap on layers (128), past its cap
|
||||
// on the stream's own nesting (384), and far past both.
|
||||
let files: Vec<Vec<u8>> = [20u32, 120, 180, 370, 2000]
|
||||
.into_iter()
|
||||
.map(|levels| {
|
||||
let group = |id: u32, fields| F::Def(id, vec![tag(b"Grup")], fields);
|
||||
let inner = (0..levels).fold(group(10, vec![]), |inner, i| group(11 + i, vec![(tag(b"Chld"), F::Shared(vec![inner]))]));
|
||||
let spread = F::Def(2, vec![tag(b"Sprd")], vec![(tag(b"Chld"), F::Shared(vec![inner]))]);
|
||||
let stream = synth::stream(&[(tag(b"DocR"), F::Def(1, vec![tag(b"DocN")], vec![(tag(b"Chld"), F::Shared(vec![spread]))]))]);
|
||||
affinity_native(&stream, Method::Stored)
|
||||
})
|
||||
.collect();
|
||||
on_a_small_stack(move || {
|
||||
for bytes in &files {
|
||||
open_deep("deep.afdesign", bytes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metafiles_saving_states_without_end_are_read_within_the_cap() {
|
||||
on_a_small_stack(|| {
|
||||
// A saved state, a clip and a rectangle, over and over (nothing is ever restored).
|
||||
let longs = |v: [i32; 4]| v.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>();
|
||||
let emf: Vec<(u32, Vec<u8>)> = (0..50_000).flat_map(|_| [(33, vec![]), (30, longs([0, 0, 500, 500])), (43, longs([0, 0, 50, 50]))]).collect();
|
||||
open_deep("deep.emf", &emf_of(&emf));
|
||||
let shorts = |v: [i16; 4]| v.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>();
|
||||
let wmf: Vec<(u16, Vec<u8>)> =
|
||||
(0..20_000).flat_map(|_| [(0x001E, vec![]), (0x0416, shorts([500, 500, 0, 0])), (0x041B, shorts([50, 50, 0, 0]))]).collect();
|
||||
open_deep("deep.wmf", &wmf_of(&wmf));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeply_nested_plugin_manifests_and_output_are_errors() {
|
||||
on_a_small_stack(|| {
|
||||
// (A manifest may be 64 kB.)
|
||||
let deep = |n: usize| format!("{}{}", "[".repeat(n), "]".repeat(n));
|
||||
assert!(vectorcraft_plugins::Manifest::parse(deep(30_000).as_bytes()).is_err());
|
||||
assert!(vectorcraft_plugins::Manifest::parse(format!("{{\"params\": {}}}", deep(30_000)).as_bytes()).is_err());
|
||||
let deep = deep(100_000);
|
||||
for output in [deep.clone(), format!("{{\"objects\": {deep}}}"), format!("{{\"objects\": [{{\"kind\": \"path\", \"path\": {deep}}}]}}")] {
|
||||
assert!(vectorcraft_plugins::objects::decode(output.as_bytes()).is_err());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A document whose symbol `s0` and pattern `p0` are each a line stroked with the brush `loop`,
|
||||
/// with an instance of the symbol and a square painted with the pattern on the layer. The brush
|
||||
/// is the `kind`th of those that hold art (art, scatter, pattern), and its art is the `art`th of:
|
||||
/// an instance of the symbol; that instance inside as many groups as a file holds; a square
|
||||
/// painted with the pattern. So what the brush draws is drawn with the brush.
|
||||
fn brushed_document(kind: usize, art: usize) -> Document {
|
||||
use vectorcraft_brush::{ArtBrush, Brush, BrushKind, PatternBrush, Scatter};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Node, NodeKind, PatternDef, Symbol};
|
||||
let mut doc = nested_document(1);
|
||||
let square = || vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 60.0, 20.0));
|
||||
let brushed = |doc: &mut Document| {
|
||||
let mut look = Appearance::basic(Paint::None, Paint::solid(vectorcraft_color::Color::rgb8(0, 0, 0)), 4.0);
|
||||
for item in &mut look.items {
|
||||
if let AppearanceItem::Stroke(s) = item {
|
||||
s.brush = Some("loop".into());
|
||||
}
|
||||
}
|
||||
Node::path(doc.alloc_id(), square(), look)
|
||||
};
|
||||
let instance =
|
||||
|doc: &mut Document| Node::new(doc.alloc_id(), NodeKind::SymbolInstance { symbol: "s0".into(), xf: vectorcraft_geom::Affine::IDENTITY });
|
||||
let painted = |doc: &mut Document| {
|
||||
let fill = Paint::Pattern { pattern: "p0".into(), xf: vectorcraft_geom::Affine::IDENTITY };
|
||||
Node::path(doc.alloc_id(), square(), Appearance::basic(fill, Paint::None, 0.0))
|
||||
};
|
||||
let (in_symbol, in_pattern) = (brushed(&mut doc), brushed(&mut doc));
|
||||
doc.symbols.push(Symbol { name: "s0".into(), art: std::sync::Arc::new(in_symbol) });
|
||||
doc.patterns.push(PatternDef::new("p0", vec![std::sync::Arc::new(in_pattern)]));
|
||||
let art = match art {
|
||||
0 => instance(&mut doc),
|
||||
1 => (0..MAX_DEPTH + 4).fold(instance(&mut doc), |inner, _| Node::group(doc.alloc_id(), vec![std::sync::Arc::new(inner)])),
|
||||
_ => painted(&mut doc),
|
||||
};
|
||||
let kind = match kind {
|
||||
0 => BrushKind::Art(ArtBrush { art, ..Default::default() }),
|
||||
1 => BrushKind::Scatter(Scatter { art, ..Default::default() }),
|
||||
_ => BrushKind::Pattern(PatternBrush { side: art.clone(), start: Some(art), ..Default::default() }),
|
||||
};
|
||||
vectorcraft_brush::store(&mut doc, &[Brush { name: "loop".into(), kind }]);
|
||||
let layer = doc.layers[0].id;
|
||||
for on_layer in [instance(&mut doc), painted(&mut doc), brushed(&mut doc)] {
|
||||
doc.insert(Some(layer), usize::MAX, on_layer).unwrap();
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
/// `doc` saved, opened again, drawn, exported and edited: nothing overflows.
|
||||
fn art_by_name_survives(what: &str, doc: Document) {
|
||||
let (back, _) = open_deep("named.vectorcraft", &vectorcraft_format::save_file(&doc)).expect("opens");
|
||||
let mut s = vectorcraft_engine::Session::new();
|
||||
s.add_document(back, None);
|
||||
for (cmd, p) in [
|
||||
("select.all", json!({})),
|
||||
("document.inspect", json!({})),
|
||||
("brush.expand", json!({})),
|
||||
("edit.undo", json!({})),
|
||||
("object.expand", json!({})),
|
||||
("edit.undo", json!({})),
|
||||
("object.outlineStroke", json!({})),
|
||||
("edit.undo", json!({})),
|
||||
("graph.design", json!({"paste": "deep"})),
|
||||
("graph.create", json!({"type": "line", "x": 10, "y": 10, "width": 80, "height": 60})),
|
||||
("graph.marker", json!({"design": "deep"})),
|
||||
("graph.design", json!({"new": "again"})),
|
||||
("edit.copy", json!({})),
|
||||
("edit.paste", json!({})),
|
||||
] {
|
||||
let _ = no_panic(s.execute(cmd, &p));
|
||||
assert!(!s.doc().unwrap().doc.too_deep(None), "{what}: {cmd}");
|
||||
}
|
||||
for format in ["svg", "pdf", "eps", "png", "dxf", "emf"] {
|
||||
let _ = no_panic(s.execute("document.export", &json!({"format": format})));
|
||||
}
|
||||
}
|
||||
|
||||
/// A test for each brush whose art is drawn with the brush itself ([`brushed_document`]).
|
||||
macro_rules! brushes_drawn_with_themselves {
|
||||
($($test:ident: $kind:literal, $art:literal;)*) => {$(
|
||||
#[test]
|
||||
fn $test() {
|
||||
on_a_small_stack(|| art_by_name_survives(stringify!($test), brushed_document($kind, $art)));
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
brushes_drawn_with_themselves! {
|
||||
an_art_brush_of_its_own_symbol: 0, 0;
|
||||
an_art_brush_of_its_own_symbol_in_deep_art: 0, 1;
|
||||
an_art_brush_painted_with_its_own_pattern: 0, 2;
|
||||
a_scatter_brush_of_its_own_symbol: 1, 0;
|
||||
a_scatter_brush_of_its_own_symbol_in_deep_art: 1, 1;
|
||||
a_scatter_brush_painted_with_its_own_pattern: 1, 2;
|
||||
a_pattern_brush_of_its_own_symbol: 2, 0;
|
||||
a_pattern_brush_of_its_own_symbol_in_deep_art: 2, 1;
|
||||
a_pattern_brush_painted_with_its_own_pattern: 2, 2;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_graph_design_of_deep_art_and_of_a_symbol_holding_itself_is_drawn() {
|
||||
use vectorcraft_doc::{GraphDesign, Node, NodeKind};
|
||||
on_a_small_stack(|| {
|
||||
let mut doc = symbols_document(1, 1, "s0");
|
||||
let instance = Node::new(doc.alloc_id(), NodeKind::SymbolInstance { symbol: "s0".into(), xf: vectorcraft_geom::Affine::IDENTITY });
|
||||
let art = (0..MAX_DEPTH + 4).fold(instance, |inner, _| Node::group(doc.alloc_id(), vec![std::sync::Arc::new(inner)]));
|
||||
doc.graph_designs.push(GraphDesign { name: "deep".into(), art: std::sync::Arc::new(art) });
|
||||
art_by_name_survives("a graph design of deep art", doc);
|
||||
});
|
||||
}
|
||||
|
||||
/// A layer holding `levels` groups in one another around a rectangle, each with a second
|
||||
/// rectangle beside the next: `levels + 2` deep.
|
||||
fn nested_document(levels: usize) -> Document {
|
||||
|
||||
@@ -542,7 +542,7 @@ impl<'a> Scene<'a> {
|
||||
let doc = self.doc;
|
||||
let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc));
|
||||
let Some(b) = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name)) else { return false };
|
||||
let art = vectorcraft_brush::stroke_pieces(b, bp, st);
|
||||
let art = vectorcraft_brush::stroke_pieces(doc, b, bp, st);
|
||||
self.note_item(st.opacity, st.blend);
|
||||
self.nest += 1;
|
||||
for piece in &art {
|
||||
|
||||
@@ -403,7 +403,7 @@ impl<'a> Scene<'a> {
|
||||
let doc = self.doc;
|
||||
let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc));
|
||||
let b = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name))?;
|
||||
Some(vectorcraft_brush::stroke_pieces(b, bp, st))
|
||||
Some(vectorcraft_brush::stroke_pieces(doc, b, bp, st))
|
||||
}
|
||||
|
||||
/// A placed image, decoded once per image.
|
||||
|
||||
@@ -27,6 +27,7 @@ hayro-syntax = "0.8.0"
|
||||
hayro-interpret = { version = "0.8.0", default-features = false, features = ["embed-fonts", "embed-cmaps"] }
|
||||
log = { workspace = true }
|
||||
smallvec = "1"
|
||||
weezl = "0.1"
|
||||
serde = { workspace = true }
|
||||
# PDF security (encrypt.rs): pure Rust, MIT OR Apache-2.0, builds for wasm.
|
||||
aes = "0.8"
|
||||
|
||||
@@ -1336,7 +1336,7 @@ impl Exporter<'_> {
|
||||
let doc = self.doc;
|
||||
let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc));
|
||||
let Some(b) = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name)) else { return false };
|
||||
let art = vectorcraft_brush::stroke_pieces(b, bp, st);
|
||||
let art = vectorcraft_brush::stroke_pieces(doc, b, bp, st);
|
||||
let mut pushes = 0;
|
||||
if st.blend != BlendMode::Normal {
|
||||
s.push_blend_mode(blend(st.blend));
|
||||
|
||||
@@ -64,6 +64,7 @@ mod post;
|
||||
mod presets;
|
||||
mod print;
|
||||
mod settings;
|
||||
mod streams;
|
||||
mod syntax;
|
||||
|
||||
pub use editing::{EDITING_FILE, Editing, LEGACY_EDITING_FILE, editing, editing_with};
|
||||
|
||||
+23
-4
@@ -114,6 +114,21 @@ fn procedures_too_deep(o: &Object<'_>) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// The error of a file with a stream the reader would fail on ([`crate::streams`]).
|
||||
const STREAM_NOTE: &str = "a stream in it would decode to more than 256 MB, or its decode parameters can't be applied";
|
||||
|
||||
/// The error of a file [`nests_without_end`].
|
||||
const NESTED_NOTE: &str = "its functions or colour spaces are built on one another without end";
|
||||
|
||||
/// Why `pdf` is refused, if it is: a stream the reader would fail on (its own checks come first:
|
||||
/// the calculator functions are decoded below), or [`nests_without_end`].
|
||||
fn hostile(pdf: &Pdf) -> Option<&'static str> {
|
||||
if pdf.objects().into_iter().any(|o| matches!(&o, Object::Stream(s) if crate::streams::unsafe_stream(s))) {
|
||||
return Some(STREAM_NOTE);
|
||||
}
|
||||
nests_without_end(pdf).then_some(NESTED_NOTE)
|
||||
}
|
||||
|
||||
/// Does `pdf` hold functions or colour spaces built on one another past [`MAX_LINKS`] (or on
|
||||
/// themselves), or a calculator function nested past [`MAX_PROCEDURES`]? Every object is looked
|
||||
/// at, wherever it is used from: the interpreter builds them from resources, shadings, images,
|
||||
@@ -142,11 +157,15 @@ fn nests_without_end(pdf: &Pdf) -> bool {
|
||||
/// dictionaries 64 deep and no deeper, wherever they are: the file's text, an object stream, the
|
||||
/// content of a page, form, pattern or glyph; and follows a page tree or a chain of object streams
|
||||
/// once). What it reads beyond that is left out, as an object it can't read. What the interpreter
|
||||
/// builds by recursion with no cap is refused here ([`nests_without_end`]).
|
||||
/// builds by recursion with no cap, and streams it would decode without a ceiling or with a
|
||||
/// predictor it can't apply, are refused here ([`hostile`], [`crate::streams`]).
|
||||
pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError> {
|
||||
if is_postscript(bytes) {
|
||||
return Err(PdfError::PostScript);
|
||||
}
|
||||
if crate::streams::unsafe_file_streams(bytes) {
|
||||
return Err(PdfError::Parse(STREAM_NOTE.into()));
|
||||
}
|
||||
let password = password.unwrap_or_default();
|
||||
let read = |pw: &str| Pdf::new_with_password(bytes.to_vec(), pw);
|
||||
read(password)
|
||||
@@ -165,9 +184,9 @@ pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError
|
||||
})
|
||||
// Twice: looking at every object makes the reader repair a damaged table of objects (once
|
||||
// only), and the objects are other ones after that.
|
||||
.and_then(|pdf| match (0..2).any(|_| nests_without_end(&pdf)) {
|
||||
true => Err(PdfError::Parse("its functions or colour spaces are built on one another without end".into())),
|
||||
false => Ok(pdf),
|
||||
.and_then(|pdf| match (0..2).find_map(|_| hostile(&pdf)) {
|
||||
Some(why) => Err(PdfError::Parse(why.into())),
|
||||
None => Ok(pdf),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
//! Streams looked at before the PDF reader (hayro) decodes them. It inflates Flate and LZW data
|
||||
//! with no ceiling, so a few megabytes can ask for gigabytes (#1700), and it applies a predictor
|
||||
//! before checking its parameters: `Columns 0` divides by zero, a huge `Columns` allocates a row
|
||||
//! that large whatever the stream holds (#1701). A file with such a stream is refused instead.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::io::Read;
|
||||
|
||||
use hayro_syntax::content::TypedIter;
|
||||
use hayro_syntax::content::ops::TypedInstruction;
|
||||
use hayro_syntax::object::{Array, Dict, FromBytes, MaybeRef, Name, Object, Stream};
|
||||
|
||||
/// The most a stream may decode to: 256 MB, the pixels of the largest image the app reads (64
|
||||
/// million, 4 bytes each).
|
||||
pub(crate) const MAX_DECODED: usize = 256 << 20;
|
||||
|
||||
/// The longest row a predictor may have, in bytes.
|
||||
const MAX_ROW: usize = 1 << 24;
|
||||
|
||||
/// A stream that decodes to more than [`MAX_DECODED`].
|
||||
struct TooBig;
|
||||
|
||||
/// The filters of the stream with dictionary `dict`, by name (`Filter` or `F`: a name or an array).
|
||||
fn filters<'a>(dict: &Dict<'a>) -> Vec<Name<'a>> {
|
||||
[&b"Filter"[..], b"F"]
|
||||
.into_iter()
|
||||
.find_map(|key| dict.get::<Name<'a>>(key).map(|n| vec![n]).or_else(|| dict.get::<Array<'a>>(key).map(|a| a.iter::<Name<'a>>().collect())))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Every decode parameter dictionary of the stream with dictionary `dict`, as the reader may take
|
||||
/// them (`DecodeParms` or `DP`: a dictionary, or an array with one for each filter).
|
||||
fn params<'a>(dict: &Dict<'a>) -> Vec<Dict<'a>> {
|
||||
let mut all = vec![];
|
||||
for key in [&b"DecodeParms"[..], b"DP"] {
|
||||
all.extend(dict.get::<Dict<'a>>(key));
|
||||
all.extend(dict.get::<Array<'a>>(key).into_iter().flat_map(|a| a.iter::<Object<'a>>().filter_map(|o| o.into_dict()).collect::<Vec<_>>()));
|
||||
}
|
||||
all
|
||||
}
|
||||
|
||||
/// Would the reader's predictor fail on `params`? It reads `Colors` and `BitsPerComponent` as
|
||||
/// bytes and `Columns` as a size, multiplies the first two as bytes, divides by the length of a row
|
||||
/// and allocates one before it checks anything.
|
||||
fn bad_predictor(params: &Dict<'_>) -> bool {
|
||||
if params.get::<u8>(b"Predictor").unwrap_or(1) == 1 {
|
||||
return false;
|
||||
}
|
||||
let colors = usize::from(params.get::<u8>(b"Colors").unwrap_or(1));
|
||||
let bits = usize::from(params.get::<u8>(b"BitsPerComponent").unwrap_or(8));
|
||||
let columns = params.get::<usize>(b"Columns").unwrap_or(1);
|
||||
let pixel = colors * bits;
|
||||
let row = columns.checked_mul(pixel).map(|b| b.div_ceil(8));
|
||||
!(matches!(bits, 1 | 2 | 4 | 8 | 16) && colors > 0 && pixel <= 255 && columns > 0 && row.is_some_and(|r| r <= MAX_ROW))
|
||||
}
|
||||
|
||||
/// `data` inflated, up to one byte past [`MAX_DECODED`] (zlib, or raw deflate as the reader also
|
||||
/// takes); `None` when it is damaged before that.
|
||||
fn inflate(data: &[u8]) -> Option<Vec<u8>> {
|
||||
let read = |mut r: Box<dyn Read + '_>| {
|
||||
let mut out = vec![];
|
||||
// (Damaged data: what came out before the damage still counts.)
|
||||
let done = r.by_ref().take(MAX_DECODED as u64 + 1).read_to_end(&mut out).is_ok();
|
||||
(done || out.len() > MAX_DECODED).then_some(out)
|
||||
};
|
||||
read(Box::new(flate2::read::ZlibDecoder::new(data))).or_else(|| read(Box::new(flate2::read::DeflateDecoder::new(data))))
|
||||
}
|
||||
|
||||
/// `data` decoded from LZW, up to one byte past [`MAX_DECODED`]. `early`: the code length grows
|
||||
/// one code early (`EarlyChange`, 1 by default).
|
||||
fn lzw(data: &[u8], early: bool) -> Option<Vec<u8>> {
|
||||
use weezl::{BitOrder, LzwStatus, decode::Decoder};
|
||||
let mut decoder = if early { Decoder::with_tiff_size_switch(BitOrder::Msb, 8) } else { Decoder::new(BitOrder::Msb, 8) };
|
||||
let (mut out, mut chunk, mut input) = (vec![], vec![0; 1 << 16], data);
|
||||
while out.len() <= MAX_DECODED {
|
||||
let r = decoder.decode_bytes(input, &mut chunk);
|
||||
input = input.get(r.consumed_in..).unwrap_or_default();
|
||||
out.extend_from_slice(chunk.get(..r.consumed_out).unwrap_or_default());
|
||||
match r.status {
|
||||
Ok(LzwStatus::Ok) => {}
|
||||
Ok(LzwStatus::Done | LzwStatus::NoProgress) => break,
|
||||
Err(_) => return (out.len() > MAX_DECODED).then_some(out),
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// `data` decoded from run-length encoding, up to one byte past [`MAX_DECODED`].
|
||||
fn run_length(data: &[u8]) -> Vec<u8> {
|
||||
let (mut out, mut at) = (vec![], 0);
|
||||
while let Some(&n) = data.get(at)
|
||||
&& n != 128
|
||||
&& out.len() <= MAX_DECODED
|
||||
{
|
||||
if n < 128 {
|
||||
let take = usize::from(n) + 1;
|
||||
out.extend_from_slice(data.get(at + 1..).unwrap_or_default().get(..take).unwrap_or_default());
|
||||
at += 1 + take;
|
||||
} else {
|
||||
out.extend(std::iter::repeat_n(data.get(at + 1).copied().unwrap_or(0), 257 - usize::from(n)));
|
||||
at += 2;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `data` decoded from hexadecimal digits (white space skipped, up to `>`).
|
||||
fn hex(data: &[u8]) -> Vec<u8> {
|
||||
let digits: Vec<u8> = data.iter().take_while(|c| **c != b'>').filter_map(|c| char::from(*c).to_digit(16)).map(|d| d as u8).collect();
|
||||
digits.chunks(2).map(|p| (p.first().copied().unwrap_or(0) << 4) | p.get(1).copied().unwrap_or(0)).collect()
|
||||
}
|
||||
|
||||
/// `data` decoded from base-85 (white space skipped, `z` for four zeros, up to `~>`).
|
||||
fn base85(data: &[u8]) -> Vec<u8> {
|
||||
let (mut out, mut group) = (vec![], vec![]);
|
||||
let flush = |group: &mut Vec<u32>, out: &mut Vec<u8>| {
|
||||
let n = group.len();
|
||||
if n > 1 {
|
||||
group.resize(5, 84);
|
||||
let v = group.iter().fold(0u32, |v, d| v.wrapping_mul(85).wrapping_add(*d));
|
||||
out.extend_from_slice(v.to_be_bytes().get(..n - 1).unwrap_or_default());
|
||||
}
|
||||
group.clear();
|
||||
};
|
||||
for &c in data.iter().take_while(|c| **c != b'~') {
|
||||
match c {
|
||||
b'z' if group.is_empty() => out.extend([0; 4]),
|
||||
b'!'..=b'u' => {
|
||||
group.push(u32::from(c - b'!'));
|
||||
if group.len() == 5 {
|
||||
flush(&mut group, &mut out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
flush(&mut group, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// The most data of `len` bytes can grow to through `filters`: 1,032 times through Flate (its
|
||||
/// largest ratio), 3,641 through LZW (4,096 bytes from a 9-bit code), 64 through run-length.
|
||||
fn most_after(len: usize, filters: &[Name<'_>]) -> u128 {
|
||||
filters.iter().fold(len as u128, |n, f| {
|
||||
n.saturating_mul(match f.as_ref() {
|
||||
b"FlateDecode" | b"Fl" => 1032,
|
||||
b"LZWDecode" | b"LZW" => 3641,
|
||||
b"RunLengthDecode" | b"RL" => 64,
|
||||
_ => 1,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// `data` (of the stream with dictionary `dict`) through its filters, as far as this reads them:
|
||||
/// Flate, LZW, run-length and the ASCII encodings. Not decoded when it can't decode past
|
||||
/// [`MAX_DECODED`] and `keep` doesn't ask for it, or when an image codec or damaged data comes
|
||||
/// first (an image's own decoder knows its size).
|
||||
fn decode(data: &[u8], dict: &Dict<'_>, keep: bool) -> Result<Option<Vec<u8>>, TooBig> {
|
||||
let filters = filters(dict);
|
||||
if !keep && most_after(data.len(), &filters) <= MAX_DECODED as u128 {
|
||||
return Ok(None);
|
||||
}
|
||||
let params = params(dict);
|
||||
let mut data = Cow::Borrowed(data);
|
||||
for (i, f) in filters.iter().enumerate() {
|
||||
let early = params.get(i).and_then(|p| p.get::<u8>(b"EarlyChange")).is_none_or(|e| e != 0);
|
||||
let out = match f.as_ref() {
|
||||
b"FlateDecode" | b"Fl" => inflate(&data),
|
||||
b"LZWDecode" | b"LZW" => lzw(&data, early),
|
||||
b"RunLengthDecode" | b"RL" => Some(run_length(&data)),
|
||||
b"ASCIIHexDecode" | b"AHx" => Some(hex(&data)),
|
||||
b"ASCII85Decode" | b"A85" => Some(base85(&data)),
|
||||
_ => None,
|
||||
};
|
||||
match out {
|
||||
Some(out) if out.len() > MAX_DECODED => return Err(TooBig),
|
||||
Some(out) => data = Cow::Owned(out),
|
||||
None => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(Some(data.into_owned()))
|
||||
}
|
||||
|
||||
/// May the stream with dictionary `dict` be content (of a page, form, pattern or glyph), which can
|
||||
/// hold inline images? Not an image, font, profile, function, metadata or object stream.
|
||||
fn maybe_content(dict: &Dict<'_>) -> bool {
|
||||
let named = |key: &[u8], ok: &[&[u8]]| dict.get::<Name<'_>>(key).is_none_or(|n| ok.contains(&n.as_ref()));
|
||||
named(b"Subtype", &[b"Form"])
|
||||
&& named(b"Type", &[b"XObject", b"Pattern"])
|
||||
&& ![&b"Length1"[..], b"Length2", b"Length3", b"FunctionType", b"N"].iter().any(|k| dict.contains_key(k))
|
||||
}
|
||||
|
||||
/// Would the reader fail on `stream` (or on an inline image in it, when it is content): a
|
||||
/// predictor it can't apply, or more than [`MAX_DECODED`] decoded?
|
||||
pub(crate) fn unsafe_stream(stream: &Stream<'_>) -> bool {
|
||||
unsafe_data(stream, true)
|
||||
}
|
||||
|
||||
/// [`unsafe_stream`], looking for inline images in it when `inside` (not in an inline image's own
|
||||
/// data: it would be read again for each inline image a hostile one nests).
|
||||
fn unsafe_data(stream: &Stream<'_>, inside: bool) -> bool {
|
||||
let dict = stream.dict();
|
||||
if params(dict).iter().any(bad_predictor) {
|
||||
return true;
|
||||
}
|
||||
let content = inside && maybe_content(dict);
|
||||
match decode(&stream.raw_data(), dict, content) {
|
||||
Err(TooBig) => true,
|
||||
Ok(Some(data)) if content && data.windows(2).any(|w| w == b"ID") => {
|
||||
let mut ops = TypedIter::new(&data);
|
||||
while let Some(op) = ops.next() {
|
||||
if let TypedInstruction::InlineImage(image) = op
|
||||
&& unsafe_data(image.0, false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Ok(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Would the reader fail on a stream it decodes while it opens `pdf`, before anything else can
|
||||
/// look at it: an object stream or a cross-reference stream (read here from the file's text)?
|
||||
/// Their decode parameters must be in their dictionary, as the reader can't be asked for others
|
||||
/// yet. Encrypted object streams are looked at once the file is open (their data is ciphered
|
||||
/// here).
|
||||
pub(crate) fn unsafe_file_streams(pdf: &[u8]) -> bool {
|
||||
let find = |from: usize, what: &[u8]| pdf.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at);
|
||||
let mut from = 0;
|
||||
while let Some(at) = find(from, b"stream") {
|
||||
from = at + 6;
|
||||
if at >= 3 && pdf.get(at - 3..at) == Some(b"end") {
|
||||
continue;
|
||||
}
|
||||
// The dictionary before it: back from its `>>` to the `<<` that opens it.
|
||||
let Some(close) = pdf.get(..at).and_then(|head| head.iter().rposition(|c| !c.is_ascii_whitespace())).map(|i| i + 1) else { continue };
|
||||
if close < 2 || pdf.get(close - 2..close) != Some(b">>") {
|
||||
continue;
|
||||
}
|
||||
let (mut depth, mut open, mut i) = (0usize, None, close);
|
||||
while i >= 2 && close - i < 1 << 16 {
|
||||
match pdf.get(i - 2..i) {
|
||||
Some(b">>") => (depth, i) = (depth + 1, i - 2),
|
||||
Some(b"<<") => {
|
||||
depth -= 1;
|
||||
i -= 2;
|
||||
if depth == 0 {
|
||||
open = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => i -= 1,
|
||||
}
|
||||
}
|
||||
let Some(dict) = open.and_then(|o| pdf.get(o..close)).and_then(Dict::from_bytes) else { continue };
|
||||
if !dict.get::<Name<'_>>(b"Type").is_some_and(|t| matches!(t.as_ref(), b"ObjStm" | b"XRef")) {
|
||||
continue;
|
||||
}
|
||||
let indirect = [&b"DecodeParms"[..], b"DP"].iter().any(|k| matches!(dict.get_raw::<Object<'_>>(k), Some(MaybeRef::Ref(_))));
|
||||
if indirect || params(&dict).iter().any(bad_predictor) {
|
||||
return true;
|
||||
}
|
||||
let start = from
|
||||
+ usize::from(pdf.get(from) == Some(&b'\r'))
|
||||
+ usize::from(pdf.get(from + usize::from(pdf.get(from) == Some(&b'\r'))) == Some(&b'\n'));
|
||||
let end = dict
|
||||
.get::<usize>(b"Length")
|
||||
.and_then(|n| start.checked_add(n))
|
||||
.filter(|e| *e <= pdf.len())
|
||||
.or_else(|| find(start, b"endstream"))
|
||||
.unwrap_or(pdf.len());
|
||||
if matches!(decode(pdf.get(start..end).unwrap_or_default(), &dict, false), Err(TooBig)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn dict(text: &str) -> Dict<'_> {
|
||||
Dict::from_bytes(text.as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn predictors_the_reader_can_apply_pass() {
|
||||
for ok in [
|
||||
"<< >>",
|
||||
"<< /Predictor 1 /Columns 0 >>",
|
||||
"<< /Predictor 12 /Columns 5 >>",
|
||||
"<< /Predictor 2 /Colors 3 /BitsPerComponent 16 /Columns 1000 >>",
|
||||
] {
|
||||
assert!(!bad_predictor(&dict(ok)), "{ok}");
|
||||
}
|
||||
for bad in [
|
||||
"<< /Predictor 2 /Columns 0 >>",
|
||||
"<< /Predictor 12 /Colors 0 >>",
|
||||
"<< /Predictor 2 /BitsPerComponent 0 >>",
|
||||
"<< /Predictor 2 /BitsPerComponent 3 >>",
|
||||
"<< /Predictor 12 /Colors 32 /BitsPerComponent 16 >>",
|
||||
"<< /Predictor 15 /Columns 99999999999 >>",
|
||||
"<< /Predictor 10 /Columns 18446744073709551615 /Colors 4 >>",
|
||||
] {
|
||||
assert!(bad_predictor(&dict(bad)), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_decoders_read_what_the_specification_writes() {
|
||||
assert_eq!(hex(b"48 65 6c6C6\n>junk"), b"Hell`");
|
||||
assert_eq!(base85(b"87cURD]i,\"Ebo80~>"), b"Hello World!");
|
||||
assert_eq!(base85(b"z!!~>"), [0, 0, 0, 0, 0]);
|
||||
assert_eq!(run_length(&[2, b'a', b'b', b'c', 254, b'x', 128, b'q']), b"abcxxx");
|
||||
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
std::io::Write::write_all(&mut e, &[7; 1000]).unwrap();
|
||||
assert_eq!(inflate(&e.finish().unwrap()).unwrap(), [7; 1000]);
|
||||
// "-----A---B" in LZW, as the specification's example writes it (7.4.4.2).
|
||||
let lzw_example = [0x80, 0x0B, 0x60, 0x50, 0x22, 0x0C, 0x0C, 0x85, 0x01];
|
||||
assert_eq!(lzw(&lzw_example, true).unwrap(), b"-----A---B");
|
||||
}
|
||||
|
||||
/// Zeros past the cap, compressed.
|
||||
fn zeros_past_the_cap() -> Vec<u8> {
|
||||
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::best());
|
||||
for _ in 0..(MAX_DECODED >> 20) + 1 {
|
||||
std::io::Write::write_all(&mut e, &[0; 1 << 20]).unwrap();
|
||||
}
|
||||
e.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stream_that_decodes_past_the_cap_is_found_without_holding_it() {
|
||||
// Inflated in pieces: never more than one byte past the cap is held.
|
||||
let bomb = zeros_past_the_cap();
|
||||
assert!(bomb.len() < MAX_DECODED / 500);
|
||||
assert_eq!(inflate(&bomb).map(|d| d.len()), Some(MAX_DECODED + 1));
|
||||
let d = dict("<< /Filter /FlateDecode >>");
|
||||
assert!(matches!(decode(&bomb, &d, false), Err(TooBig)));
|
||||
// Small enough that it can't get there: not decoded at all.
|
||||
assert!(matches!(decode(&[1, 2, 3], &d, false), Ok(None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_and_cross_reference_streams_are_looked_at_before_the_file_is_read() {
|
||||
let stream = |dict: &str, data: &[u8]| {
|
||||
[format!("1 0 obj\n<< {dict} /Length {} >>\nstream\n", data.len()).as_bytes(), data, b"\nendstream\nendobj\n"].concat()
|
||||
};
|
||||
for bad in [
|
||||
stream("/Type /ObjStm /N 1 /First 4 /Filter /FlateDecode /DecodeParms << /Predictor 2 /Columns 0 >>", b"x"),
|
||||
stream("/Type /XRef /Filter /FlateDecode /DecodeParms 9 0 R", b"x"),
|
||||
stream("/Type /ObjStm /N 1 /First 4 /Filter /FlateDecode", &zeros_past_the_cap()),
|
||||
] {
|
||||
assert!(unsafe_file_streams(&bad));
|
||||
}
|
||||
for fine in [
|
||||
stream("/Type /XRef /Filter /FlateDecode /DecodeParms << /Predictor 12 /Columns 5 >>", b"x"),
|
||||
// Another kind of stream is looked at once the file is open, its references followed.
|
||||
stream("/Filter /FlateDecode /DecodeParms << /Predictor 2 /Columns 0 >>", b"x"),
|
||||
] {
|
||||
assert!(!unsafe_file_streams(&fine));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ impl Renderer {
|
||||
let pieces = match self.brushes.entries.get(&key, stamp) {
|
||||
Some(e) if Arc::ptr_eq(&e.brush, &brush) && e.stroke == *st && e.path == *bp => e.pieces.clone(),
|
||||
_ => {
|
||||
let pieces = Arc::new(vectorcraft_brush::stroke_pieces(&brush, bp, st));
|
||||
let pieces = Arc::new(vectorcraft_brush::stroke_pieces(f.doc, &brush, bp, st));
|
||||
let bytes = size_of::<Entry>() + lru::path_bytes(bp) + pieces.iter().map(lru::node_bytes).sum::<usize>();
|
||||
self.brushes.entries.insert(key, Entry { path: bp.clone(), stroke: st.clone(), brush, pieces: pieces.clone() }, bytes, stamp);
|
||||
pieces
|
||||
|
||||
@@ -917,7 +917,7 @@ impl Writer<'_> {
|
||||
let doc = self.doc;
|
||||
let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc));
|
||||
match st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name)) {
|
||||
Some(b) => StrokePlan::Brush(vectorcraft_brush::stroke_pieces(b, bp, st)),
|
||||
Some(b) => StrokePlan::Brush(vectorcraft_brush::stroke_pieces(doc, b, bp, st)),
|
||||
None => StrokePlan::Written(stroke::for_writer(bp, st)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user