diff --git a/README.md b/README.md index 840da87..742bddf 100644 --- a/README.md +++ b/README.md @@ -86,8 +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, 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`, +`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. 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/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..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 @@ -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 @@ -10,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()); } @@ -51,11 +56,14 @@ 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()) - return imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) + imgToHtml("data:image/svg+xml;base64,${imgContent}", alt, attributes) } } @@ -63,7 +71,24 @@ 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) + } + } + + private static String chartToMd(CharmChart x, double width, double height, String alt, Map 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 + svgToMd(x.render(), alt, attributes) + } finally { + x.width = originalWidth + x.height = originalHeight } } @@ -104,14 +129,62 @@ 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 = 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 = 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 = 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, DEFAULT_CHART_WIDTH, DEFAULT_CHART_HEIGHT, alt, attributes)) + } + + void println(CharmChart x, String alt, Map 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 = 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 = [:]) { + /** + * Renders a GgChart at the supplied dimensions. The chart's original + * dimensions are restored before this method returns. + */ + 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(svgToMd(x.render(), alt, attributes)) + } + + void println(GgChart x, String alt, Map attributes = [:]) { + println(svgToMd(x.render(), 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..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 @@ -547,13 +547,75 @@ 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')) - 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(' 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 + void testGgChart() { + def text = ''' +```{groovy echo=false} +import static se.alipsa.matrix.gg.GgPlot.* +import se.alipsa.matrix.core.Matrix + +def data = Matrix.builder().data(x: [1, 2], y: [3, 4]).types(int, int).build() +def chart = ggplot(data, aes(x: 'x', y: 'y')) + geom_point() +chart.width = 1200 +chart.height = 900 +out.println(chart, 640, 480, 'GG chart', [class: 'chart']) +out.println(chart, 'GG default chart') +``` +''' + + String md = new Gmd().gmdToMd(text) + + assertTrue(md.contains('GG 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 List 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('