Merge pull request #4910 from dotCooCoo/fix/issue-3325

Fix #3325: art nested in Affinity text is clipped to the characters
This commit is contained in:
BFlat
2026-10-31 23:48:22 +07:00
committed by GitHub
4 changed files with 70 additions and 20 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ Affinity files.
| Curves (`PCrv`): cubic subpaths, closed flags, live corners (`CnrD`) | paths (multi-subpath curves fill even-odd) |
| 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) |
| A shape, curve or text with children clips them | clip group whose clipping path keeps its fill (behind) and stroke (over) |
| Vector masks (`AdCh` curves and shapes) | clip group |
| Pixel masks (`MRst`, attached or as a layer in a group) | opacity mask |
| Solid fills: RGBA, HSLA, CMYK, grey, Lab (D50) | fill appearance (alpha as fill opacity) |
+19 -13
View File
@@ -98,20 +98,26 @@ pub fn utf8(text: &str) -> F {
}
pub fn document(blocks: Vec<F>, frame: bool, dpi: f64, transform: [f64; 6]) -> Vec<u8> {
let text = F::Obj(
tag(if frame { b"TxtF" } else { b"TxtA" }),
vec![
(tag(b"StSt"), F::Obj(tag(b"Stry"), vec![(tag(b"Blok"), F::Shared(blocks))])),
(
tag(b"TxtH"),
F::Obj(
tag(if frame { b"FrFr" } else { b"ArFr" }),
vec![(tag(b"FrmB"), F::F64s(vec![10.0, 20.0, 210.0, 120.0])), (tag(b"ArtV"), F::F64(18.0))],
),
document_with_children(blocks, frame, dpi, transform, vec![])
}
/// [`document`] whose text holds `children` (objects nested inside the text).
pub fn document_with_children(blocks: Vec<F>, frame: bool, dpi: f64, transform: [f64; 6], children: Vec<F>) -> Vec<u8> {
let mut fields = vec![
(tag(b"StSt"), F::Obj(tag(b"Stry"), vec![(tag(b"Blok"), F::Shared(blocks))])),
(
tag(b"TxtH"),
F::Obj(
tag(if frame { b"FrFr" } else { b"ArFr" }),
vec![(tag(b"FrmB"), F::F64s(vec![10.0, 20.0, 210.0, 120.0])), (tag(b"ArtV"), F::F64(18.0))],
),
(tag(b"Xfrm"), F::F64s(transform.to_vec())),
],
);
),
(tag(b"Xfrm"), F::F64s(transform.to_vec())),
];
if !children.is_empty() {
fields.push((tag(b"Chld"), F::Shared(children)));
}
let text = F::Obj(tag(if frame { b"TxtF" } else { b"TxtA" }), fields);
let mut root = vec![
(tag(b"UVCn"), F::Obj(tag(b"UVCn"), vec![(tag(b"UPPI"), F::F64(dpi))])),
(
+11 -6
View File
@@ -186,14 +186,19 @@ impl Builder {
return Some(Arc::new(g));
};
self.outline_text(&n.effects, &mut text);
let mut node = self.base(n, NodeKind::Text(Box::new(text)));
node.opacity *= alpha;
if !children.is_empty() {
let mut kids = vec![Arc::new(node)];
if children.is_empty() {
let mut node = self.base(n, NodeKind::Text(Box::new(text)));
node.opacity *= alpha;
node
} else {
// Text clips its children as a shape does: the type is the clipping path, its
// characters' fill behind the clipped art and their stroke over it.
let mut clip = Node::new(self.doc.alloc_id(), NodeKind::Text(Box::new(text)));
clip.opacity = alpha;
let mut kids = vec![Arc::new(clip)];
kids.extend(children);
node = Node::group(self.doc.alloc_id(), kids);
self.base(n, NodeKind::Group { children: kids, clip: true })
}
node
}
Kind::Table(t) => {
let mut kids = self.table(t, to_doc);
+39
View File
@@ -88,6 +88,45 @@ fn frame_type_keeps_dimensions_alignment_and_unit_conversion() {
assert_eq!(frame.bounds().unwrap().height(), 50.0);
}
#[test]
fn objects_nested_in_text_are_clipped_by_the_type() {
let ellipse = F::Obj(
tag(b"ShpN"),
vec![
(tag(b"Desc"), F::Str("Inside".into())),
(tag(b"Shpe"), F::Obj(tag(b"ShpE"), vec![])),
(tag(b"ShpB"), F::F64s(vec![10.0, 20.0, 110.0, 70.0])),
(tag(b"BFFl"), F::Shared(vec![F::Obj(tag(b"FDsc"), vec![(tag(b"FDeF"), solid(1.0))])])),
],
);
// A 4 × 4 RGBA pixel layer of one flat colour (tile state 2: every sample 255).
let mut bitmap = vec![(tag(b"Frmt"), F::Enum(0, 0)), (tag(b"BmpW"), F::I32(4)), (tag(b"BmpH"), F::I32(4))];
for c in 1..=4 {
bitmap.push((tag(&[b'T', b'W', b'i', b'0' + c]), F::I32(1)));
bitmap.push((tag(&[b'T', b'H', b'i', b'0' + c]), F::I32(1)));
bitmap.push((tag(&[b'S', b't', b'a', b'0' + c]), F::U8s(vec![2])));
}
let pixels = F::Obj(tag(b"Rstr"), vec![(tag(b"Bitm"), F::Obj(tag(b"DyBm"), bitmap)), (tag(b"Desc"), F::Str("Photo".into()))]);
let bytes = document_with_children(
vec![block(utf8("TEXT"), vec![run(4, Some(attrs("Clip", "", 400, false, 40.0)))], &[])],
false,
72.0,
IDENTITY,
vec![ellipse, pixels],
);
let loaded = fileio::load("nested.af", &bytes).unwrap();
let NodeKind::Layer { children, .. } = &loaded.doc.layers[0].kind else { panic!("{:?}", loaded.doc.layers[0].kind) };
let NodeKind::Group { children: clipped, clip: true } = &children[0].kind else { panic!("{:?}", children[0].kind) };
let [text, inside, photo] = clipped.as_slice() else { panic!("{clipped:?}") };
let NodeKind::Text(t) = &text.kind else { panic!("{:?}", text.kind) };
assert_eq!(t.runs[0].text, "TEXT");
assert_eq!(inside.name.as_deref(), Some("Inside"));
assert!(matches!(inside.kind, NodeKind::Path { clipping: false, .. }), "{:?}", inside.kind);
assert_eq!(photo.name.as_deref(), Some("Photo"));
let NodeKind::Image(image) = &photo.kind else { panic!("{:?}", photo.kind) };
assert_eq!((image.width, image.height), (4, 4));
}
#[test]
fn a_frame_too_short_for_its_first_line_is_made_taller_with_a_warning() {
// #1423: a title taller than its frame (60 pt type in a 50 pt frame) drew nothing.