diff --git a/ROADMAP.md b/ROADMAP.md index a83979937..e4e2b626c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -401,6 +401,7 @@ Newest first. - **2026-10-24** · Text frames: a custom baseline grid's Relative To (Text Frame Options › Baseline Options, `object.textFrameOptions {baselineGrid: {relativeTo}}`, IDML `BaselineFrameGridRelativeOption`) measures the grid's start from the top of the page, the top margin, the top of the frame or its top inset. Text Frame Options had no custom baseline grid controls, imports dropped the setting and every frame grid started from the page top, so grid-aligned text in a frame measured from its own top sat on the wrong lines. No readiness percentage change. - **2026-10-24** · Paragraph Style Options › Indents and Spacing has an Align to Grid menu: None, All Lines or First Line Only. It shows the style's current setting. A First Line Only style could be set only in the CJK Grid Settings section, which shows only with CJK features on, or through commands. No readiness percentage change. - **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. diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index c71e1df41..3a40a9b84 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -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()); diff --git a/crates/pdf/src/text.rs b/crates/pdf/src/text.rs index 15dc16baf..4ec9d4eab 100644 --- a/crates/pdf/src/text.rs +++ b/crates/pdf/src/text.rs @@ -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 { diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 83e8def38..16d20d2be 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -2174,6 +2174,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()); diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index a9cdc5ce3..c29107c36 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -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 {