Tables: gradient cell fills draw as gradients on screen and in PDF

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alichaudhry55
2026-10-24 18:36:54 +05:00
co-authored by Claude Opus 5.5
parent 4be45a713a
commit fe44b1cfdd
5 changed files with 74 additions and 9 deletions
+1
View File
@@ -359,6 +359,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-24** · Anchored text frames, such as marginal notes, compose where their host story places them: they align to the baseline grid at that position, and page-number markers and spine-relative alignment inside them read the host's page. They composed against a grid measured from their stored position and on no page, so a note set to Only Align First Line to Grid could sit a fraction of a line off the body text's grid line. No readiness percentage change.
- **2026-10-24** · Tables: a gradient swatch in a cell fill draws as a gradient on the canvas and in PDF, in one gradient space for the whole table, so it runs across cells without restarting. Cells showed the gradient's first stop as a solid colour. No readiness percentage change.
- **2026-10-24** · New documents start with Hyphenate Capitalized Words, Hyphenate Last Word and Hyphenate Across Column off in [Basic Paragraph], as in InDesign. Opened and imported files keep the values they carry. No readiness percentage change.
- **2026-10-24** · The baseline grid's Relative To setting applies: with Top Margin, the grid starts at each page's own top margin in text composition, on the canvas and for snapping. It started at the page top, so grid-aligned text sat on the wrong lines and a page could fit one line fewer. No readiness percentage change.
- **2026-10-24** · Custom-positioned anchored objects honour Relative to Spine (`anchored.options {spineRelative}`, IDML `SpineRelative`): the settings describe a left-hand page and mirror on a right-hand page, so marginal notes stay in the outside margin on both pages of a spread. On right-hand pages they took the left-hand placement and landed in the inside margin, across the spine or over the body text. No readiness percentage change.
+19
View File
@@ -200,6 +200,25 @@ fn standards() {
assert_eq!(Standard::parse("pdfa-2b"), Some(Standard::PdfA2b));
}
#[test]
fn table_gradient_fill_exports_a_shading() {
let mut d = Document::new(&NewDocument::default());
let mut gradient = designcraft_color::Gradient::default();
gradient.stops[0].color = Color::rgb(1.0, 0.0, 0.0);
gradient.stops[1].color = Color::rgb(0.0, 0.0, 1.0);
d.swatches.push(Swatch { name: "Table gradient".into(), value: SwatchValue::Gradient { gradient }, locked: false, named: true, hidden: false });
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 336.0, 300.0), d.default_layer(), "", ParaFormat::default()).unwrap();
let mut t = designcraft_doc::Table::new(1, 1, 2, 0, 0, 200.0);
t.rows[0].height = 40.0;
t.rows[0].mode = designcraft_doc::RowHeightMode::Exactly;
for cell in &mut t.cells {
cell.fill = "Table gradient".into();
}
d.story_mut(sid).unwrap().insert_table(0, t);
let pdf = uncompressed(&d, PdfOptions::default());
assert!(pdf.contains("/ShadingType 2") || pdf.contains("/ShadingType2"), "table fill must export a linear gradient");
}
#[test]
fn tables_export_cell_text() {
let mut d = Document::new(&NewDocument::default());
+10 -4
View File
@@ -326,10 +326,16 @@ impl Exporter<'_> {
fn tables(&mut self, s: &mut Surface, ft: &FrameText) {
for t in &ft.tables {
for c in &t.cells {
if let Some((sw, tint)) = &c.fill
&& let Some(col) = self.swatch_color(sw, *tint)
{
self.fill_rect(s, c.rect, col);
if let Some((sw, tint)) = &c.fill {
let fill = designcraft_doc::Fill { swatch: sw.clone(), tint: *tint, ..designcraft_doc::Fill::none() };
if let Some(paint) = self.fill_paint(&fill, t.rect)
&& let Some(path) = to_path(&c.rect.to_path(0.1))
{
s.set_stroke(None);
s.set_fill(Some(paint_fill(paint)));
s.draw_path(&path);
s.set_fill(None);
}
}
}
for c in &t.cells {
+37
View File
@@ -2161,6 +2161,43 @@ mod tests {
assert_eq!((a, b), (designcraft_geom::Point::new(top.x0 - 4.0, top.y0), edge.b), "only table corners extend");
}
#[test]
fn table_gradient_fill_is_continuous_across_cells() {
let mut d = Document::new(&NewDocument::default());
let mut gradient = designcraft_color::Gradient::default();
gradient.stops[0].color = designcraft_color::Color::rgb(1.0, 0.0, 0.0);
gradient.stops[1].color = designcraft_color::Color::rgb(0.0, 0.0, 1.0);
d.swatches.push(designcraft_color::Swatch {
name: "Table gradient".into(),
value: designcraft_color::SwatchValue::Gradient { gradient },
locked: false,
named: true,
hidden: false,
});
let (_, sid) =
d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 336.0, 300.0), d.default_layer(), "", ParaFormat::default()).unwrap();
let mut t = designcraft_doc::Table::new(1, 1, 2, 0, 0, 200.0);
t.rows[0].height = 40.0;
t.rows[0].mode = designcraft_doc::RowHeightMode::Exactly;
for cell in &mut t.cells {
cell.fill = "Table gradient".into();
}
d.story_mut(sid).unwrap().insert_table(0, t);
let cache = Cache::new();
let rect = cache.get(&d, sid, None).frames[0].tables[0].rect;
let mut renderer = Renderer::new();
renderer.threads = 0;
let img = renderer.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
let y = rect.center().y as u32;
let a = img.pixel((rect.x0 + 20.0) as u32, y);
let b = img.pixel((rect.x1 - 20.0) as u32, y);
assert!(i16::from(a[0]) - i16::from(a[2]) > 80, "red end: {a:?}");
assert!(i16::from(b[2]) - i16::from(b[0]) > 80, "blue end: {b:?}");
let left = img.pixel((rect.center().x - 2.0) as u32, y);
let right = img.pixel((rect.center().x + 2.0) as u32, y);
assert!((i16::from(left[0]) - i16::from(right[0])).abs() < 20, "no gradient restart between cells");
}
#[test]
fn renders_tables() {
let mut d = Document::new(&NewDocument::default());
+7 -5
View File
@@ -470,14 +470,16 @@ impl Renderer {
let doc = f.doc;
for t in &ft.tables {
for c in &t.cells {
if let Some((sw, tint)) = &c.fill
&& let Some(col) = doc.resolve_color(sw, *tint)
{
if let Some((sw, tint)) = &c.fill {
ctx.set_transform(f.view * xf);
ctx.set_paint(color_of(&col, 1.0));
let fill = designcraft_doc::Fill { swatch: sw.clone(), tint: *tint, ..designcraft_doc::Fill::none() };
// Overlap neighbours by half a device pixel so adjacent fills show no seams.
let h = 0.5 * f.px;
ctx.fill_rect(&c.rect.inflate(h, h));
// All cells share the table's gradient space; it must not restart at a cell edge.
if crate::set_fill_paint(ctx, doc, &fill, t.rect) {
ctx.fill_rect(&c.rect.inflate(h, h));
}
ctx.reset_paint_transform();
}
}
for c in &t.cells {