Integration: #825 PDF table-cell and footnote text lays a gradient across its own lines, as on screen

(cherry picked from commit 14e98419ad)
This commit is contained in:
aiongg
2026-10-14 09:18:00 +02:00
committed by moppeeer
parent 1faaebef25
commit 57c997634f
2 changed files with 91 additions and 0 deletions
+71
View File
@@ -2612,4 +2612,75 @@ mod gradient_text_tests {
let d = p.doc().unwrap().doc.clone();
check(&rr.render_page(&d, &p.cache, 0, 1.0, false, &Default::default()).unwrap(), "pdf");
}
/// The columns the red → blue ink spans (black table edges left out).
fn ink_span(img: &designcraft_render::Rendered) -> (u32, u32) {
let cols: Vec<u32> = (0..img.width)
.filter(|&x| {
(0..img.height).any(|y| {
let p = img.pixel(x, y);
p[1] < 100 && p[0].max(p[2]) > 100
})
})
.collect();
(*cols.first().unwrap(), *cols.last().unwrap())
}
/// Mean red and blue of the red → blue ink in columns `x0..x1`.
fn coloured_ink(img: &designcraft_render::Rendered, x0: u32, x1: u32) -> (f64, f64) {
let (mut r, mut b, mut n) = (0.0, 0.0, 0.0);
for x in x0..x1 {
for y in 0..img.height {
let p = img.pixel(x, y);
if p[1] < 100 && p[0].max(p[2]) > 100 {
(r, b, n) = (r + p[0] as f64, b + p[2] as f64, n + 1.0);
}
}
}
assert!(n > 20.0, "no ink in {x0}..{x1}");
(r / n, b / n)
}
#[test]
fn a_gradient_on_table_cell_text_spans_the_cells_text_in_pdf_as_on_screen() {
let mut s = crate::Session::new();
s.execute("file.new", &json!({"width": 300, "height": 120, "facingPages": false, "margins": 0})).unwrap();
let g = s
.execute("swatch.create", &json!({"gradient": {"stops": [{"location": 0, "color": "#ff0000"}, {"location": 100, "color": "#0000ff"}]}}))
.unwrap()["name"]
.as_str()
.unwrap()
.to_string();
let r = s.execute("frame.create", &json!({"rect": [0, 0, 300, 120], "content": "text", "text": ""})).unwrap();
s.execute("text.select", &json!({"story": r["story"], "anchor": 0, "focus": 0})).unwrap();
s.execute("table.insert", &json!({"rows": 1, "cols": 3})).unwrap();
// A third of the frame's width: the cell's text, red → blue.
let mut d: designcraft_doc::Document = (*s.doc().unwrap().doc).clone();
let sid = d.stories.values().find(|st| !st.tables.is_empty()).unwrap().id;
let t = std::sync::Arc::make_mut(d.story_mut(sid).unwrap().tables.values_mut().next().unwrap());
let text = &mut t.cell_mut(0, 0).unwrap().text;
text.insert(0, "HHHH");
text.format_chars(0..4, |f| {
f.over.fill = Some(g.clone());
f.over.size = Some(24.0);
});
let mut rr = designcraft_render::Renderer::new();
let cache = designcraft_compose::Cache::new();
let screen = rr.render_page(&d, &cache, 0, 1.0, false, &Default::default()).unwrap();
let pdf = designcraft_pdf::export_pdf(&d, &cache, &Default::default()).unwrap();
let mut p = crate::Session::new();
p.execute("file.new", &json!({"width": 300, "height": 120, "facingPages": false})).unwrap();
p.execute("file.place", &json!({"base64": super::super::file::base64_encode(&pdf), "name": "cell.pdf", "x": 0, "y": 0, "width": 300}))
.unwrap();
let placed = rr.render_page(&p.doc().unwrap().doc.clone(), &p.cache, 0, 1.0, false, &Default::default()).unwrap();
let (x0, x1) = ink_span(&screen);
assert!(x1 < 120, "the text stays in the first column: {x0}..{x1}");
let q = (x1 - x0) / 4;
for (what, img) in [("screen", &screen), ("pdf", &placed)] {
let (lr, lb) = coloured_ink(img, x0, x0 + q);
let (rr, rb) = coloured_ink(img, x1 + 1 - q, x1 + 1);
assert!(lr > lb + 50.0, "{what}: the left ink is red ({lr:.0}, {lb:.0})");
assert!(rb > rr + 50.0, "{what}: the right ink is blue ({rr:.0}, {rb:.0})");
}
}
}
+20
View File
@@ -35,6 +35,22 @@ impl Exporter<'_> {
}
}
/// The box a gradient on a cell's or a note's text spans: its lines, each padded by its ascent as
/// the screen renderer pads them, so both lay the gradient out alike.
fn lines_area(ft: &FrameText) -> Option<Rect> {
ft.lines
.iter()
.filter_map(|l| {
let first = l.glyphs.first()?;
let x0 = l.glyphs.iter().map(|g| g.x).fold(first.x, f64::min);
let x1 = l.glyphs.iter().map(|g| g.x + g.adv.max(0.0)).fold(l.end_x, f64::max);
let pad = l.ascent.max(1.0);
let bottom = l.drop_cap.map_or(l.baseline + l.descent.max(0.0), |dc| dc.rect.y1.max(l.baseline + l.descent.max(0.0)));
Some(Rect::new(x0 - pad, l.baseline - l.ascent - pad, x1 + pad, bottom + pad))
})
.reduce(|a, b| a.union(b))
}
/// An opaque nonzero fill with `paint`.
fn paint_fill(paint: Paint) -> Fill {
Fill { paint, opacity: NormalizedF32::ONE, rule: FillRule::NonZero }
@@ -195,7 +211,9 @@ impl Exporter<'_> {
for n in &ft.notes {
if let Some(nft) = n.text.frames.first() {
s.push_transform(&tf(Affine::translate(n.origin.to_vec2())));
let area = std::mem::replace(&mut me.text_area, lines_area(nft));
me.frame_text(s, &n.text, nft, &n.source);
me.text_area = area;
s.pop();
}
}
@@ -223,7 +241,9 @@ impl Exporter<'_> {
}
} else if let Some(cft) = c.text.frames.first() {
s.push_transform(&tf(Affine::translate(c.origin.to_vec2())));
let area = std::mem::replace(&mut self.text_area, lines_area(cft));
self.frame_text(s, &c.text, cft, &c.source);
self.text_area = area;
s.pop();
}
}