From 8a58b4a93358fbeccb992ffb38868f71ec8d71d5 Mon Sep 17 00:00:00 2001 From: per Date: Wed, 23 Sep 2026 16:34:35 +0200 Subject: [PATCH 1/5] Add Charm and GG chart printing --- gmd-core/pom.xml | 8 ++++ .../groovy/se/alipsa/gmd/core/Printer.groovy | 34 ++++++++++++++ .../test/alipsa/groovy/gmd/GmdTest.groovy | 46 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/gmd-core/pom.xml b/gmd-core/pom.xml index 111e30a..41fb97b 100644 --- a/gmd-core/pom.xml +++ b/gmd-core/pom.xml @@ -177,6 +177,14 @@ se.alipsa.matrix matrix-pict + + se.alipsa.matrix + matrix-charts + + + se.alipsa.matrix + matrix-ggplot + se.alipsa.matrix matrix-xchart diff --git a/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy b/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy index 6dda131..4311a71 100644 --- a/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy +++ b/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy @@ -3,6 +3,8 @@ package se.alipsa.gmd.core import org.jsoup.nodes.Entities import se.alipsa.groovy.svg.Svg import se.alipsa.matrix.chartexport.ChartToSvg +import se.alipsa.matrix.charm.Chart as CharmChart +import se.alipsa.matrix.gg.GgChart import se.alipsa.matrix.pict.CharmBridge; import se.alipsa.matrix.pict.Chart; import se.alipsa.matrix.core.Matrix @@ -67,6 +69,22 @@ class Printer extends PrintWriter { } } + private static String chartToMd(CharmChart x, double width, double height, String alt, Map attributes) { + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + ChartToSvg.export(x.render(width as int, height as int), os) + String imgContent = Base64.getEncoder().encodeToString(os.toByteArray()) + return imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) + } + } + + private static String chartToMd(GgChart x, String alt, Map attributes) { + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + ChartToSvg.export(x.render(), os) + String imgContent = Base64.getEncoder().encodeToString(os.toByteArray()) + return imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) + } + } + /** * CommonMark's image syntax has no attribute extension, so Pandoc-style * {key=value} suffixes are left as literal text in the output. Emit raw @@ -112,6 +130,22 @@ class Printer extends PrintWriter { println(chartToMd(x, width, height, alt, attributes)) } + void print(CharmChart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + printChart(chartToMd(x, width, height, alt, attributes)) + } + + void println(CharmChart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + println(chartToMd(x, width, height, alt, attributes)) + } + + void print(GgChart x, String alt = '', Map attributes = [:]) { + printChart(chartToMd(x, alt, attributes)) + } + + void println(GgChart x, String alt = '', Map attributes = [:]) { + println(chartToMd(x, alt, attributes)) + } + void print(MatrixXChart x, String alt = '', Map attributes = [:]) { printChart(chartToMd(x, alt, attributes)) } diff --git a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy index a646dc8..b312b05 100644 --- a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy +++ b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy @@ -556,6 +556,52 @@ out.println(chart) assertTrue(new String(Base64.decoder.decode(image), StandardCharsets.UTF_8).contains(' Date: Wed, 23 Sep 2026 16:39:18 +0200 Subject: [PATCH 2/5] remove unused spec --- req/v3.2.0-improvements.md | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 req/v3.2.0-improvements.md diff --git a/req/v3.2.0-improvements.md b/req/v3.2.0-improvements.md deleted file mode 100644 index e30ed2e..0000000 --- a/req/v3.2.0-improvements.md +++ /dev/null @@ -1,35 +0,0 @@ -# Add a se.alipsa.matrix.charm.Chart and GgChart print overload in gmd-core -# Add a fluent api -```groovy -Gmd gmd = new Gmd() -def outputDir = new File("build") -def htmlFile = new File(outputDir, "whiskeyAnalysis.html") -def gmdFile = new File("src/main/gmd/WhiskeyAnalysis.gmd") -gmd.gmdToHtml(gmdFile.text, htmlFile, [:]) -``` - -Consider adding a fluent api e.g: - -```groovy -Gmd.builder() - .input("src/main/gmd/WhiskeyAnalysis.gmd") - .output("build/whiskeyAnalysis.html") - .gmdToHtml() - -// also support File (and Path) -Gmd.builder() - .input(new File("src/main/gmd/WhiskeyAnalysis.gmd")) - .output(new File("build/whiskeyAnalysis.html")) - .gmdToHtml() - -Gmd.builder() - .input(Paths.get("src/main/gmd/WhiskeyAnalysis.gmd")) - .output(Paths.get("build/whiskeyAnalysis.html")) - .gmdToHtml() - -// as well as PDF's (add overloads for File and Path similar to above) -Gmd.builder() - .input("src/main/gmd/WhiskeyAnalysis.gmd") - .output("build/whiskeyAnalysis.pdf") - .gmdToPdf() -``` From 16e3b9ba91ca03688fe30bf6dda424edd752ec8c Mon Sep 17 00:00:00 2001 From: per Date: Wed, 23 Sep 2026 16:53:39 +0200 Subject: [PATCH 3/5] Improve Charm and GG chart printing --- README.md | 9 ++- .../groovy/se/alipsa/gmd/core/Printer.groovy | 62 ++++++++++++++----- .../test/alipsa/groovy/gmd/GmdTest.groovy | 26 +++++--- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 840da87..af26070 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,13 @@ def html = gmd.gmdToHtml(text, [name: "Per"]) gmd.htmlToPdf(html, new File("pdfFile.pdf")) ``` -GMD supports the [Matrix](https://github.com/Alipsa/matrix) library directly, i.e. Matrix, Chart and MatrixXChart -types can be used with the `out` PrintWriter object without needing to convert them into markdown first. +GMD supports the [Matrix](https://github.com/Alipsa/matrix) library directly. `Matrix`, legacy +`se.alipsa.matrix.pict.Chart`, `se.alipsa.matrix.charm.Chart`, `se.alipsa.matrix.gg.GgChart`, and +`MatrixXChart` types can be used with the `out` PrintWriter object without needing to convert them +into markdown first. Charm and GG charts accept `width`, `height`, optional alt text, and optional +HTML attributes (for example, `out.println(chart, 640, 480, 'Sales chart', [class: 'chart'])`). +GG chart dimensions are restored after rendering. MatrixXChart accepts optional alt text and HTML +attributes; legacy pict charts retain their existing overloads. Here is an example: ```` # Employees diff --git a/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy b/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy index 4311a71..63fe85c 100644 --- a/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy +++ b/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy @@ -53,7 +53,10 @@ class Printer extends PrintWriter { private static String chartToMd(Chart x, double width, double height, String alt, Map attributes) { - Svg svg = CharmBridge.renderSvg(x, width as int, height as int) + svgToMd(CharmBridge.renderSvg(x, width as int, height as int), alt, attributes) + } + + private static String svgToMd(Svg svg, String alt, Map attributes) { try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { ChartToSvg.export(svg, os) String imgContent = Base64.getEncoder().encodeToString(os.toByteArray()) @@ -70,18 +73,19 @@ class Printer extends PrintWriter { } private static String chartToMd(CharmChart x, double width, double height, String alt, Map attributes) { - try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { - ChartToSvg.export(x.render(width as int, height as int), os) - String imgContent = Base64.getEncoder().encodeToString(os.toByteArray()) - return imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) - } - } - - private static String chartToMd(GgChart x, String alt, Map attributes) { - try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { - ChartToSvg.export(x.render(), os) - String imgContent = Base64.getEncoder().encodeToString(os.toByteArray()) - return imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) + svgToMd(x.render(width as int, height as int), alt, attributes) + } + + private static String chartToMd(GgChart x, double width, double height, String alt, Map attributes) { + int originalWidth = x.width + int originalHeight = x.height + try { + x.width = width as int + x.height = height as int + return svgToMd(x.render(), alt, attributes) + } finally { + x.width = originalWidth + x.height = originalHeight } } @@ -138,12 +142,36 @@ class Printer extends PrintWriter { println(chartToMd(x, width, height, alt, attributes)) } - void print(GgChart x, String alt = '', Map attributes = [:]) { - printChart(chartToMd(x, alt, attributes)) + void print(CharmChart x, String alt, Map attributes = [:]) { + printChart(chartToMd(x, 800, 600, alt, attributes)) } - void println(GgChart x, String alt = '', Map attributes = [:]) { - println(chartToMd(x, alt, attributes)) + void println(CharmChart x, String alt, Map attributes = [:]) { + println(chartToMd(x, 800, 600, alt, attributes)) + } + + /** + * Renders a GgChart at the supplied dimensions. The chart's original + * dimensions are restored before this method returns. + */ + void print(GgChart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + printChart(chartToMd(x, width, height, alt, attributes)) + } + + /** + * Renders a GgChart at the supplied dimensions. The chart's original + * dimensions are restored before this method returns. + */ + void println(GgChart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + println(chartToMd(x, width, height, alt, attributes)) + } + + void print(GgChart x, String alt, Map attributes = [:]) { + printChart(chartToMd(x, 800, 600, alt, attributes)) + } + + void println(GgChart x, String alt, Map attributes = [:]) { + println(chartToMd(x, 800, 600, alt, attributes)) } void print(MatrixXChart x, String alt = '', Map attributes = [:]) { diff --git a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy index b312b05..0ee646b 100644 --- a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy +++ b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy @@ -552,8 +552,7 @@ out.println(chart) Gmd gmd = new Gmd() String md = gmd.gmdToMd(text) assertTrue(md.contains('# Employees')) - String image = md.split("data:image/svg\\+xml;base64,", 2)[1].split('"', 2)[0] - assertTrue(new String(Base64.decoder.decode(image), StandardCharsets.UTF_8).contains(' Date: Wed, 23 Sep 2026 17:20:34 +0200 Subject: [PATCH 4/5] Preserve configured GG chart dimensions --- README.md | 11 +++--- .../groovy/se/alipsa/gmd/core/Printer.groovy | 37 ++++++++++++------- .../test/alipsa/groovy/gmd/GmdTest.groovy | 27 +++++++++----- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index af26070..742bddf 100644 --- a/README.md +++ b/README.md @@ -86,13 +86,14 @@ def html = gmd.gmdToHtml(text, [name: "Per"]) gmd.htmlToPdf(html, new File("pdfFile.pdf")) ``` -GMD supports the [Matrix](https://github.com/Alipsa/matrix) library directly. `Matrix`, legacy +GMD supports the [Matrix](https://github.com/Alipsa/matrix) library directly. `Matrix`, `se.alipsa.matrix.pict.Chart`, `se.alipsa.matrix.charm.Chart`, `se.alipsa.matrix.gg.GgChart`, and `MatrixXChart` types can be used with the `out` PrintWriter object without needing to convert them -into markdown first. Charm and GG charts accept `width`, `height`, optional alt text, and optional -HTML attributes (for example, `out.println(chart, 640, 480, 'Sales chart', [class: 'chart'])`). -GG chart dimensions are restored after rendering. MatrixXChart accepts optional alt text and HTML -attributes; legacy pict charts retain their existing overloads. +into markdown first. Pict, Charm, and GG charts accept `width`, `height`, optional alt text, and +optional HTML attributes (for example, `out.println(chart, 640, 480, 'Sales chart', [class: 'chart'])`). +They also accept alt text and attributes without explicit dimensions. Alt-only GG printing honors +the dimensions configured on the chart; explicit GG dimensions are restored after rendering. +MatrixXChart accepts optional alt text and HTML attributes. Here is an example: ```` # Employees diff --git a/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy b/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy index 63fe85c..def4b64 100644 --- a/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy +++ b/gmd-core/src/main/groovy/se/alipsa/gmd/core/Printer.groovy @@ -12,6 +12,9 @@ import se.alipsa.matrix.xchart.abstractions.MatrixXChart; class Printer extends PrintWriter { + private static final int DEFAULT_CHART_WIDTH = 800 + private static final int DEFAULT_CHART_HEIGHT = 600 + Printer() { super(new StringWriter()); } @@ -60,7 +63,7 @@ class Printer extends PrintWriter { try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { ChartToSvg.export(svg, os) String imgContent = Base64.getEncoder().encodeToString(os.toByteArray()) - return imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) + imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) } } @@ -68,7 +71,7 @@ class Printer extends PrintWriter { try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { x.exportSvg(os) String imgContent = Base64.getEncoder().encodeToString(os.toByteArray()) - return imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) + imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) } } @@ -82,7 +85,7 @@ class Printer extends PrintWriter { try { x.width = width as int x.height = height as int - return svgToMd(x.render(), alt, attributes) + svgToMd(x.render(), alt, attributes) } finally { x.width = originalWidth x.height = originalHeight @@ -126,35 +129,43 @@ class Printer extends PrintWriter { print(isAtLineStart() ? terminateHtmlBlock(html) : html) } - void print(Chart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + void print(Chart x, double width = DEFAULT_CHART_WIDTH, double height = DEFAULT_CHART_HEIGHT, String alt = '', Map attributes = [:]) { printChart(chartToMd(x, width, height, alt, attributes)) } - void println(Chart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + void println(Chart x, double width = DEFAULT_CHART_WIDTH, double height = DEFAULT_CHART_HEIGHT, String alt = '', Map attributes = [:]) { println(chartToMd(x, width, height, alt, attributes)) } - void print(CharmChart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + void print(CharmChart x, double width = DEFAULT_CHART_WIDTH, double height = DEFAULT_CHART_HEIGHT, String alt = '', Map attributes = [:]) { printChart(chartToMd(x, width, height, alt, attributes)) } - void println(CharmChart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + void println(CharmChart x, double width = DEFAULT_CHART_WIDTH, double height = DEFAULT_CHART_HEIGHT, String alt = '', Map attributes = [:]) { println(chartToMd(x, width, height, alt, attributes)) } void print(CharmChart x, String alt, Map attributes = [:]) { - printChart(chartToMd(x, 800, 600, alt, attributes)) + printChart(chartToMd(x, DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT, alt, attributes)) } void println(CharmChart x, String alt, Map attributes = [:]) { - println(chartToMd(x, 800, 600, alt, attributes)) + println(chartToMd(x, DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT, alt, attributes)) + } + + void print(Chart x, String alt, Map attributes = [:]) { + printChart(chartToMd(x, DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT, alt, attributes)) + } + + void println(Chart x, String alt, Map attributes = [:]) { + println(chartToMd(x, DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT, alt, attributes)) } /** * Renders a GgChart at the supplied dimensions. The chart's original * dimensions are restored before this method returns. */ - void print(GgChart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + void print(GgChart x, double width = DEFAULT_CHART_WIDTH, double height = DEFAULT_CHART_HEIGHT, String alt = '', Map attributes = [:]) { printChart(chartToMd(x, width, height, alt, attributes)) } @@ -162,16 +173,16 @@ class Printer extends PrintWriter { * Renders a GgChart at the supplied dimensions. The chart's original * dimensions are restored before this method returns. */ - void println(GgChart x, double width = 800, double height = 600, String alt = '', Map attributes = [:]) { + void println(GgChart x, double width = DEFAULT_CHART_WIDTH, double height = DEFAULT_CHART_HEIGHT, String alt = '', Map attributes = [:]) { println(chartToMd(x, width, height, alt, attributes)) } void print(GgChart x, String alt, Map attributes = [:]) { - printChart(chartToMd(x, 800, 600, alt, attributes)) + printChart(svgToMd(x.render(), alt, attributes)) } void println(GgChart x, String alt, Map attributes = [:]) { - println(chartToMd(x, 800, 600, alt, attributes)) + println(svgToMd(x.render(), alt, attributes)) } void print(MatrixXChart x, String alt = '', Map attributes = [:]) { diff --git a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy index 0ee646b..86b8dc6 100644 --- a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy +++ b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy @@ -547,11 +547,13 @@ def empData = Matrix.builder().data( .build() BarChart chart = BarChart.createVertical("Salaries", empData, "emp_name", ChartType.BASIC, "salary") out.println(chart) +out.println(chart, 'Pict chart') ``` ''' Gmd gmd = new Gmd() String md = gmd.gmdToMd(text) assertTrue(md.contains('# Employees')) + assertTrue(md.contains('Pict chart svgs = decodeSvgImages(md, 'GG chart should be SVG-backed') + assertTrue(svgs[0].contains('width="640"') && svgs[0].contains('height="480"'), svgs[0].take(200)) + assertTrue(svgs[1].contains('width="1200"') && svgs[1].contains('height="900"'), svgs[1].take(200)) } private static void assertSvgImage(String markdown, String message) { @@ -607,11 +610,17 @@ assert chart.height == 600 } private static String decodeSvgImage(String markdown, String message) { - assertTrue(markdown.contains('data:image/svg+xml;base64,'), "$message — no image in: $markdown") - String image = markdown.split("data:image/svg\\+xml;base64,", 2)[1].split('"', 2)[0] - String svg = new String(Base64.decoder.decode(image), StandardCharsets.UTF_8) - assertTrue(svg.contains(' decodeSvgImages(String markdown, String message) { + String marker = 'data:image/svg+xml;base64,' + assertTrue(markdown.contains(marker), "$message — no image in: $markdown") + List svgs = markdown.split(java.util.regex.Pattern.quote(marker)).drop(1).collect { image -> + new String(Base64.decoder.decode(image.split('"', 2)[0]), StandardCharsets.UTF_8) + } + svgs.each { svg -> assertTrue(svg.contains(' Date: Wed, 23 Sep 2026 22:28:43 +0200 Subject: [PATCH 5/5] Verify chart default render dimensions --- .../test/alipsa/groovy/gmd/GmdTest.groovy | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy index 86b8dc6..78172b9 100644 --- a/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy +++ b/gmd-core/src/test/groovy/test/alipsa/groovy/gmd/GmdTest.groovy @@ -554,7 +554,9 @@ out.println(chart, 'Pict chart') String md = gmd.gmdToMd(text) assertTrue(md.contains('# Employees')) assertTrue(md.contains('Pict chart svgs = decodeSvgImages(md, 'Charm chart should be SVG-backed') + assertTrue(svgs[0].contains('width="640"') && svgs[0].contains('height="480"'), svgs[0].take(200)) + assertTrue(svgs[1].contains('width="800"') && svgs[1].contains('height="600"'), svgs[1].take(200)) } @Test @@ -605,14 +608,6 @@ out.println(chart, 'GG default chart') assertTrue(svgs[1].contains('width="1200"') && svgs[1].contains('height="900"'), svgs[1].take(200)) } - private static void assertSvgImage(String markdown, String message) { - decodeSvgImage(markdown, message) - } - - private static String decodeSvgImage(String markdown, String message) { - decodeSvgImages(markdown, message).first() - } - private static List decodeSvgImages(String markdown, String message) { String marker = 'data:image/svg+xml;base64,' assertTrue(markdown.contains(marker), "$message — no image in: $markdown")