From 85290a2263d4081df1bef5094ee6de1d82d4e5e7 Mon Sep 17 00:00:00 2001 From: Rachit Singh Date: Thu, 8 Oct 2026 11:46:15 -0400 Subject: [PATCH] Render LaTeX labels with the shape's font color LaTeX labels were filled with the shape's stroke color, so they ignored style.font-color. This gives more flexibility to set font colors independent of the stroke (a feature I'm currently trying to use) Applies to both SVG output (d2svg) and PNG/PDF/PPTX/GIF exports (d2scenebuild). --- ci/release/changelogs/next.md | 1 + d2renderers/d2scenebuild/assets.go | 2 +- d2renderers/d2scenebuild/build.go | 2 +- d2renderers/d2scenebuild/latex_test.go | 11 ++++++----- d2renderers/d2svg/d2svg.go | 2 +- .../sequence-diagram-note-md/dagre/sketch.exp.svg | 2 +- .../txtar/sequence-diagram-note-md/elk/sketch.exp.svg | 2 +- .../sequence-diagram-note-md/tala/sketch.exp.svg | 2 +- 8 files changed, 13 insertions(+), 11 deletions(-) diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index ffe01dbc1e..878fec7783 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -78,6 +78,7 @@ - Encode and escape clip-path identifiers consistently in definitions and references. [#2906](https://github.com/d2lang/d2/pull/2906) - Escape color attributes across shapes, connections, labels, and themes. [#2909](https://github.com/d2lang/d2/pull/2909) + - Render LaTeX labels with the shape's font color instead of its stroke color. --- diff --git a/d2renderers/d2scenebuild/assets.go b/d2renderers/d2scenebuild/assets.go index 8ba2de1753..b5a9c8f9a4 100644 --- a/d2renderers/d2scenebuild/assets.go +++ b/d2renderers/d2scenebuild/assets.go @@ -43,7 +43,7 @@ func (b *builder) preflightAssets() error { if targetShape.Opacity == 0 || targetShape.Label == "" || targetShape.Language != "latex" { continue } - if _, err := b.resolveLatexAsset(fmt.Sprintf("shape %q", targetShape.ID), targetShape.Label, targetShape.Stroke); err != nil { + if _, err := b.resolveLatexAsset(fmt.Sprintf("shape %q", targetShape.ID), targetShape.Label, targetShape.GetFontColor()); err != nil { return err } } diff --git a/d2renderers/d2scenebuild/build.go b/d2renderers/d2scenebuild/build.go index 55279368c2..6c6e5ff05f 100644 --- a/d2renderers/d2scenebuild/build.go +++ b/d2renderers/d2scenebuild/build.go @@ -972,7 +972,7 @@ func (b *builder) buildShapeText(targetShape d2target.Shape) ([]*d2scene.Node, e topLeft := latexShapeLabelTopLeft(targetShape) node, err := b.buildLatexLabelNode( fmt.Sprintf("shape %q", targetShape.ID), targetShape.ID+":label:0", - targetShape.Label, targetShape.Stroke, topLeft, + targetShape.Label, targetShape.GetFontColor(), topLeft, ) if err != nil { return nil, err diff --git a/d2renderers/d2scenebuild/latex_test.go b/d2renderers/d2scenebuild/latex_test.go index a1cc9a79fd..8e2e0e4263 100644 --- a/d2renderers/d2scenebuild/latex_test.go +++ b/d2renderers/d2scenebuild/latex_test.go @@ -27,7 +27,7 @@ func TestBuildLatexShapeAndConnectionLabels(t *testing.T) { LabelFill: "#ff0000", Underline: true, Bold: true, } shape.LabelPosition = "INSIDE_MIDDLE_CENTER" - shape.Stroke = "#000000" + shape.Color = "#000000" connection := &diagram.Connections[0] connection.Text = d2target.Text{ Label: formula, Language: "latex", @@ -114,11 +114,11 @@ func TestBuildLatexResolvesCurrentColorWithoutOverridingExplicitColor(t *testing if err != nil { t.Fatal(err) } - makeShape := func(id string, x int, stroke string) d2target.Shape { + makeShape := func(id string, x int, fontColor string) d2target.Shape { return d2target.Shape{ ID: id, Type: d2target.ShapeText, Pos: d2target.Point{X: x}, Width: width, Height: height, - Opacity: 1, Fill: "none", Stroke: stroke, - Text: d2target.Text{Label: formula, Language: "latex", LabelWidth: width, LabelHeight: height}, + Opacity: 1, Fill: "none", + Text: d2target.Text{Label: formula, Language: "latex", LabelWidth: width, LabelHeight: height, Color: fontColor}, LabelPosition: "INSIDE_MIDDLE_CENTER", } } @@ -214,9 +214,10 @@ func TestBuildLatexLabelRendersPixels(t *testing.T) { diagram.Shapes = []d2target.Shape{{ ID: "formula", Type: d2target.ShapeText, Width: width, Height: height, Opacity: 1, - Fill: "none", Stroke: "#0055cc", + Fill: "none", Text: d2target.Text{ Label: formula, Language: "latex", LabelWidth: width, LabelHeight: height, + Color: "#0055cc", }, LabelPosition: "INSIDE_MIDDLE_CENTER", }} diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go index 2d5fbfc156..1813e75bca 100644 --- a/d2renderers/d2svg/d2svg.go +++ b/d2renderers/d2svg/d2svg.go @@ -2123,7 +2123,7 @@ func drawShape(writer, appendixWriter io.Writer, diagramHash string, targetShape ) gEl.SetTranslate(labelTL.X, labelTL.Y) - gEl.Color = targetShape.Stroke + gEl.Color = targetShape.GetFontColor() gEl.SetInnerSVG(svg.TrustedFragment(render)) fmt.Fprint(writer, gEl.Render()) } else if targetShape.Language == "markdown" { diff --git a/e2etests/testdata/txtar/sequence-diagram-note-md/dagre/sketch.exp.svg b/e2etests/testdata/txtar/sequence-diagram-note-md/dagre/sketch.exp.svg index f9716b4eab..12f94386d8 100644 --- a/e2etests/testdata/txtar/sequence-diagram-note-md/dagre/sketch.exp.svg +++ b/e2etests/testdata/txtar/sequence-diagram-note-md/dagre/sketch.exp.svg @@ -54,7 +54,7 @@ .d2-2097271006 .fill-B5{fill:#EDF0FD;} .d2-2097271006 .stroke-B1{stroke:#0D32B2;} .d2-2097271006 .stroke-B2{stroke:#0D32B2;} - .d2-2097271006 .color-B1{color:#0D32B2;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}]]>xy helloA man who fishes for marlin in ponds...dramatic pausewill put his money in Etruscan bonds.1 + 1 = 21 + 1 = 2 + .d2-2097271006 .color-N1{color:#0A0F25;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}]]>xy helloA man who fishes for marlin in ponds...dramatic pausewill put his money in Etruscan bonds.1 + 1 = 21 + 1 = 2 \ No newline at end of file diff --git a/e2etests/testdata/txtar/sequence-diagram-note-md/elk/sketch.exp.svg b/e2etests/testdata/txtar/sequence-diagram-note-md/elk/sketch.exp.svg index f9716b4eab..12f94386d8 100644 --- a/e2etests/testdata/txtar/sequence-diagram-note-md/elk/sketch.exp.svg +++ b/e2etests/testdata/txtar/sequence-diagram-note-md/elk/sketch.exp.svg @@ -54,7 +54,7 @@ .d2-2097271006 .fill-B5{fill:#EDF0FD;} .d2-2097271006 .stroke-B1{stroke:#0D32B2;} .d2-2097271006 .stroke-B2{stroke:#0D32B2;} - .d2-2097271006 .color-B1{color:#0D32B2;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}]]>xy helloA man who fishes for marlin in ponds...dramatic pausewill put his money in Etruscan bonds.1 + 1 = 21 + 1 = 2 + .d2-2097271006 .color-N1{color:#0A0F25;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}]]>xy helloA man who fishes for marlin in ponds...dramatic pausewill put his money in Etruscan bonds.1 + 1 = 21 + 1 = 2 \ No newline at end of file diff --git a/e2etests/testdata/txtar/sequence-diagram-note-md/tala/sketch.exp.svg b/e2etests/testdata/txtar/sequence-diagram-note-md/tala/sketch.exp.svg index f9716b4eab..12f94386d8 100644 --- a/e2etests/testdata/txtar/sequence-diagram-note-md/tala/sketch.exp.svg +++ b/e2etests/testdata/txtar/sequence-diagram-note-md/tala/sketch.exp.svg @@ -54,7 +54,7 @@ .d2-2097271006 .fill-B5{fill:#EDF0FD;} .d2-2097271006 .stroke-B1{stroke:#0D32B2;} .d2-2097271006 .stroke-B2{stroke:#0D32B2;} - .d2-2097271006 .color-B1{color:#0D32B2;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}]]>xy helloA man who fishes for marlin in ponds...dramatic pausewill put his money in Etruscan bonds.1 + 1 = 21 + 1 = 2 + .d2-2097271006 .color-N1{color:#0A0F25;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.light-code{display: block}.dark-code{display: none}]]>xy helloA man who fishes for marlin in ponds...dramatic pausewill put his money in Etruscan bonds.1 + 1 = 21 + 1 = 2 \ No newline at end of file