mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-27 19:56:51 -06:00
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:
co-authored by
Claude Opus 5.5
parent
4be45a713a
commit
fe44b1cfdd
@@ -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.
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user