Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71174477cc | ||
|
|
8716fc8fbc | ||
|
|
268a4452f9 | ||
|
|
f97b24b388 | ||
|
|
a747daa346 | ||
|
|
2d619baaba | ||
|
|
f45ffdeb96 | ||
|
|
d6f1c9862d | ||
|
|
5a8682e45d | ||
|
|
52e9b25371 | ||
|
|
5d5e7a1dd2 | ||
|
|
42b355c21c | ||
|
|
a48ed5e253 |
No files matched your search
Generated
+27
-27
@@ -4490,7 +4490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"block2 0.6.2",
|
||||
@@ -4524,7 +4524,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-affinity"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
@@ -4535,7 +4535,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-brush"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"serde",
|
||||
@@ -4547,7 +4547,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-cad"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4563,7 +4563,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-cli"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"objc2-core-foundation",
|
||||
@@ -4584,7 +4584,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-color"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"moxcms",
|
||||
@@ -4596,7 +4596,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-doc"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"jpeg-encoder",
|
||||
@@ -4613,7 +4613,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-effects"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"serde_json",
|
||||
@@ -4628,7 +4628,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-engine"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
@@ -4663,7 +4663,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-eps"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"image",
|
||||
@@ -4683,7 +4683,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-format"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"proptest",
|
||||
@@ -4697,7 +4697,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-geom"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"proptest",
|
||||
@@ -4709,7 +4709,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-mcp"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4728,7 +4728,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-metafile"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4742,7 +4742,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-pathops"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"linesweeper",
|
||||
@@ -4754,7 +4754,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-pdf"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"flate2",
|
||||
@@ -4783,7 +4783,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-plugins"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4797,7 +4797,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-render"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"gif",
|
||||
@@ -4822,7 +4822,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-svg"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"image",
|
||||
@@ -4846,7 +4846,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-testkit"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4863,7 +4863,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-text"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"harfrust 0.13.3",
|
||||
"icu_segmenter",
|
||||
@@ -4883,14 +4883,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-three-d"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"vectorcraft-geom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-tools"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4905,7 +4905,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-trace"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo 0.13.1",
|
||||
@@ -4918,7 +4918,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-ui-egui"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"craft-ui",
|
||||
"egui",
|
||||
@@ -4951,7 +4951,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vectorcraft-web"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -6061,7 +6061,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/vectorcraft", "apps/vectorcraft-cli", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -25,10 +25,6 @@ real_name = "Brandon Thomas"
|
||||
display_name = "Brandon Thomas"
|
||||
sync_github_name = false
|
||||
|
||||
[people.callmeBalloch]
|
||||
real_name = "Riccardo Balloch"
|
||||
sync_github_name = false
|
||||
|
||||
[people.crqch]
|
||||
sync_github_name = true
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Affinity files.
|
||||
| Artboards (legacy `ABEn` or Affinity 3 `phrp`/`aprp`), with name, paint and any transform | artboards plus a clip group with the actual vector outline, background and stroke |
|
||||
| Layers (`Scop`), groups (`Grup`), pass-through or isolated | layers and sublayers, groups |
|
||||
| Curves (`PCrv`): cubic subpaths, closed flags, live corners (`CnrD`) | paths (multi-subpath curves fill even-odd) |
|
||||
| Rectangles with corner radii (relative or absolute), ellipses, polygons (smooth too), stars, square stars, pies, triangles, trapezoids | paths |
|
||||
| Rectangles with corner radii (relative or absolute, locked or per corner) and round or straight corners, ellipses, polygons (smooth too), stars, square stars, pies, triangles, trapezoids | paths |
|
||||
| Compound shapes (`Comp`, add and subtract) | one even-odd path |
|
||||
| A shape or curve with children clips them | clip group whose clipping path keeps its fill (behind) and stroke (over) |
|
||||
| Vector masks (`AdCh` curves and shapes) | clip group |
|
||||
@@ -33,7 +33,7 @@ Everything else is reported in the import warnings, one line per kind with a cou
|
||||
dropped silently: layer effects, adjustment layers and live filters, brush and pressure strokes,
|
||||
transparency gradients, fill opacity, bitmap fills, master pages, conical gradients (as radial),
|
||||
special shapes (cloud, heart, cog, callouts, arrows…, imported as their bounding ellipse), corner
|
||||
types other than round, stars with rounded points, several fills or strokes on one object (the
|
||||
types other than round or straight, stars with rounded points, several fills or strokes on one object (the
|
||||
active one is used), outlined or scaled text, text fields such as page numbers, frames with
|
||||
columns or a curved outline, paragraph indents/spacing, mixed paragraph alignments, unsupported font selectors, grey/Lab/32-bit pixels, CMYK pixels (converted to RGB without the
|
||||
document's profile) and Affinity-only blend modes (Add, Linear Light… as Normal).
|
||||
|
||||
@@ -51,28 +51,44 @@ pub(crate) fn shape(r: &mut Reader, shape: ObjId, b: [f64; 4], world: Affine) ->
|
||||
let class = r.s.class(shape)?;
|
||||
let sub = match &class.0.to_be_bytes() {
|
||||
b"ShNR" | b"ShRR" => {
|
||||
let rounded = matches!(last(r, shape, b"CTyp"), Some(Value::Array(a)) if !a.is_empty());
|
||||
let radius = match last(r, shape, b"ShCR") {
|
||||
Some(Value::Floats(v)) => v.first().copied().filter(|f| f.is_finite() && *f > 0.0),
|
||||
_ => None,
|
||||
let types = match last(r, shape, b"CTyp") {
|
||||
Some(Value::Array(a)) => a.iter().map(|t| if let Value::Enum { id, .. } = t { Some(*id) } else { None }).collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
if let (true, Some(f)) = (rounded, radius) {
|
||||
if let Some(Value::Array(types)) = last(r, shape, b"CTyp")
|
||||
&& types.iter().any(|t| !matches!(t, Value::Enum { id: 0 | 4, .. }))
|
||||
{
|
||||
r.warn("rectangles with straight, concave or other corner types (imported with round corners)");
|
||||
}
|
||||
let (crx, cry) = if r.s.bool(shape, b"AbSz") == Some(true) {
|
||||
// An absolute radius is in document pixels, whatever the node's scale.
|
||||
let [a, b2, c, d, ..] = world.0;
|
||||
let (sx, sy) = ((a * a + b2 * b2).sqrt(), (c * c + d * d).sqrt());
|
||||
if sx > 0.0 && sy > 0.0 { (f / sx, f / sy) } else { (0.0, 0.0) }
|
||||
} else {
|
||||
(f * w.min(h), f * w.min(h))
|
||||
};
|
||||
rounded_rect(b, crx.min(w / 2.0), cry.min(h / 2.0))
|
||||
} else {
|
||||
let radii = match last(r, shape, b"ShCR") {
|
||||
Some(Value::Floats(v)) => v.iter().map(|f| if f.is_finite() && *f > 0.0 { *f } else { 0.0 }).collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
// Corners follow the first one (top left) unless they were unlocked; then each has
|
||||
// its own radius and type, clockwise from the top left.
|
||||
let locked = !matches!(last(r, shape, b"Lock"), Some(Value::Bool(false)));
|
||||
let corners: [(f64, Option<u16>); 4] = std::array::from_fn(|i| {
|
||||
let i = if locked { 0 } else { i };
|
||||
(radii.get(i).copied().unwrap_or(0.0), types.get(i).copied().flatten())
|
||||
});
|
||||
if types.is_empty() || corners.iter().all(|(f, _)| *f == 0.0) {
|
||||
polygon([pt(x0, y0), pt(x1, y0), pt(x1, y1), pt(x0, y1)])?
|
||||
} else {
|
||||
if corners.iter().any(|(f, t)| *f > 0.0 && !matches!(t, Some(0 | 1 | 4))) {
|
||||
r.warn("rectangles with concave or other corner types (imported with round corners)");
|
||||
}
|
||||
let absolute = r.s.bool(shape, b"AbSz") == Some(true);
|
||||
let [a, b2, c, d, ..] = world.0;
|
||||
let (sx, sy) = ((a * a + b2 * b2).sqrt(), (c * c + d * d).sqrt());
|
||||
cornered_rect(
|
||||
b,
|
||||
corners.map(|(f, t)| {
|
||||
let (crx, cry) = if !absolute {
|
||||
(f * w.min(h), f * w.min(h))
|
||||
} else if sx > 0.0 && sy > 0.0 {
|
||||
// An absolute radius is in document pixels, whatever the node's scale.
|
||||
(f / sx, f / sy)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
(crx, cry, t == Some(1))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
b"ShpE" => ellipse(cx, cy, rx, ry),
|
||||
@@ -165,22 +181,31 @@ fn ellipse(cx: f64, cy: f64, rx: f64, ry: f64) -> SubPath {
|
||||
}
|
||||
}
|
||||
|
||||
fn rounded_rect([x0, y0, x1, y1]: [f64; 4], rx: f64, ry: f64) -> SubPath {
|
||||
let (kx, ky) = ((1.0 - KAPPA) * rx, (1.0 - KAPPA) * ry);
|
||||
SubPath {
|
||||
start: pt(x0 + rx, y0),
|
||||
segments: vec![
|
||||
[pt(x1 - rx, y0), pt(x1 - rx, y0), pt(x1 - rx, y0)],
|
||||
[pt(x1 - kx, y0), pt(x1, y0 + ky), pt(x1, y0 + ry)],
|
||||
[pt(x1, y1 - ry), pt(x1, y1 - ry), pt(x1, y1 - ry)],
|
||||
[pt(x1, y1 - ky), pt(x1 - kx, y1), pt(x1 - rx, y1)],
|
||||
[pt(x0 + rx, y1), pt(x0 + rx, y1), pt(x0 + rx, y1)],
|
||||
[pt(x0 + kx, y1), pt(x0, y1 - ky), pt(x0, y1 - ry)],
|
||||
[pt(x0, y0 + ry), pt(x0, y0 + ry), pt(x0, y0 + ry)],
|
||||
[pt(x0, y0 + ky), pt(x0 + kx, y0), pt(x0 + rx, y0)],
|
||||
],
|
||||
closed: true,
|
||||
/// A rectangle whose corners (top left, top right, bottom right, bottom left) are each rounded,
|
||||
/// or cut straight across, by their (horizontal, vertical) radii. Two radii that don't fit along
|
||||
/// one side shrink in proportion until they do.
|
||||
fn cornered_rect([x0, y0, x1, y1]: [f64; 4], corners: [(f64, f64, bool); 4]) -> SubPath {
|
||||
let fit = |r: f64, pair: f64, side: f64| if pair > side { side * (r / pair) } else { r };
|
||||
let [tl, tr, br, bl] = corners;
|
||||
let (w, h) = (x1 - x0, y1 - y0);
|
||||
let rx = [fit(tl.0, tl.0 + tr.0, w), fit(tr.0, tl.0 + tr.0, w), fit(br.0, bl.0 + br.0, w), fit(bl.0, bl.0 + br.0, w)];
|
||||
let ry = [fit(tl.1, tl.1 + bl.1, h), fit(tr.1, tr.1 + br.1, h), fit(br.1, tr.1 + br.1, h), fit(bl.1, tl.1 + bl.1, h)];
|
||||
let (kx, ky) = (rx.map(|r| (1.0 - KAPPA) * r), ry.map(|r| (1.0 - KAPPA) * r));
|
||||
// (where the corner leaves the previous side, its two handles, where it joins the next side)
|
||||
let arcs = [
|
||||
[pt(x1 - rx[1], y0), pt(x1 - kx[1], y0), pt(x1, y0 + ky[1]), pt(x1, y0 + ry[1])],
|
||||
[pt(x1, y1 - ry[2]), pt(x1, y1 - ky[2]), pt(x1 - kx[2], y1), pt(x1 - rx[2], y1)],
|
||||
[pt(x0 + rx[3], y1), pt(x0 + kx[3], y1), pt(x0, y1 - ky[3]), pt(x0, y1 - ry[3])],
|
||||
[pt(x0, y0 + ry[0]), pt(x0, y0 + ky[0]), pt(x0 + kx[0], y0), pt(x0 + rx[0], y0)],
|
||||
];
|
||||
let mut segments = Vec::with_capacity(8);
|
||||
for ([s, c1, c2, e], straight) in arcs.into_iter().zip([tr.2, br.2, bl.2, tl.2]) {
|
||||
segments.push([s, s, s]);
|
||||
if s != e {
|
||||
segments.push(if straight { [e, e, e] } else { [c1, c2, e] });
|
||||
}
|
||||
}
|
||||
SubPath { start: pt(x0 + rx[0], y0), segments, closed: true }
|
||||
}
|
||||
|
||||
/// Elliptical arc from angle `a0` to `a1` (radians, measured y-up as Affinity's pie angles are),
|
||||
|
||||
@@ -166,3 +166,39 @@ fn wrapped_pie_angles_keep_the_same_geometry_as_the_positive_span() {
|
||||
let make = |end| read(vec![shape(100, b"ShPi", vec![(tag(b"AngS"), F::F64(0.0)), (tag(b"AngE"), F::F64(end))])], 100);
|
||||
assert_eq!(path(&make(-std::f64::consts::FRAC_PI_2)), path(&make(3.0 * std::f64::consts::FRAC_PI_2)));
|
||||
}
|
||||
|
||||
/// `CTyp`: an array of corner-type enums (top left, top right, bottom right, bottom left).
|
||||
fn corner_types(ids: [u16; 4]) -> F {
|
||||
let mut raw = vec![0xaa];
|
||||
raw.extend(tag(b"CTyp").0.to_le_bytes());
|
||||
raw.extend(4u32.to_le_bytes());
|
||||
raw.extend(0u16.to_le_bytes());
|
||||
raw.extend(ids.iter().flat_map(|id| id.to_le_bytes()));
|
||||
F::Raw(raw)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unlocked_rectangle_corners_keep_their_own_radius_and_type() {
|
||||
let p = |x: f64, y: f64| Point { x, y };
|
||||
// 10×20 box: relative radii scale by 10. Round 5, round 1, straight 3, none.
|
||||
let corners = |lock: Option<bool>| {
|
||||
let mut fields = vec![(tag(b"ShCR"), F::F64s(vec![0.5, 0.1, 0.3, 0.0])), (tag(b"CTyp"), corner_types([0, 0, 1, 4]))];
|
||||
fields.extend(lock.map(|l| (tag(b"Lock"), F::Bool(l))));
|
||||
read(vec![shape(100, b"ShNR", fields)], 100)
|
||||
};
|
||||
|
||||
let unlocked = corners(Some(false));
|
||||
assert!(unlocked.warnings.is_empty(), "{:?}", unlocked.warnings);
|
||||
let sub = &path(&unlocked).subpaths[0];
|
||||
assert_eq!(sub.start, p(5.0, 0.0));
|
||||
let ends: Vec<Point> = sub.segments.iter().map(|s| s[2]).collect();
|
||||
assert_eq!(ends, [p(9.0, 0.0), p(10.0, 1.0), p(10.0, 17.0), p(7.0, 20.0), p(0.0, 20.0), p(0.0, 5.0), p(5.0, 0.0)]);
|
||||
assert_eq!(sub.segments[3], [p(7.0, 20.0); 3], "the straight corner is a line");
|
||||
assert_ne!(sub.segments[1][0], p(9.0, 0.0), "the round corner is a curve");
|
||||
|
||||
// Locked (the default): every corner follows the top left one.
|
||||
let locked = corners(None);
|
||||
let sub = &path(&locked).subpaths[0];
|
||||
assert_eq!(sub.segments.len(), 8);
|
||||
assert_eq!(sub.segments[3][2], p(5.0, 20.0));
|
||||
}
|
||||
@@ -20,7 +20,7 @@ patchy-live-filter.af dpi=72;spreads=1;model_crc32=30c5f8ee;counts={"depth": 2,
|
||||
patchy-rgba16.af dpi=72;spreads=1;model_crc32=1bb23571;counts={"depth": 1, "images": 1, "nodes": 1};boards=[];fonts={};warnings=[]
|
||||
patchy-rgba8.af dpi=72;spreads=1;model_crc32=1bb23571;counts={"depth": 1, "images": 1, "nodes": 1};boards=[];fonts={};warnings=[]
|
||||
patchy-shapes-2.af dpi=72;spreads=1;model_crc32=7828d3cd;counts={"depth": 1, "fills": 12, "gradients": 0, "nodes": 12, "segments": 62, "shapes": 12, "strokes": 0, "subpaths": 12};boards=[];fonts={};warnings=["cloud, heart, cog, callout, arrow, tear and other special shapes (imported as their bounding ellipse) (9×)"]
|
||||
patchy-shapes.af dpi=72;spreads=1;model_crc32=e127e129;counts={"depth": 1, "fills": 4, "gradients": 0, "nodes": 4, "segments": 29, "shapes": 4, "strokes": 0, "subpaths": 4};boards=[];fonts={};warnings=["rectangles with straight, concave or other corner types (imported with round corners)"]
|
||||
patchy-shapes.af dpi=72;spreads=1;model_crc32=7519f5fe;counts={"depth": 1, "fills": 4, "gradients": 0, "nodes": 4, "segments": 28, "shapes": 4, "strokes": 0, "subpaths": 4};boards=[];fonts={};warnings=[]
|
||||
patchy-text-artistic.af dpi=72;spreads=1;model_crc32=b6137f6e;counts={"depth": 1, "nodes": 1, "text_chars": 5, "text_runs": 1, "texts": 1};boards=[];fonts={"Arial:ArialMT:400:false"};warnings=[]
|
||||
patchy-text-caps.af dpi=72;spreads=1;model_crc32=b5527e72;counts={"depth": 1, "nodes": 2, "text_chars": 21, "text_runs": 2, "texts": 2};boards=[];fonts={"Arial:ArialNarrow:400:false"};warnings=["font width variants (exact face depends on the installed PostScript font) (2×)"]
|
||||
patchy-text-frame.af dpi=72;spreads=1;model_crc32=7a2cd32d;counts={"depth": 1, "nodes": 1, "text_chars": 11, "text_runs": 1, "texts": 1};boards=[];fonts={"Arial:ArialMT:400:false"};warnings=["paragraph indents or spacing (imported with defaults)"]
|
||||
|
||||
@@ -23,6 +23,7 @@ const FILES: &[(&str, f64)] = &[
|
||||
("patchy-group.af", 0.25), // 0.00
|
||||
("patchy-transform.af", 1.0), // 0.19
|
||||
("patchy-vector.af", 1.0), // 0.12
|
||||
("patchy-shapes.af", 1.0), // 0.24: per-corner radii, a straight corner
|
||||
("patchy-lazy-placed.af", 1.0), // 0.50: upstream byte-derived lazy-image regression
|
||||
("patchy-embedded-jpeg.af", 1.0), // 0.50
|
||||
("affinity3-mexican-guy.af", 3.0), // 1.80: brush strokes as plain strokes
|
||||
|
||||
@@ -485,6 +485,15 @@ fn mutate_text(src: &str, cut: usize, edits: &[(usize, char)]) -> String {
|
||||
chars.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Type shifted two billion points down exports to PDF: an input of
|
||||
/// `svg_hostile_markup_never_panics`, cut down to its type.
|
||||
#[test]
|
||||
fn svg_type_shifted_two_billion_points_exports_to_pdf() {
|
||||
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><text baseline-shift="-2147483649">f</text></svg>"#;
|
||||
let d = vectorcraft_svg::import(svg).unwrap();
|
||||
assert!(vectorcraft_engine::export_pdf(&d, &vectorcraft_pdf::PdfOptions::default()).is_ok());
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
|
||||
+139
-40
@@ -14,7 +14,7 @@ use krilla::num::NormalizedF32;
|
||||
use krilla::page::PageSettings;
|
||||
use krilla::paint::{Fill, LinearGradient, RadialGradient, SpreadMethod, Stop, Stroke, StrokeDash};
|
||||
use krilla::surface::Surface;
|
||||
use krilla::text::{GlyphId, KrillaGlyph};
|
||||
use krilla::text::{Glyph, GlyphId, KrillaGlyph};
|
||||
use kurbo::{PathEl, Shape, Vec2};
|
||||
use vectorcraft_color::{BlendMode, Color, GradientKind, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, Document, LineCap, LineJoin, Node, NodeKind, StrokeAlign, StrokeLayer, TextObject};
|
||||
@@ -229,8 +229,10 @@ impl Writer {
|
||||
if sheet.negative {
|
||||
cover(&mut s, paper, 255, 1.0);
|
||||
}
|
||||
// A page whose art or marks would be placed beyond [`MAX_COORD`] is left blank.
|
||||
let fits = writable(&s, sheet.view, Rect::ZERO) && writable(&s, sheet.view * sheet.place, Rect::ZERO);
|
||||
s.push_transform(&xf(sheet.view));
|
||||
let window = sheet.window.and_then(|r| to_path(&r.to_path(0.1)));
|
||||
let window = sheet.window.and_then(|r| to_path(&s, &r.to_path(0.1)));
|
||||
if let Some(clip) = &window {
|
||||
s.push_clip_path(clip, &krilla::paint::FillRule::NonZero);
|
||||
}
|
||||
@@ -239,7 +241,7 @@ impl Writer {
|
||||
s.push_transform(&xf(sheet.place));
|
||||
}
|
||||
let r = sheet.area;
|
||||
let clip = sheet.clip.then(|| to_path(&r.to_path(0.1))).flatten();
|
||||
let clip = sheet.clip.then(|| to_path(&s, &r.to_path(0.1))).flatten();
|
||||
if let Some(clip) = &clip {
|
||||
s.push_clip_path(clip, &krilla::paint::FillRule::NonZero);
|
||||
}
|
||||
@@ -248,15 +250,17 @@ impl Writer {
|
||||
// at the top of a page is put in a non-isolated group of its own, whose blending space
|
||||
// `cmyk_blending` rewrites.
|
||||
let doc = ex.doc;
|
||||
let page_group = !self.pdf13 && (doc.page_isolate || doc.page_knockout);
|
||||
let cmyk_page_group = self.cmyk && !page_group && ex.shows_transparency();
|
||||
let page_group = fits && !self.pdf13 && (doc.page_isolate || doc.page_knockout);
|
||||
let cmyk_page_group = fits && self.cmyk && !page_group && ex.shows_transparency();
|
||||
if page_group {
|
||||
s.push_isolated();
|
||||
} else if cmyk_page_group {
|
||||
let all = constant_mask(&mut s, r, 1.0);
|
||||
s.push_mask(all);
|
||||
}
|
||||
ex.children(&mut s, &doc.layers, r);
|
||||
if fits {
|
||||
ex.children(&mut s, &doc.layers, r);
|
||||
}
|
||||
if page_group || cmyk_page_group {
|
||||
s.pop();
|
||||
}
|
||||
@@ -266,7 +270,7 @@ impl Writer {
|
||||
if placed {
|
||||
s.pop();
|
||||
}
|
||||
if let Some(art) = sheet.marks {
|
||||
if fits && let Some(art) = sheet.marks {
|
||||
// The drawing space the page shows (the marks' area).
|
||||
let shown = sheet.view.inverse().transform_rect_bbox(paper);
|
||||
let knockout = std::mem::replace(&mut ex.knockout, false);
|
||||
@@ -525,12 +529,43 @@ struct GlyphLine {
|
||||
/// Most pixels along a side of a freeform gradient's image (its colour field is smooth).
|
||||
const MAX_FIELD_PX: f64 = 512.0;
|
||||
|
||||
/// The largest magnitude a coordinate, length or transform entry may have in the content stream it
|
||||
/// is drawn into (for glyph spacing, in thousandths of an em). Art that would go beyond it is left
|
||||
/// out. A PDF/A-2 file may hold only integers from -2,147,483,648 to 2,147,483,647 (ISO 19005-2,
|
||||
/// 6.1.13). The PDF writer writes a whole number in the `i32` range as an integer, and in builds
|
||||
/// with overflow checks it panics on -2,147,483,648.
|
||||
const MAX_COORD: f64 = 1e9;
|
||||
|
||||
fn xf(a: Affine) -> Transform {
|
||||
let c = a.as_coeffs();
|
||||
Transform::from_row(c[0] as f32, c[1] as f32, c[2] as f32, c[3] as f32, c[4] as f32, c[5] as f32)
|
||||
}
|
||||
|
||||
fn to_path(bp: &BezPath) -> Option<Path> {
|
||||
/// The current transform of surface `s` in the content stream it draws into, without that stream's
|
||||
/// root transform (a page's y flip).
|
||||
fn ctm(s: &Surface) -> Affine {
|
||||
let t = s.ctm();
|
||||
Affine::new([t.sx(), t.ky(), t.kx(), t.sy(), t.tx(), t.ty()].map(f64::from))
|
||||
}
|
||||
|
||||
/// Are `values` finite and within [`MAX_COORD`]?
|
||||
fn in_range(values: &[f64]) -> bool {
|
||||
values.iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD)
|
||||
}
|
||||
|
||||
/// Can art spanning `b`, in the space `space` maps into the one `s` draws in, be written: `b`, the
|
||||
/// transform of `s` followed by `space`, and `b` through that transform all within [`MAX_COORD`]?
|
||||
fn writable(s: &Surface, space: Affine, b: Rect) -> bool {
|
||||
let t = ctm(s) * space;
|
||||
let to = t.transform_rect_bbox(b);
|
||||
in_range(&t.as_coeffs()) && in_range(&[b.x0, b.y0, b.x1, b.y1, to.x0, to.y0, to.x1, to.y1])
|
||||
}
|
||||
|
||||
/// `bp` as a path to draw on `s`; `None` when it is empty or reaches beyond [`MAX_COORD`] there.
|
||||
fn to_path(s: &Surface, bp: &BezPath) -> Option<Path> {
|
||||
if !writable(s, Affine::IDENTITY, bp.control_box()) {
|
||||
return None;
|
||||
}
|
||||
let mut pb = PathBuilder::new();
|
||||
for el in bp.elements() {
|
||||
match *el {
|
||||
@@ -597,7 +632,7 @@ fn color(c: &Color) -> krilla::color::Color {
|
||||
/// Fill the page (with a margin) with a grey level (0 = black, 255 = white) at `opacity`: mask
|
||||
/// backdrops.
|
||||
fn cover(s: &mut Surface, page: Rect, level: u8, opacity: f32) {
|
||||
let Some(p) = to_path(&page.inflate(1.0, 1.0).to_path(0.1)) else { return };
|
||||
let Some(p) = to_path(s, &page.inflate(1.0, 1.0).to_path(0.1)) else { return };
|
||||
s.set_stroke(None);
|
||||
s.set_fill(Some(Fill { paint: rgb::Color::new(level, level, level).into(), opacity: norm(opacity), rule: krilla::paint::FillRule::NonZero }));
|
||||
s.draw_path(&p);
|
||||
@@ -718,14 +753,15 @@ impl Exporter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a paint. `bounds` resolves unset gradient geometry (like the renderer).
|
||||
fn paint(&mut self, p: &Paint, bounds: Rect) -> Option<krilla::paint::Paint> {
|
||||
self.paint_in(p, bounds, Affine::IDENTITY)
|
||||
/// Convert a paint to draw on `s`. `bounds` resolves unset gradient geometry (like the
|
||||
/// renderer). A gradient placed beyond [`MAX_COORD`] there gives no paint.
|
||||
fn paint(&mut self, s: &Surface, p: &Paint, bounds: Rect) -> Option<krilla::paint::Paint> {
|
||||
self.paint_in(s, p, bounds, Affine::IDENTITY)
|
||||
}
|
||||
|
||||
/// [`Self::paint`] drawn in another space: `space` maps the paint's space (where `bounds`
|
||||
/// is) to the one drawn in.
|
||||
fn paint_in(&mut self, p: &Paint, bounds: Rect, space: Affine) -> Option<krilla::paint::Paint> {
|
||||
fn paint_in(&mut self, s: &Surface, p: &Paint, bounds: Rect, space: Affine) -> Option<krilla::paint::Paint> {
|
||||
match p {
|
||||
Paint::None => None,
|
||||
Paint::Solid { color: c, swatch, tint } => Some(self.solid(c, swatch.as_deref(), *tint).into()),
|
||||
@@ -761,6 +797,9 @@ impl Exporter<'_> {
|
||||
}
|
||||
match g.gradient.kind {
|
||||
GradientKind::Linear => {
|
||||
if !writable(s, space, Rect::from_points(geom.start, geom.end)) {
|
||||
return None;
|
||||
}
|
||||
let (s, mut e) = (geom.start, geom.end);
|
||||
if s.distance(e) < 1e-9 {
|
||||
e = s + Vec2::new(1.0, 0.0);
|
||||
@@ -785,6 +824,10 @@ impl Exporter<'_> {
|
||||
let (cx, cy) = (geom.start.x as f32, geom.start.y as f32);
|
||||
// An off-centre focal point: a two-point radial shading from it.
|
||||
let f = t.inverse() * geom.focal_point();
|
||||
let circles = Rect::from_center_size(geom.start, (2.0 * f64::from(r), 2.0 * f64::from(r))).union_pt(f);
|
||||
if !writable(s, space * t, circles) {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
RadialGradient {
|
||||
fx: f.x as f32,
|
||||
@@ -968,7 +1011,7 @@ impl Exporter<'_> {
|
||||
// The region every output clips to; nothing to clip by hides the clipped art. The
|
||||
// clipping path's fill paints behind the clipped art and its stroke over it, unclipped.
|
||||
if let Some((clip, rest)) = children.split_first()
|
||||
&& let Some((p, r)) = vectorcraft_effects::clip_outline(clip).and_then(|(bp, r)| to_path(&bp).map(|p| (p, r)))
|
||||
&& let Some((p, r)) = vectorcraft_effects::clip_outline(clip).and_then(|(bp, r)| to_path(s, &bp).map(|p| (p, r)))
|
||||
{
|
||||
let paint = clip.clip_paint();
|
||||
s.push_clip_path(&p, &rule(r));
|
||||
@@ -1020,7 +1063,7 @@ impl Exporter<'_> {
|
||||
}
|
||||
|
||||
fn shape(&mut self, s: &mut Surface, n: &Node, bp: &BezPath, r: FillRule, page: Rect) {
|
||||
let Some(path) = to_path(bp) else { return };
|
||||
let Some(path) = to_path(s, bp) else { return };
|
||||
let bounds = bp.bounding_box();
|
||||
for item in &n.appearance.items {
|
||||
match item {
|
||||
@@ -1033,7 +1076,7 @@ impl Exporter<'_> {
|
||||
if ex.area(s, &path, rule(r), &fl.paint, (fl.opacity, fl.blend), bounds, bounds) {
|
||||
return;
|
||||
}
|
||||
let Some(paint) = ex.paint(&fl.paint, bounds) else { return };
|
||||
let Some(paint) = ex.paint(s, &fl.paint, bounds) else { return };
|
||||
let bl = fl.blend != BlendMode::Normal;
|
||||
if bl {
|
||||
s.push_blend_mode(blend(fl.blend));
|
||||
@@ -1166,14 +1209,18 @@ impl Exporter<'_> {
|
||||
// The area the stroke paints (dashes, caps, joins, profile, arrowheads, alignment), as
|
||||
// Outline Stroke makes it, painted with the pattern's tiles or the freeform's image.
|
||||
let region = stroke::outline_region(&vectorcraft_geom::PathData::from_bezpath(bp), r, st).to_bezpath();
|
||||
if let Some(clip) = to_path(®ion) {
|
||||
if let Some(clip) = to_path(s, ®ion) {
|
||||
let area = region.bounding_box();
|
||||
self.area(s, &clip, krilla::paint::FillRule::NonZero, &st.paint, (st.opacity, st.blend), st.paint_bounds(bounds), area);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(paint) = self.paint(&st.paint, st.paint_bounds(bounds)) else { return };
|
||||
let Some(paint) = self.paint(s, &st.paint, st.paint_bounds(bounds)) else { return };
|
||||
let w = stroke::for_writer(bp, st);
|
||||
// A stroke whose width, caps or miter spikes reach beyond [`MAX_COORD`] is left out.
|
||||
if !writable(s, Affine::IDENTITY, w.reach(st, bp.control_box()).inflate(1.0, 1.0)) {
|
||||
return;
|
||||
}
|
||||
let mut pushes = 0;
|
||||
if st.blend != BlendMode::Normal {
|
||||
s.push_blend_mode(blend(st.blend));
|
||||
@@ -1189,7 +1236,7 @@ impl Exporter<'_> {
|
||||
// (miter spikes included) plus the path, even-odd.
|
||||
let mut outside = w.reach(st, bounds).inflate(1.0, 1.0).to_path(0.1);
|
||||
outside.extend(bp.iter());
|
||||
if let Some(p) = to_path(&outside) {
|
||||
if let Some(p) = to_path(s, &outside) {
|
||||
s.push_clip_path(&p, &krilla::paint::FillRule::EvenOdd);
|
||||
pushes += 1;
|
||||
}
|
||||
@@ -1203,7 +1250,11 @@ impl Exporter<'_> {
|
||||
if pat.len() % 2 == 1 {
|
||||
pat.extend(pat.clone());
|
||||
}
|
||||
StrokeDash { array: pat, offset: d.offset as f32 }
|
||||
// An offset beyond [`MAX_COORD`] is written as its remainder after whole
|
||||
// periods of the pattern.
|
||||
let period: f64 = pat.iter().copied().map(f64::from).sum();
|
||||
let offset = if in_range(&[d.offset]) { d.offset } else { d.offset.rem_euclid(period) };
|
||||
StrokeDash { array: pat, offset: if offset.is_finite() { offset as f32 } else { 0.0 } }
|
||||
});
|
||||
s.set_fill(None);
|
||||
s.set_stroke(Some(Stroke {
|
||||
@@ -1230,7 +1281,7 @@ impl Exporter<'_> {
|
||||
// A gradient along or across the stroke: slices clipped to its outlines, under
|
||||
// the stroke's opacity as a group.
|
||||
if let Some(ws) = stroke::written_slices(bp, r, st, outlines)
|
||||
&& let Some(clip) = to_path(&ws.clip)
|
||||
&& let Some(clip) = to_path(s, &ws.clip)
|
||||
{
|
||||
self.warn("gradients along or across strokes are exported as slices of linear gradients");
|
||||
if st.opacity < 1.0 {
|
||||
@@ -1241,7 +1292,7 @@ impl Exporter<'_> {
|
||||
s.set_stroke(None);
|
||||
for (shape, paint) in &ws.slices {
|
||||
let b = shape.bounding_box();
|
||||
if let (Some(paint), Some(p)) = (self.paint(paint, b), to_path(shape)) {
|
||||
if let (Some(paint), Some(p)) = (self.paint(s, paint, b), to_path(s, shape)) {
|
||||
s.set_fill(Some(Fill { paint, opacity: NormalizedF32::ONE, rule: krilla::paint::FillRule::NonZero }));
|
||||
s.draw_path(&p);
|
||||
}
|
||||
@@ -1260,8 +1311,10 @@ impl Exporter<'_> {
|
||||
let opacity = if grouped { NormalizedF32::ONE } else { norm(st.opacity) };
|
||||
s.set_stroke(None);
|
||||
s.set_fill(Some(Fill { paint, opacity, rule: krilla::paint::FillRule::NonZero }));
|
||||
for p in outlines.iter().filter_map(to_path) {
|
||||
s.draw_path(&p);
|
||||
for outline in outlines {
|
||||
if let Some(p) = to_path(s, outline) {
|
||||
s.draw_path(&p);
|
||||
}
|
||||
}
|
||||
s.set_fill(None);
|
||||
}
|
||||
@@ -1315,14 +1368,16 @@ impl Exporter<'_> {
|
||||
all.extend(bar.iter());
|
||||
}
|
||||
all.apply_affine(t.xf);
|
||||
to_path(&all).map(|p| (all, p))
|
||||
to_path(s, &all).map(|p| (all, p))
|
||||
});
|
||||
let all = all.flatten();
|
||||
if let Some((bp, path)) = &all {
|
||||
self.text_items(s, below, bp, path, page, tb);
|
||||
}
|
||||
s.push_transform(&xf(t.xf));
|
||||
// Real text: the characters' fills as text in embedded fonts.
|
||||
// Real text: the characters' fills as text in embedded fonts. The type's transform is
|
||||
// pushed for each fill and stroke of the characters, inside its form when the fill or
|
||||
// stroke overprints.
|
||||
let text_space = xf(t.xf);
|
||||
let plain = (!self.outline_text).then(|| t.plain_text());
|
||||
let bars = vectorcraft_text::decorations(&layout, vectorcraft_text::FontDb::global(), t);
|
||||
for (i, run) in t.runs.iter().enumerate() {
|
||||
@@ -1338,6 +1393,7 @@ impl Exporter<'_> {
|
||||
bp.extend(run_bars.iter());
|
||||
let fill = &run.style.fill;
|
||||
self.overprinting(s, run.style.overprint_fill, fill, |ex, s| {
|
||||
s.push_transform(&text_space);
|
||||
// The outlines left to fill as paths: all of them, or those real text leaves.
|
||||
let outlines = match plain.as_deref().filter(|_| !fill.is_none() && !ex.area_paint(fill)) {
|
||||
Some(text) => {
|
||||
@@ -1348,26 +1404,31 @@ impl Exporter<'_> {
|
||||
None => Cow::Borrowed(&bp),
|
||||
};
|
||||
let opaque = (1.0, BlendMode::Normal);
|
||||
if let Some(path) = to_path(&outlines)
|
||||
if let Some(path) = to_path(s, &outlines)
|
||||
&& !ex.area(s, &path, krilla::paint::FillRule::NonZero, fill, opaque, layout.bounds, outlines.bounding_box())
|
||||
&& let Some(paint) = ex.paint(fill, layout.bounds)
|
||||
&& let Some(paint) = ex.paint(s, fill, layout.bounds)
|
||||
{
|
||||
s.set_stroke(None);
|
||||
s.set_fill(Some(Fill { paint, opacity: NormalizedF32::ONE, rule: krilla::paint::FillRule::NonZero }));
|
||||
s.draw_path(&path);
|
||||
}
|
||||
s.pop();
|
||||
});
|
||||
if run.style.has_stroke()
|
||||
&& let Some(path) = to_path(&bp)
|
||||
{
|
||||
s.push_transform(&text_space);
|
||||
let path = run.style.has_stroke().then(|| to_path(s, &bp)).flatten();
|
||||
s.pop();
|
||||
if let Some(path) = path {
|
||||
// Character strokes are drawn in text space, with their cap, join and dashes.
|
||||
let st = run.style.stroke_layer();
|
||||
self.overprinting(s, st.overprint, &st.paint, |ex, s| ex.stroke(s, &bp, &path, FillRule::NonZero, &st, page, layout.bounds));
|
||||
self.overprinting(s, st.overprint, &st.paint, |ex, s| {
|
||||
s.push_transform(&text_space);
|
||||
ex.stroke(s, &bp, &path, FillRule::NonZero, &st, page, layout.bounds);
|
||||
s.pop();
|
||||
});
|
||||
}
|
||||
}
|
||||
s.set_fill(None);
|
||||
s.set_stroke(None);
|
||||
s.pop();
|
||||
if self.inline_depth < MAX_INLINE_DEPTH {
|
||||
self.inline_depth += 1;
|
||||
for ig in &layout.inlines {
|
||||
@@ -1393,7 +1454,7 @@ impl Exporter<'_> {
|
||||
match item {
|
||||
AppearanceItem::Fill(fl) if fl.visible => self.overprinting(s, fl.overprint, &fl.paint, |ex, s| {
|
||||
if !ex.area(s, path, krilla::paint::FillRule::NonZero, &fl.paint, (fl.opacity, fl.blend), tb, bp.bounding_box())
|
||||
&& let Some(paint) = ex.paint(&fl.paint, tb)
|
||||
&& let Some(paint) = ex.paint(s, &fl.paint, tb)
|
||||
{
|
||||
s.set_stroke(None);
|
||||
s.set_fill(Some(Fill { paint, opacity: norm(fl.opacity), rule: krilla::paint::FillRule::NonZero }));
|
||||
@@ -1468,9 +1529,9 @@ impl Exporter<'_> {
|
||||
}
|
||||
|
||||
/// Draw glyphs `line` (of `text`), filled with `fill` (whose space is text space, where
|
||||
/// `bounds` is).
|
||||
/// `bounds` is). A line that would be written with a pen position, size or spacing (in
|
||||
/// thousandths of an em) beyond [`MAX_COORD`] is left out.
|
||||
fn draw_glyph_line(&mut self, s: &mut Surface, line: GlyphLine, text: &str, fill: &Paint, bounds: Rect) {
|
||||
let Some(paint) = self.paint_in(fill, bounds, line.frame.inverse()) else { return };
|
||||
let size = line.size;
|
||||
let glyphs: Vec<KrillaGlyph> = line
|
||||
.glyphs
|
||||
@@ -1481,10 +1542,18 @@ impl Exporter<'_> {
|
||||
KrillaGlyph::new(GlyphId::new(*gid), ((next - x) / size) as f32, 0.0, 0.0, 0.0, range.clone(), None)
|
||||
})
|
||||
.collect();
|
||||
let pens =
|
||||
line.glyphs.iter().fold(Rect::new(0.0, -size, 0.0, size), |r, (_, x, advance, _)| r.union_pt((*x, 0.0)).union_pt((x + advance, 0.0)));
|
||||
let spacing: Vec<f64> = glyphs.iter().map(|g| f64::from(g.x_advance(1.0)) * 1000.0).collect();
|
||||
if !(writable(s, line.frame, pens) && in_range(&spacing)) {
|
||||
return;
|
||||
}
|
||||
s.push_transform(&xf(line.frame));
|
||||
s.set_stroke(None);
|
||||
s.set_fill(Some(Fill { paint, opacity: NormalizedF32::ONE, rule: krilla::paint::FillRule::NonZero }));
|
||||
s.draw_glyphs(krilla::geom::Point::from_xy(0.0, 0.0), &glyphs, line.font, text, size as f32, false);
|
||||
if let Some(paint) = self.paint_in(s, fill, bounds, line.frame.inverse()) {
|
||||
s.set_stroke(None);
|
||||
s.set_fill(Some(Fill { paint, opacity: NormalizedF32::ONE, rule: krilla::paint::FillRule::NonZero }));
|
||||
s.draw_glyphs(krilla::geom::Point::from_xy(0.0, 0.0), &glyphs, line.font, text, size as f32, false);
|
||||
}
|
||||
s.pop();
|
||||
}
|
||||
|
||||
@@ -1558,6 +1627,10 @@ impl Exporter<'_> {
|
||||
}
|
||||
|
||||
fn image(&mut self, s: &mut Surface, im: &vectorcraft_doc::ImageObject) {
|
||||
// An image placed or sized beyond [`MAX_COORD`] is left out.
|
||||
if !writable(s, im.xf, Rect::new(0.0, 0.0, im.width.max(1).into(), im.height.max(1).into())) {
|
||||
return;
|
||||
}
|
||||
// The size it is placed at: its pixel grid's sides through its transform.
|
||||
let [a, b, c, d, ..] = im.xf.as_coeffs();
|
||||
let size = (im.width as f64 * a.hypot(b), im.height as f64 * c.hypot(d));
|
||||
@@ -1586,3 +1659,29 @@ fn embed(blob: &vectorcraft_doc::ImageBlob, interpolate: bool) -> Option<Image>
|
||||
Some(Image::from_rgba8(rgba.into_raw(), w, h))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn writable_bounds_values_the_transform_and_values_through_it() {
|
||||
let mut pdf = krilla::Document::new();
|
||||
let mut page = pdf.start_page_with(PageSettings::new(Size::from_wh(100.0, 100.0).unwrap()));
|
||||
let mut s = page.surface();
|
||||
let r = Rect::new(0.0, 0.0, 10.0, 10.0);
|
||||
assert!(writable(&s, Affine::IDENTITY, r));
|
||||
assert!(!writable(&s, Affine::IDENTITY, Rect::new(-2_147_483_649.0, 0.0, 10.0, 10.0)));
|
||||
assert!(!writable(&s, Affine::IDENTITY, Rect::new(f64::NAN, 0.0, 10.0, 10.0)));
|
||||
assert!(!writable(&s, Affine::translate((0.0, 2e9)), r));
|
||||
assert!(!writable(&s, Affine::scale(2e9), Rect::ZERO));
|
||||
// `writable` also checks the surface's own transform: with the surface moved to
|
||||
// x = MAX_COORD, a point at the origin fits and a 10 by 10 box does not.
|
||||
s.push_transform(&xf(Affine::translate((MAX_COORD, 0.0))));
|
||||
assert!(writable(&s, Affine::IDENTITY, Rect::ZERO));
|
||||
assert!(!writable(&s, Affine::IDENTITY, r));
|
||||
s.pop();
|
||||
s.finish();
|
||||
page.finish();
|
||||
}
|
||||
}
|
||||
+74
-1
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_color::{BlendMode, Color, Gradient, GradientKind, GradientPaint, Paint};
|
||||
use vectorcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, Paint};
|
||||
use vectorcraft_doc::{
|
||||
Appearance, AppearanceItem, Artboard, CharStyle, Dash, Document, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, TextObject,
|
||||
};
|
||||
@@ -268,6 +268,79 @@ fn objects_outside_artboard_are_culled() {
|
||||
assert_eq!(leaves(&roundtrip(&d)).len(), 1);
|
||||
}
|
||||
|
||||
/// A line from (10, `y`) to (90, `y`).
|
||||
fn line(y: f64) -> PathData {
|
||||
let mut bp = vectorcraft_geom::BezPath::new();
|
||||
bp.move_to((10.0, y));
|
||||
bp.line_to((90.0, y));
|
||||
PathData::from_bezpath(&bp)
|
||||
}
|
||||
|
||||
/// Art that the PDF would hold a value beyond a billion points for is left out: a path that far,
|
||||
/// a gradient placed that far, a stroke so wide that its group's box reaches that far, and an
|
||||
/// image placed that far. Each would be written with -2,147,483,648, which the PDF writer panics
|
||||
/// on in builds with overflow checks.
|
||||
#[test]
|
||||
fn art_reaching_beyond_a_billion_points_is_left_out() {
|
||||
let far = -2_147_483_649.0;
|
||||
let shaded = |kind, start, end| {
|
||||
let mut n = rect_node(Rect::new(60.0, 10.0, 90.0, 50.0), Color::BLACK);
|
||||
let geom = GradientGeom { start, end, aspect: 1.0, focal: None };
|
||||
let g = GradientPaint { geom: Some(geom), ..GradientPaint::new(Gradient { kind, ..Gradient::default() }) };
|
||||
n.appearance.set_fill(Paint::Gradient(Box::new(g)));
|
||||
n
|
||||
};
|
||||
let mut wide = Node::path(NodeId(0), line(0.0), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 4_294_967_296.0));
|
||||
wide.opacity = 0.5;
|
||||
let image = ImageObject {
|
||||
key: "img1".into(),
|
||||
width: 4,
|
||||
height: 2,
|
||||
xf: Affine::new([5.5e8, 0.0, 0.0, 10.0, far, 60.0]),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
};
|
||||
for (what, n) in [
|
||||
("path", rect_node(Rect::new(far, 60.0, 90.0, 90.0), Color::BLACK)),
|
||||
("linear gradient", shaded(GradientKind::Linear, Point::new(far, 0.0), Point::new(90.0, 0.0))),
|
||||
("radial gradient", shaded(GradientKind::Radial, Point::new(far, 30.0), Point::new(far + 100.0, 30.0))),
|
||||
("stroke", wide),
|
||||
("image", Node::new(NodeId(0), NodeKind::Image(Box::new(image)))),
|
||||
] {
|
||||
let mut d = doc(100.0, 100.0);
|
||||
d.images.insert("img1".into(), ImageBlob::new("image/png", tiny_png()));
|
||||
add(&mut d, rect_node(Rect::new(10.0, 10.0, 50.0, 50.0), Color::BLACK));
|
||||
add(&mut d, n);
|
||||
assert_eq!(leaves(&roundtrip(&d)).len(), 1, "{what}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A dash offset beyond a billion points is written as its remainder after whole periods of the
|
||||
/// pattern: -2,147,483,649 (-2,147,483,648 as an f32, which the PDF writer panics on in builds
|
||||
/// with overflow checks) is 1 in a pattern of 5 and 5.
|
||||
#[test]
|
||||
fn dash_offsets_beyond_a_billion_points_wrap_into_their_pattern() {
|
||||
let mut d = doc(100.0, 100.0);
|
||||
let mut n = Node::path(NodeId(0), line(50.0), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0));
|
||||
n.appearance.stroke_mut().unwrap().dash = Some(Dash { pattern: vec![5.0, 5.0], offset: -2_147_483_649.0, align_corners: false });
|
||||
add(&mut d, n);
|
||||
assert!(uncompressed(&d).contains("[5 5]1 d"));
|
||||
}
|
||||
|
||||
/// The page of an artboard beyond a billion points is left blank: what is on it would be placed
|
||||
/// through -2,147,483,648, which the PDF writer panics on in builds with overflow checks.
|
||||
#[test]
|
||||
fn artboards_beyond_a_billion_points_export_blank_pages() {
|
||||
let mut d = doc(100.0, 100.0);
|
||||
d.artboards[0].rect = Rect::new(2_147_483_649.0, 0.0, 2_147_483_749.0, 100.0);
|
||||
let mut n = rect_node(Rect::new(2_147_483_659.0, 10.0, 2_147_483_699.0, 50.0), Color::BLACK);
|
||||
n.opacity = 0.5;
|
||||
add(&mut d, n);
|
||||
let bytes = export(&d, &PdfOptions::default()).unwrap();
|
||||
assert_eq!(page_count(&bytes), 1);
|
||||
assert!(leaves(&import(&bytes).unwrap()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opacity_and_blend_modes() {
|
||||
let mut d = doc(200.0, 200.0);
|
||||
|
||||
@@ -110,6 +110,58 @@ fn real_text_lands_within_half_a_point_of_the_outlines() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A run shifted two billion points off its baseline is left out, as real text or outlines, and
|
||||
/// the rest of the type is written. On a page 50 points high, the page's y flip puts the shifted
|
||||
/// run's baseline at -2,147,483,648, which the PDF writer panics on in builds with overflow checks.
|
||||
#[test]
|
||||
fn a_run_shifted_two_billion_points_is_left_out() {
|
||||
let mut t = TextObject::point(Point::new(5.0, 40.0), "", style(12.0));
|
||||
t.runs = vec![
|
||||
TextRun { text: "in".into(), style: style(12.0), inline: None },
|
||||
TextRun { text: "out".into(), style: CharStyle { baseline_shift: -2_147_483_649.0, ..style(12.0) }, inline: None },
|
||||
];
|
||||
let mut d = doc(vec![t]);
|
||||
d.artboards[0].rect = Rect::new(0.0, 0.0, 50.0, 50.0);
|
||||
assert_eq!(texts(&import_as(&pdf(&d, false).bytes, TextAs::Text)), ["in"]);
|
||||
let b = ink(&import_as(&pdf(&d, true).bytes, TextAs::Outlines));
|
||||
assert!(b.y0 > 0.0 && b.y1 < 50.0, "{b:?}");
|
||||
}
|
||||
|
||||
/// An overprinting run is written, or left out, as the same run is without overprint: the form
|
||||
/// marked to overprint the run holds the type's transform. For type moved 2,147,483,689 points down
|
||||
/// with its run shifted back onto a page 50 points high, a form placed through that transform would
|
||||
/// sit at y = -2,147,483,648, which the PDF writer panics on in builds with overflow checks. For
|
||||
/// type scaled 3 times, a form without that transform would hold a run shifted 500,000,000 points
|
||||
/// up, and the export would write the run 1,500,000,000 points up.
|
||||
#[test]
|
||||
fn overprinting_runs_are_written_or_left_out_as_other_runs_are() {
|
||||
let moved = |overprint_fill| {
|
||||
let back = CharStyle { baseline_shift: 2_147_483_649.0, overprint_fill, ..style(12.0) };
|
||||
TextObject::point(Point::new(5.0, 2_147_483_689.0), "moved", back)
|
||||
};
|
||||
let scaled = |overprint_fill| {
|
||||
let mut t = TextObject::point(Point::new(5.0, 40.0), "", style(12.0));
|
||||
t.xf *= Affine::scale(3.0);
|
||||
let up = CharStyle { baseline_shift: 500_000_000.0, overprint_fill, ..style(12.0) };
|
||||
t.runs = vec![TextRun { text: "in".into(), style: style(12.0), inline: None }, TextRun { text: "out".into(), style: up, inline: None }];
|
||||
t
|
||||
};
|
||||
let written = |mut t: TextObject| {
|
||||
// The export culls type by the laid-out bounds that the engine keeps in `cached_bounds`.
|
||||
t.cached_bounds = Some(vectorcraft_text::layout(FontDb::global(), &t).bounds);
|
||||
let mut d = doc(vec![t]);
|
||||
d.artboards[0].rect = Rect::new(0.0, 0.0, 50.0, 50.0);
|
||||
pdf(&d, false).bytes
|
||||
};
|
||||
let text_of = |bytes: &[u8]| texts(&import_as(bytes, TextAs::Text));
|
||||
let (on, off) = (written(moved(true)), written(moved(false)));
|
||||
assert!(String::from_utf8_lossy(&on).contains("/VCop gs"), "an overprinting form");
|
||||
assert_eq!(text_of(&on), ["moved"]);
|
||||
assert_eq!(text_of(&off), ["moved"]);
|
||||
assert_eq!(text_of(&written(scaled(true))), ["in"]);
|
||||
assert_eq!(text_of(&written(scaled(false))), ["in"]);
|
||||
}
|
||||
|
||||
/// A named instance of a variable font is real text in that instance: its embedded subset draws
|
||||
/// the instance's glyphs where the outlines would be, not the default instance's (#296).
|
||||
#[test]
|
||||
|
||||
@@ -4194,3 +4194,377 @@ lighting Light Luz
|
||||
@msg the file stopped being read (please report this bug) se dejó de leer el archivo (informe de este error)
|
||||
Concentric Dividers: Divisiones concéntricas:
|
||||
Radial Dividers: Divisiones radiales:
|
||||
|
||||
# Rows other catalogs already had: document and print presets, keyboard-shortcut editor, print and
|
||||
# PDF options, arrowheads, harmony rules, Save for Web, graphic styles and file filters.
|
||||
@id pathfinder.add Unir
|
||||
3 x 3 Average Promedio de 3 x 3
|
||||
5 x 5 Average Promedio de 5 x 5
|
||||
PDF File Archivo PDF
|
||||
1 Color Job Trabajo a 1 color
|
||||
2 Color Job Trabajo a 2 colores
|
||||
3 Color Job Trabajo a 3 colores
|
||||
CMYK profile in effect Perfil CMYK en vigor
|
||||
CMYK Equivalents Equivalentes CMYK
|
||||
effect Soft Suave
|
||||
effect Horizontal Horizontal
|
||||
effect Vertical Vertical
|
||||
theme Appearance Apariencia
|
||||
effect Points Puntos
|
||||
effect Regular Regular
|
||||
Phone 393×852 Teléfono 393×852
|
||||
Phone 430×932 Teléfono 430×932
|
||||
Phone 360×800 Teléfono 360×800
|
||||
Phone 412×915 Teléfono 412×915
|
||||
Phone 375×667 Teléfono 375×667
|
||||
Tablet 768×1024 Tableta 768×1024
|
||||
Tablet 820×1180 Tableta 820×1180
|
||||
Tablet 834×1194 Tableta 834×1194
|
||||
Tablet 1024×1366 Tableta 1024×1366
|
||||
Tablet 800×1280 Tableta 800×1280
|
||||
Watch 184×224 Reloj 184×224
|
||||
Watch 198×242 Reloj 198×242
|
||||
Web 1600×900 Web 1600×900
|
||||
Web 1440×900 Web 1440×900
|
||||
Web 1366×768 Web 1366×768
|
||||
Web 1280×800 Web 1280×800
|
||||
Web 1024×768 Web 1024×768
|
||||
Legal Legal
|
||||
Tabloid Tabloide
|
||||
A3 A3
|
||||
A4 A4
|
||||
A5 A5
|
||||
B4 B4
|
||||
B5 B5
|
||||
HDTV 1080p HDTV 1080p
|
||||
HDTV 720p HDTV 720p
|
||||
4K UHD 4K UHD
|
||||
8K UHD 8K UHD
|
||||
2K Cinema Cine 2K
|
||||
4K Cinema Cine 4K
|
||||
Greeting Card Tarjeta de felicitación
|
||||
Square 8 × 8 in Cuadrado de 8 × 8 pulgadas
|
||||
Poster 18 × 24 in Póster de 18 × 24 pulgadas
|
||||
Poster 24 × 36 in Póster de 24 × 36 pulgadas
|
||||
Logo 1000×1000 Logotipo 1000×1000
|
||||
Business Card 3.5 × 2 in Tarjeta de presentación de 3.5 × 2 pulgadas
|
||||
Business Card 85 × 55 mm Tarjeta de presentación de 85 × 55 mm
|
||||
Letterhead Letter Membrete Carta
|
||||
Letterhead A4 Membrete A4
|
||||
Envelope DL Sobre DL
|
||||
Envelope #10 Sobre #10
|
||||
Social Portrait Post 1080×1350 Publicación vertical para redes 1080×1350
|
||||
Social Landscape Post 1200×630 Publicación horizontal para redes 1200×630
|
||||
Social Story 1080×1920 Historia para redes 1080×1920
|
||||
Social Tall Post 1000×1500 Publicación alta para redes 1000×1500
|
||||
Profile Cover 1500×500 Portada de perfil 1500×500
|
||||
Page Cover 1640×624 Portada de página 1640×624
|
||||
Video Banner 2560×1440 Banner de video 2560×1440
|
||||
Video Thumbnail 1280×720 Miniatura de video 1280×720
|
||||
Keyboard Shortcuts Métodos abreviados de teclado
|
||||
Set: Conjunto:
|
||||
Reset to Defaults Restablecer valores predeterminados
|
||||
Tool Herramienta
|
||||
Command Comando
|
||||
Shortcut Método abreviado
|
||||
Menu Commands Comandos de menú
|
||||
Press keys… Presiona las teclas…
|
||||
Use Default Usar predeterminado
|
||||
Go to Conflict Ir al conflicto
|
||||
Remove the shortcut Quitar el método abreviado
|
||||
Click, then press the new shortcut (Esc cancels) Haz clic y presiona el nuevo método abreviado (Esc cancela)
|
||||
All shortcuts reset to defaults. Se restablecieron todos los métodos abreviados a los valores predeterminados.
|
||||
Imported. Press OK to keep the imported set. Importado. Presiona Aceptar para conservar el conjunto importado.
|
||||
⚠ {chord} was already used by {names} — it has been removed there. Atención: {chord} ya estaba en uso por {names}; se quitó de allí.
|
||||
VectorCraft Defaults Valores predeterminados de VectorCraft
|
||||
@plural {n} shortcut is assigned more than once|{n} shortcuts are assigned more than once {n} método abreviado está asignado más de una vez|{n} métodos abreviados están asignados más de una vez
|
||||
Simulate prints overprints as Overprint Preview shows them; separations always keep them. A preset flattens transparency before printing (PostScript files always flatten it). «Simular» imprime las sobreimpresiones tal como las muestra la vista previa de sobreimpresión; las separaciones siempre las conservan. Un preajuste acopla la transparencia antes de imprimir (los archivos PostScript siempre la acoplan).
|
||||
Preserve Conservar
|
||||
Simulate Simular
|
||||
Portrait (flipped) Vertical (invertida)
|
||||
Landscape (flipped) Horizontal (invertida)
|
||||
Visible & Printable Layers Capas visibles e imprimibles
|
||||
Visible Layers Capas visibles
|
||||
All Layers Todas las capas
|
||||
Do Not Scale No escalar
|
||||
Fit to Page Ajustar a la página
|
||||
Custom Scale Escala personalizada
|
||||
Tile Imageable Areas Mosaico de áreas imprimibles
|
||||
Composite Compuesto
|
||||
Separations Separaciones
|
||||
Up (right reading) Arriba (lectura correcta)
|
||||
Down (right reading) Abajo (lectura correcta)
|
||||
Positive Positivo
|
||||
Negative Negativo
|
||||
Subset Subconjunto
|
||||
Complete Completa
|
||||
[Custom] [Personalizado]
|
||||
[Default] [Predeterminado]
|
||||
No downsampling Sin reducción de resolución
|
||||
Subsample Submuestreo
|
||||
Bicubic Bicúbica
|
||||
Minimum Mínima
|
||||
Run Length Compresión por longitud de secuencia
|
||||
Roman Romano
|
||||
No conversion Sin conversión
|
||||
Convert to destination Convertir al destino
|
||||
Convert to destination (preserve numbers) Convertir al destino (conservar valores)
|
||||
Don't include profiles No incluir perfiles
|
||||
Include all profiles Incluir todos los perfiles
|
||||
Include destination profile Incluir perfil de destino
|
||||
Include tagged source profiles Incluir perfiles de origen etiquetados
|
||||
Low resolution (150 ppi) Baja resolución (150 ppp)
|
||||
High resolution Alta resolución
|
||||
Inserting, deleting and rotating pages Insertar, eliminar y rotar páginas
|
||||
Filling in form fields and signing Rellenar campos de formulario y firmar
|
||||
Commenting, filling in form fields and signing Comentar, rellenar campos de formulario y firmar
|
||||
Any except extracting pages Cualquiera excepto extraer páginas
|
||||
Bounding Box Cuadro delimitador
|
||||
Mobile Móvil
|
||||
Film & Video Cine y video
|
||||
Art & Illustration Arte e ilustración
|
||||
Branding Identidad de marca
|
||||
Social Redes sociales
|
||||
Triangle Triángulo
|
||||
Triangle (open) Triángulo (abierto)
|
||||
Circle (open) Círculo (abierto)
|
||||
Square (open) Cuadrado (abierto)
|
||||
Bar Barra
|
||||
Arrow Flecha
|
||||
Arrow (open) Flecha (abierta)
|
||||
Barbed Con púas
|
||||
Half Arrow (left) Media flecha (izquierda)
|
||||
Half Arrow (right) Media flecha (derecha)
|
||||
Concave Cóncava
|
||||
Double Bar Doble barra
|
||||
Feather Emplumado
|
||||
Dot on Bar Punto sobre barra
|
||||
Chevron Galón
|
||||
Double Arrow Doble flecha
|
||||
Cross Cruz
|
||||
Plus Signo más
|
||||
Hexagon Hexágono
|
||||
Hexagon (open) Hexágono (abierto)
|
||||
Tag Etiqueta
|
||||
Tag (open) Etiqueta (abierta)
|
||||
Half Circle Semicírculo
|
||||
Drop Gota
|
||||
Slash Barra inclinada
|
||||
Double Slash Doble barra inclinada
|
||||
Diamond (open) Rombo (abierto)
|
||||
Reverse Triangle Triángulo invertido
|
||||
Swallowtail Cola de golondrina
|
||||
Bracket Corchete
|
||||
Fork Horquilla
|
||||
Leaf Hoja
|
||||
Kite Cometa
|
||||
Triangle to Bar Triángulo a barra
|
||||
Oval Óvalo
|
||||
Complementary Complementario
|
||||
Complementary 2 Complementario 2
|
||||
Split Complementary Complementario dividido
|
||||
Left Complement Complemento izquierdo
|
||||
Right Complement Complemento derecho
|
||||
Analogous Análogo
|
||||
Analogous 2 Análogo 2
|
||||
Monochromatic Monocromático
|
||||
Shades Sombras
|
||||
Triad Tríada
|
||||
Triad 2 Tríada 2
|
||||
Triad 3 Tríada 3
|
||||
Tetrad Tétrada
|
||||
Tetrad 2 Tétrada 2
|
||||
Tetrad 3 Tétrada 3
|
||||
Compound Compuesto
|
||||
Compound 2 Compuesto 2
|
||||
High Contrast Alto contraste
|
||||
High Contrast 2 Alto contraste 2
|
||||
High Contrast 3 Alto contraste 3
|
||||
Pentagram Pentagrama
|
||||
Perceptual Perceptual
|
||||
Relative Colorimetric Colorimétrico relativo
|
||||
Absolute Colorimetric Colorimétrico absoluto
|
||||
Exact Exacto
|
||||
Preserve Tints Conservar matices
|
||||
Scale Tints Escalar matices
|
||||
Linear Lineal
|
||||
Freeform Libre
|
||||
8 Colors 8 colores
|
||||
16 Colors 16 colores
|
||||
256 Colors 256 colores
|
||||
True Colors Colores verdaderos
|
||||
Preserve Appearance Conservar apariencia
|
||||
Maximize Editability Maximizar editabilidad
|
||||
Clip Group Grupo de recorte
|
||||
Guide Guía
|
||||
Symbol Símbolo
|
||||
Envelope Envolvente
|
||||
Mesh Malla
|
||||
Placed Document Documento colocado
|
||||
Radial Repeat Repetición radial
|
||||
Grid Repeat Repetición de cuadrícula
|
||||
Mirror Repeat Repetición reflejada
|
||||
Auto Size Tamaño automático
|
||||
Shrink Text to Fit Reducir texto para que quepa
|
||||
Ascent Ascendente
|
||||
Cap Height Altura de mayúsculas
|
||||
x Height Altura de la x
|
||||
Justify Justificar
|
||||
First Baseline Primera línea de base
|
||||
Vertical Align Alineación vertical
|
||||
Save Selection Guardar selección
|
||||
Twirl Molinete
|
||||
Pucker Fruncir
|
||||
Bloat Engordar
|
||||
Scallop Festonear
|
||||
Wrinkle Arrugar
|
||||
Unknown Desconocido
|
||||
Copyrighted Con copyright
|
||||
Public Domain Dominio público
|
||||
None (Color Preview) Ninguno (vista previa de color)
|
||||
Rasterized Complex Regions Regiones complejas rasterizadas
|
||||
Transparent Objects Objetos transparentes
|
||||
All Affected Objects Todos los objetos afectados
|
||||
Expanded Patterns Motivos expandidos
|
||||
Outlined Strokes Trazos contorneados
|
||||
Outlined Text Texto contorneado
|
||||
All Rasterized Regions Todas las regiones rasterizadas
|
||||
High Resolution Alta resolución
|
||||
Medium Resolution Resolución media
|
||||
Low Resolution Baja resolución
|
||||
Copyright and Contact Info Copyright e información de contacto
|
||||
All Slices Todos los sectores
|
||||
Selected Slices Sectores seleccionados
|
||||
No Slices Sin sectores
|
||||
Images Only Solo imágenes
|
||||
HTML and Images HTML e imágenes
|
||||
Selective Selectivo
|
||||
Adaptive Adaptativo
|
||||
Black & White Blanco y negro
|
||||
No Dither Sin tramado
|
||||
Diffusion Difusión
|
||||
Unsorted Sin ordenar
|
||||
Sort by Hue Ordenar por tono
|
||||
Sort by Luminance Ordenar por luminancia
|
||||
Sort by Popularity Ordenar por popularidad
|
||||
Baseline (Standard) Secuencial (estándar)
|
||||
Baseline Optimized Secuencial optimizado
|
||||
Art Optimized Optimizado para ilustración
|
||||
Type Optimized Optimizado para texto
|
||||
Language Level 2 Nivel de lenguaje 2
|
||||
Language Level 3 Nivel de lenguaje 3
|
||||
TIFF (Black & White) TIFF (blanco y negro)
|
||||
TIFF (8-bit Color) TIFF (color de 8 bits)
|
||||
1 bit 1 bit
|
||||
4 bit 4 bits
|
||||
8 bit 8 bits
|
||||
16 bit 16 bits
|
||||
24 bit 24 bits
|
||||
32 bit 32 bits
|
||||
16 bits/pixel 16 bits/píxel
|
||||
24 bits/pixel 24 bits/píxel
|
||||
32 bits/pixel 32 bits/píxel
|
||||
DWG (use DXF) DWG (usa DXF)
|
||||
file-filter All readable files Todos los archivos legibles
|
||||
file-filter All placeable files Todos los archivos que se pueden colocar
|
||||
file-filter Swatch libraries Bibliotecas de muestras
|
||||
file-filter Flattener presets Preajustes del acoplador
|
||||
file-filter PDF presets Preajustes de PDF
|
||||
file-filter Print presets Preajustes de impresión
|
||||
file-filter SVG Compressed SVG comprimido
|
||||
file-filter PDF-compatible .ai .ai compatible con PDF
|
||||
file-filter VectorCraft Template Plantilla de VectorCraft
|
||||
file-filter Templates Plantillas
|
||||
diagnostics render renderizado
|
||||
diagnostics ui interfaz
|
||||
Preset {number} Preajuste {number}
|
||||
PRs PR
|
||||
@{login}: {prs}, {commits}, +{added} / −{deleted} lines (Δ {delta}), +{binary_added} / −{binary_deleted} binary assets, {first} – {last} @{login}: {prs}, {commits}, +{added} / −{deleted} líneas (Δ {delta}), +{binary_added} / −{binary_deleted} recursos binarios, {first} – {last}
|
||||
@plural {n} PR|{n} PRs {n} PR|{n} PR
|
||||
@plural {n} commit|{n} commits {n} commit|{n} commits
|
||||
gradient-kind Radial Radial
|
||||
mark-style Japanese Japonesas
|
||||
2-Up 2 vistas
|
||||
2up 2 vistas
|
||||
3 Colors 3 colores
|
||||
4-Up 4 vistas
|
||||
4up 4 vistas
|
||||
6 Colors 6 colores
|
||||
A6 A6
|
||||
Alt+Click Alt+clic
|
||||
Alt+Drag Alt+arrastrar
|
||||
Anchors Anclas
|
||||
Area Type Texto de área
|
||||
Basic Básico
|
||||
Black Outline Contorno negro
|
||||
Brown Marrón
|
||||
Convert to Global Convertir en global
|
||||
Cool Fríos
|
||||
CRLF (Windows) CRLF (Windows)
|
||||
Current Colors Colores actuales
|
||||
Default Graphic Style Estilo gráfico predeterminado
|
||||
Direct Selection Selección directa
|
||||
Distance: Distancia:
|
||||
GIF, 128 colours GIF, 128 colores
|
||||
Gray Gris
|
||||
Grey Gris
|
||||
Group Selection Selección de grupos
|
||||
Hand Mano
|
||||
Harmony Rules Reglas de armonía
|
||||
Heavy Ink Tinta densa
|
||||
High Fidelity Photo Foto de alta fidelidad
|
||||
Image Trace (Make and Expand) Calco de imagen (crear y expandir)
|
||||
Joins: Uniones:
|
||||
Keeps the document editable: VectorCraft reopens the PDF with nothing lost. For files you'll edit again. Mantiene el documento editable: VectorCraft vuelve a abrir el PDF sin perder nada. Para archivos que volverás a editar.
|
||||
Lab Spot Colors Tintas planas Lab
|
||||
Lasso Lazo
|
||||
Length: Longitud:
|
||||
LF (macOS, Linux) LF (macOS, Linux)
|
||||
Line Art Arte lineal
|
||||
Low Fidelity Photo Foto de baja fidelidad
|
||||
Make Text Wrap Crear ceñido de texto
|
||||
Measure Medición
|
||||
Monochrome Monocromo
|
||||
Muted Apagados
|
||||
of de
|
||||
Orange Naranja
|
||||
Pattern Swatches Muestras de motivo
|
||||
PDF 1.7 PDF 1.7
|
||||
Pen Pluma
|
||||
Pink Rosa
|
||||
Pixel Píxel
|
||||
Points: Puntos:
|
||||
Purple Púrpura
|
||||
Release Image Trace Soltar calco de imagen
|
||||
Release Text Wrap Soltar ceñido de texto
|
||||
RGB RGB
|
||||
Rotate View Rotar vista
|
||||
Scissors Tijeras
|
||||
Shades of Gray Tonos de gris
|
||||
Shear: Inclinar:
|
||||
Shift+Click Shift+clic
|
||||
Shift+Drag Shift+arrastrar
|
||||
Silhouettes Siluetas
|
||||
Sketched Art Arte esbozado
|
||||
Spectrum Espectro
|
||||
sRGB IEC61966-2.1 sRGB IEC61966-2.1
|
||||
Sunshine Soleado
|
||||
SVG 1.1 SVG 1.1
|
||||
Technical Drawing Dibujo técnico
|
||||
Text Displayed in Cell Texto mostrado en la celda
|
||||
Touch Type Retocar texto
|
||||
UTF-16 (Unicode) UTF-16 (Unicode)
|
||||
Variation Variación
|
||||
VectorCraft Default Predeterminado de VectorCraft
|
||||
VectorCraft Generic CMYK (SWOP-like) CMYK genérico de VectorCraft (similar a SWOP)
|
||||
VectorCraft Swatches (.vcswatches) Muestras de VectorCraft (.vcswatches)
|
||||
Vertical Area Type Texto de área vertical
|
||||
Vertical Type Texto vertical
|
||||
Vertical Type on a Path Texto vertical en trazado
|
||||
Vivid Vivos
|
||||
Warm Cálidos
|
||||
WebP WebP
|
||||
Working RGB RGB de trabajo
|
||||
X: X:
|
||||
Y: Y:
|
||||
@id file.close Cerrar archivo
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -114,7 +114,8 @@ fn set_view(app: &mut VectorcraftApp, view: TraceView) {
|
||||
/// The selected Image Trace object's View row (Control bar, Properties).
|
||||
pub(crate) fn view_row(app: &mut VectorcraftApp, ui: &mut Ui, current: TraceView, width: f32) {
|
||||
widgets::dim_label(ui, tl!("View:"));
|
||||
if let Some(v) = view_dropdown(ui, current, width) {
|
||||
// No wider than what the label leaves of the row (#1358).
|
||||
if let Some(v) = view_dropdown(ui, current, width.min(ui.available_width())) {
|
||||
set_view(app, v);
|
||||
}
|
||||
}
|
||||
@@ -419,6 +420,31 @@ mod tests {
|
||||
app
|
||||
}
|
||||
|
||||
/// #1358: in Properties the View row fits the panel, so the panel doesn't widen and clip its
|
||||
/// labels (the row took the panel's whole width for its menu, beside its label).
|
||||
#[test]
|
||||
fn the_view_row_fits_the_properties_panel() {
|
||||
let mut app = traced_app();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
// Where the panel's content ends after a few frames.
|
||||
let right = |app: &mut VectorcraftApp| {
|
||||
let mut right = 0.0;
|
||||
for _ in 0..3 {
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, vec2(280.0, 700.0));
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| {
|
||||
crate::panels::properties::show(app, ui);
|
||||
right = ui.min_rect().right();
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
right
|
||||
};
|
||||
let traced = right(&mut app);
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
assert_eq!(traced, right(&mut app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_is_a_section_that_stays_as_it_was_left() {
|
||||
let mut app = traced_app();
|
||||
|
||||
@@ -95,7 +95,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
// The user's choice alone: a window too short for the tools scrolls them.
|
||||
let cols = if app.ui.toolbar_double { 2 } else { 1 };
|
||||
let w = if cols == 2 { 76.0 } else { WIDTH };
|
||||
// Two 36 pt buttons with 2 pt either side inside the border and separator line (at 76 they
|
||||
// overran the right border).
|
||||
let w = if cols == 2 { 80.0 } else { WIDTH };
|
||||
// Where the panel docks: along the window's left edge, under the bars.
|
||||
let edge = ui.available_rect_before_wrap();
|
||||
ui.ctx().data_mut(|d| d.insert_temp(crate::floating::tools_zone_id(), egui::Rect::from_min_size(edge.min, vec2(WIDTH, edge.height()))));
|
||||
@@ -199,11 +201,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
if cols == 1 {
|
||||
ui.add_space((WIDTH - 36.0) / 2.0);
|
||||
} else {
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
// Centred on the panel's inside (`bounds`: within its border and, docked, the
|
||||
// separator line egui reserves on its right), not on `WIDTH`, which sat them 2½ pt
|
||||
// right of centre docked; nor on this row, which the scroll area starts ½ pt in.
|
||||
ui.add_space((bounds.center().x - cols as f32 * 18.0 - ui.cursor().min.x).max(0.0));
|
||||
for &k in &row {
|
||||
let slot = &all[k].1;
|
||||
let shown_id = if slot.contains(&active) {
|
||||
@@ -862,6 +863,36 @@ pub(crate) mod tests {
|
||||
assert_eq!(frame_in(&mut app, &ctx, 8.0, vec![], 500.0)[0], tools[0]);
|
||||
}
|
||||
|
||||
/// The tool buttons sit in the middle of the panel, between its border and the separator
|
||||
/// line, docked and floating, in one or two columns (docked, they sat 2½ pt right of centre).
|
||||
#[test]
|
||||
fn the_tool_buttons_are_centred_in_the_panel() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
for (double, floating) in [(false, None), (true, None), (false, Some([100.0, 100.0])), (true, Some([100.0, 100.0]))] {
|
||||
app.run("window.toolbarColumns", json!({ "double": double })).unwrap();
|
||||
app.ui.toolbar_pos = floating;
|
||||
frame(&mut app, &ctx, 0.0, vec![]);
|
||||
let tools = frame(&mut app, &ctx, 0.1, vec![]);
|
||||
// The title bar spans the panel's inside.
|
||||
let hdr = ctx.viewport(|vp| {
|
||||
vp.prev_pass
|
||||
.widgets
|
||||
.layers()
|
||||
.flat_map(|(_, w)| w.iter())
|
||||
.filter(|w| w.rect.height() == 14.0 && w.rect.width() > 40.0)
|
||||
.map(|w| w.rect)
|
||||
.next()
|
||||
});
|
||||
let hdr = hdr.unwrap();
|
||||
let first = tools[0];
|
||||
let row_right = tools.iter().filter(|r| r.top() == first.top()).map(|r| r.right()).fold(first.right(), f32::max);
|
||||
assert_eq!(row_right - hdr.center().x, hdr.center().x - first.left(), "double {double}, floating {floating:?}: {first:?} in {hdr:?}");
|
||||
assert!(first.left() >= hdr.left() && row_right <= hdr.right(), "double {double}, floating {floating:?}: {first:?} in {hdr:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #812: double-clicking the Artboard tool opens Artboard Options of the active artboard, a
|
||||
/// graph tool Graph Type for the selected graph, and the Magic Wand tool its panel.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user