mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 09:29:44 -06:00
.ai import: a line width of 0 draws as a hairline
Squashed from #5174 onto main.
This commit is contained in:
@@ -220,6 +220,24 @@ fn a_one_bit_rgb_image_opens_from_the_editing_data_in_its_colors() {
|
||||
assert_eq!(found, [[[255, 0, 0], [0, 255, 0], [0, 0, 255]]], "{r}");
|
||||
}
|
||||
|
||||
/// A stroke of width 0 (`0 w`) in the editing data is the thinnest line the device draws: the
|
||||
/// opened `.ai` shows a hairline where the same path without its stroke shows nothing.
|
||||
#[test]
|
||||
fn a_zero_width_stroke_in_the_editing_data_draws_a_hairline() {
|
||||
let drawn = |art: &str| {
|
||||
let data = ai::editing_data(200.0, 100.0, &ai::layer("Art", art));
|
||||
let file = ai::ai(&compressed(data.as_bytes()), "");
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "zero.ai", &file, json!({})).unwrap();
|
||||
assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "{r}");
|
||||
let v = s.execute("document.export", &json!({"format": "png", "background": "transparent"})).unwrap();
|
||||
let png = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
|
||||
image::load_from_memory(&png).unwrap().to_rgba8().pixels().filter(|p| p.0[3] > 0).count()
|
||||
};
|
||||
assert!(drawn("0 1 1 0 1 0 0 XA\n0 w\n20 20 m\n180 80 L\nS\n") > 0, "the hairline draws");
|
||||
assert_eq!(drawn("0 1 1 0 1 0 0 XA\n0 w\n20 20 m\n180 80 L\nN\n"), 0, "the unstroked path draws nothing");
|
||||
}
|
||||
|
||||
/// A page of a square and a line of type, as a `.ai`'s PDF part draws them.
|
||||
const SQUARE_AND_TYPE: &str = "0 g 10 10 40 40 re f BT /F1 12 Tf 100 50 Td (Hi) Tj ET";
|
||||
|
||||
|
||||
@@ -3186,6 +3186,28 @@ proptest! {
|
||||
prop_assert!(!opened);
|
||||
}
|
||||
|
||||
/// A `.ai` whose strokes have any width (0, which draws a hairline, negative, huge, not a number) on a
|
||||
/// path, on a gradient-filled path that `1 BB` strokes and on type in any rendering mode,
|
||||
/// damaged or hostile.
|
||||
#[test]
|
||||
fn ai_line_widths_never_panic(
|
||||
width in prop::sample::select(vec!["0", "0.0", "-0", "-1", "1e308", "1e-308", "nan", "inf", "2"]),
|
||||
render in prop::sample::select(vec!["0", "1", "2", "5", "6", "7", "-1", "99"]),
|
||||
cut in 0usize..2_000,
|
||||
edits in prop::collection::vec((0usize..2_000, prop::sample::select(vec!['0', '1', '-', '.', ' ', '\n', '(', ')', 'w', 'B', 'S', 'T'])), 0..8),
|
||||
) {
|
||||
use vectorcraft_testkit::ai;
|
||||
let gradient = "%AI5_BeginGradient: (G)\n(G) 0 2 Bd\n[\n<00FF>\n0\n%_0 0 0 0 1 0 0 2 1 6 50 0 Bs\n%_0 0 0 0 0 0 1 2 1 6 50 100 Bs\nBD\n%AI5_EndGradient\n";
|
||||
let art = format!(
|
||||
"0 1 1 0 1 0 0 XA\n{width} w\n10 10 m\n90 90 L\nS\n{}Bb\n1 (G) 0 0 0 1 1 0 0 1 0 0 1 Bg\nf\n1 BB\n\
|
||||
0 To\n1 0 0 1 10 50 0 Tp\nTP\n{render} Tr\n/_Helvetica 9 Tf\n(hi) Tx\nTO\n",
|
||||
ai::rect(20.0, 20.0, 80.0, 80.0)
|
||||
);
|
||||
let data = mutate_text(&ai::editing_data(100.0, 100.0, &format!("{gradient}{}", ai::layer("L", &art))), cut, &edits);
|
||||
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&data)].concat();
|
||||
survive("hostile .ai line widths", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?;
|
||||
}
|
||||
|
||||
/// A `.ai` whose variable library is damaged or hostile: the library is counted for the warning
|
||||
/// whether the layers are read or not (an inserted `Z` can make an operator the reader doesn't
|
||||
/// handle, and then the layers aren't read).
|
||||
|
||||
@@ -328,7 +328,7 @@ impl Reader<'_> {
|
||||
};
|
||||
style.fill = if fill { gs.fill.clone() } else { Paint::None };
|
||||
style.stroke = if stroke { gs.stroke.clone() } else { Paint::None };
|
||||
style.stroke_width = if stroke { gs.width } else { 0.0 };
|
||||
style.stroke_width = if stroke { gs.line_width() } else { 0.0 };
|
||||
(style.stroke_cap, style.stroke_join, style.stroke_miter_limit, style.stroke_dash) = (gs.cap, gs.join, gs.miter, gs.dash.clone());
|
||||
(style.overprint_fill, style.overprint_stroke) = (gs.overprint_fill, gs.overprint_stroke);
|
||||
let mut text = String::new();
|
||||
|
||||
@@ -80,6 +80,7 @@ use vectorcraft_doc::{
|
||||
use vectorcraft_geom::{AnchorKind, FillRule, PathData};
|
||||
|
||||
use super::super::ate::CANVAS_CENTRE;
|
||||
use super::super::graphics::HAIRLINE;
|
||||
use super::lex::{Lexer, Tok};
|
||||
use super::obj::{self, Obj, V};
|
||||
use super::paint::{self, GradientDef, Instance, Named};
|
||||
|
||||
@@ -202,7 +202,7 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
// A clipping path keeps its stroke, which paints over the art it clips.
|
||||
if stroke {
|
||||
let mut st = StrokeLayer::new(stroke_paint.unwrap_or(gs.stroke.clone()), gs.width);
|
||||
let mut st = StrokeLayer::new(stroke_paint.unwrap_or(gs.stroke.clone()), gs.line_width());
|
||||
st.cap = gs.cap;
|
||||
st.join = gs.join;
|
||||
st.miter_limit = gs.miter;
|
||||
@@ -334,7 +334,7 @@ impl<'a> Reader<'a> {
|
||||
if !matches!(n.kind, NodeKind::Path { .. }) || n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(_))) {
|
||||
return;
|
||||
}
|
||||
let mut st = StrokeLayer::new(gs.stroke.clone(), gs.width);
|
||||
let mut st = StrokeLayer::new(gs.stroke.clone(), gs.line_width());
|
||||
st.cap = gs.cap;
|
||||
st.join = gs.join;
|
||||
st.miter_limit = gs.miter;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::containers::close;
|
||||
use super::document::slot_of;
|
||||
use super::types::{BLENDS, FLIP, Frame, GRAPH_OPS, GRAPH_PLAIN, GRAPH_UNENDED, IGNORED, Kind, MAX_DEPTH};
|
||||
use super::types::{BLENDS, FLIP, Frame, GRAPH_OPS, GRAPH_PLAIN, GRAPH_UNENDED, IGNORED, Kind, MAX_DEPTH, line_width};
|
||||
use super::{
|
||||
Affine, Appearance, AppearanceItem, Arc, BlendMode, Dash, FillLayer, FillRule, Gradient, GradientDef, Graph, HashMap, HashSet, Instance,
|
||||
Knockout, LayerColor, LineCap, LineJoin, Node, NodeId, NodeKind, Paint, Reader, Rect, StrokeLayer, V, obj, paint,
|
||||
@@ -753,7 +753,7 @@ fn styled(object: &Node, look: &Node, st: &crate::import::ai::style::ArtStyle, p
|
||||
}
|
||||
},
|
||||
StyleItem::Stroke { paint, width, cap, join, miter, dash, profile } => {
|
||||
let mut layer = StrokeLayer::new(paint.clone(), *width);
|
||||
let mut layer = StrokeLayer::new(paint.clone(), line_width(*width));
|
||||
(layer.cap, layer.join, layer.miter_limit, layer.dash, layer.profile) = (*cap, *join, *miter, dash.clone(), profile.clone());
|
||||
AppearanceItem::Stroke(layer)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Affine, BlendMode, Dash, FillRule, Knockout, LayerColor, LineCap, LineJoin, Node, Obj, Paint, Rect, V};
|
||||
use super::{Affine, BlendMode, Dash, FillRule, HAIRLINE, Knockout, LayerColor, LineCap, LineJoin, Node, Obj, Paint, Rect, V};
|
||||
|
||||
/// Deepest nesting of layers, groups and dictionaries.
|
||||
pub(super) const MAX_DEPTH: usize = 256;
|
||||
@@ -96,6 +96,18 @@ impl Default for GState {
|
||||
}
|
||||
}
|
||||
|
||||
impl GState {
|
||||
/// The width its stroke draws at (see [`line_width`]).
|
||||
pub(crate) fn line_width(&self) -> f64 {
|
||||
line_width(self.width)
|
||||
}
|
||||
}
|
||||
|
||||
/// The width a stroke of `w w` draws at: `0 w` is the thinnest line a device draws, a [`HAIRLINE`].
|
||||
pub(super) fn line_width(w: f64) -> f64 {
|
||||
if w > 0.0 { w } else { HAIRLINE }
|
||||
}
|
||||
|
||||
/// The blend modes by their number in `Xy`.
|
||||
pub(super) const BLENDS: [BlendMode; 16] = [
|
||||
BlendMode::Normal,
|
||||
|
||||
@@ -350,6 +350,51 @@ fn a_segment_that_isnt_read_leaves_the_kinds_in_step() {
|
||||
assert_eq!(anchor_kinds(&a[0]), [vec![Corner], vec![Corner, Smooth, Corner]]);
|
||||
}
|
||||
|
||||
/// `0 w` is the thinnest line the device draws (PostScript's `setlinewidth`), so a stroke of width 0
|
||||
/// comes in as a hairline: on a path, on a gradient-filled path that `1 BB` strokes, and on type.
|
||||
#[test]
|
||||
fn a_line_width_of_zero_is_a_hairline() {
|
||||
let red = Some(Color::rgb(1.0, 0.0, 0.0));
|
||||
let a = art(&read_ok(&layer("0 1 1 0 1 0 0 XA\n2 J\n0 w\n20 20 m\n180 80 L\nS\n")).doc);
|
||||
assert_eq!(a.len(), 1);
|
||||
let st = a[0].appearance.stroke().unwrap();
|
||||
assert_eq!((st.width, st.paint.color(), st.cap), (0.25, red, LineCap::Square));
|
||||
|
||||
let art_ = format!(
|
||||
"0 1 1 0 1 0 0 XA\n0 w\n{}Bb\n0 0 0 0 Bh\n1 (G) 0 0 0 1 1 0 0 1 0 0 1 Bg\n50 0 0 -50 100 50 Bm\nf\n1 BB\n",
|
||||
rect_path(50.0, 0.0, 150.0, 100.0)
|
||||
);
|
||||
let a = art(&read_ok(&format!("{G_DEFS}{}", layer(&art_))).doc);
|
||||
assert!(matches!(fill(&a[0]), Paint::Gradient(_)), "{:?}", fill(&a[0]));
|
||||
let st = a[0].appearance.stroke().unwrap();
|
||||
assert_eq!((st.width, st.paint.color()), (0.25, red));
|
||||
|
||||
let text = "0 To\n1 0 0 1 10 50 0 Tp\nTP\n1 Tr\n0 1 1 0 1 0 0 XA\n0 w\n/_Helvetica 9 Tf\n(hi) Tx\nTO\n";
|
||||
let a = art(&read_ok(&layer(text)).doc);
|
||||
let Some(NodeKind::Text(t)) = a.first().map(|n| &n.kind) else { panic!("{a:#?}") };
|
||||
let style = &t.runs[0].style;
|
||||
assert_eq!((style.stroke_width, style.stroke.color(), style.fill.clone()), (0.25, red, Paint::None));
|
||||
}
|
||||
|
||||
/// An art style's stroke of width 0 comes in as a hairline too.
|
||||
#[test]
|
||||
fn an_art_style_s_line_width_of_zero_is_a_hairline() {
|
||||
let styles = "%AI9_BeginArtStyles\n/KnownStyle :\n(Thin) /Name ,\n/ActiveStyle :\n/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
|
||||
/CompoundFilter :\n(Stack Style Filter) 0 0 /Filter ,\n1 /Visible ,\n\
|
||||
/BasicFilter :\n(Stroke Style Filter) 0 0 /Filter ,\n1 /Visible ,\n2 /FillOrStroke ,\n/Dictionary : /NotRecorded ,\n\
|
||||
/StrokeStyle : 0 R\n0 1 1 0 1 0 0 XA\n0 J 0 j 0 w 10 M []0 d\n0 XR\n1 0 Xd\n/Def ;\n (StrokeStyle) ,\n; /Dict ;\n /Part ,\n\
|
||||
/BasicFilter :\n(Conduit Filter) 0 0 /Filter ,\n1 /Visible ,\n;\n /Part ,\n;\n /Part ,\n;\n%AI9_EndArtStyles\n";
|
||||
let line = "20 20 m\n180 80 L\n";
|
||||
let art_ = format!("0 Ae\nu\n0 1 1 0 1 0 0 XA\n0 w\n{line}S\nU\n%_0 1 1 0 1 0 0 XA\n%_0 w\n%_{}S\n1 (Thin) XW\n", line.replace('\n', "\n%_"));
|
||||
let r = read_ok(&format!("{styles}{}", layer(&art_)));
|
||||
let a = art(&r.doc);
|
||||
assert_eq!(a.len(), 1, "{a:#?}");
|
||||
assert!(matches!(a[0].kind, NodeKind::Path { .. }), "the object, not its look: {a:#?}");
|
||||
assert!(!r.warnings.iter().any(|w| w.contains("drawn look")), "{:?}", r.warnings);
|
||||
let st = a[0].appearance.stroke().unwrap();
|
||||
assert_eq!((st.width, st.paint.color()), (0.25, Some(Color::rgb(1.0, 0.0, 0.0))));
|
||||
}
|
||||
|
||||
/// `Xk` and `XK` are global process colours: a name, a tint and whether the colour is RGB, as
|
||||
/// `Xx` has them (#1025). They come in as global swatches that aren't inks, linked at their tint.
|
||||
#[test]
|
||||
|
||||
@@ -31,7 +31,7 @@ const MAX_NODES: usize = 1 << 20;
|
||||
/// are left out.
|
||||
const LIMIT: f64 = 1e6;
|
||||
/// Width of a zero-width line (the thinnest a device draws), points.
|
||||
const HAIRLINE: f64 = 0.25;
|
||||
pub(super) const HAIRLINE: f64 = 0.25;
|
||||
/// Most inks a `DeviceN` space has (PostScript's own limit).
|
||||
const MAX_INKS: usize = 32;
|
||||
/// Most dashes `strokepath` makes of a path (more and it outlines the path undashed).
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user