Skip to content

Commit

Permalink
Separate image-rendering style for SVG and CSS. This fixes fuzzy pict…
Browse files Browse the repository at this point in the history
…ures not only for SVG, but also for PNG, produced by Cairo/Batik.Transcoder. (#781)
  • Loading branch information
IKupriyanov-HORIS committed May 17, 2023
1 parent 945801b commit bfde539
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ object PlotSvgExportPortable {
private val PORTABLE_SVG_STR_MAPPER =
SvgToString(rgbEncoder = null) // data-frame --> rgb image is not supported (geom_raster)

/**
* @param plotSpec Raw specification of a plot or GGBunch.
* @param useCssPixelatedImageRendering true for CSS style "pixelated", false for SVG style "optimizeSpeed". Used for compatibility.
*/
@Suppress("MemberVisibilityCanBePrivate")
fun buildSvgImageFromRawSpecs(
plotSpec: MutableMap<String, Any>,
useCssPixelatedImageRendering: Boolean
): String {
return buildSvgImageFromRawSpecs(plotSpec, plotSize = null, SvgToString(rgbEncoder = null, useCssPixelatedImageRendering))
}

/**
* @param plotSpec Raw specification of a plot or GGBunch.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,33 @@ internal class PlotSvgExportPortableTest {
SvgUID.setUpForTest()
}

@Test
fun fuzzyImshow() {
val spec = """
|{
| 'kind': 'plot',
| 'layers': [
| {
| 'geom': 'image',
| 'show_legend': true,
| 'href': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVR4nGP4z8DwHwwZ/oOZvkDCF8jyBQCLFgnfUCS+/AAAAABJRU5ErkJggg==',
| 'xmin': -0.5,
| 'ymin': -0.5,
| 'xmax': 2.5,
| 'ymax': 1.5
| }
| ]
|}
""".trimMargin()

val svg = PlotSvgExportPortable.buildSvgImageFromRawSpecs(
plotSpec = parsePlotSpec(spec),
plotSize = DoubleVector(400.0, 300.0)
)

println(svg)
}

@Test
@Ignore
fun svgFromSinglePlot() {
Expand Down
11 changes: 6 additions & 5 deletions plot-config/src/jvmMain/kotlin/plot/PlotSvgExport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ import jetbrains.datalore.vis.svgMapper.awt.RGBEncoderAwt
import jetbrains.datalore.vis.svgToString.SvgToString

object PlotSvgExport {
private val JVM_SVG_STR_MAPPER =
SvgToString(RGBEncoderAwt()) // Supports data-frame --> rgb image transform (geom_raster)

/**
* @param plotSpec Raw specification of a plot or GGBunch.
* @param plotSize Desired plot size. Has no effect on GGBunch.
* @param useCssPixelatedImageRendering true for CSS style "pixelated", false for SVG style "optimizeSpeed". Used for compatibility.
*/
@Suppress("MemberVisibilityCanBePrivate")
fun buildSvgImageFromRawSpecs(
plotSpec: MutableMap<String, Any>,
plotSize: DoubleVector? = null
plotSize: DoubleVector? = null,
useCssPixelatedImageRendering: Boolean = true
): String {
return PlotSvgExportPortable.buildSvgImageFromRawSpecs(plotSpec, plotSize, JVM_SVG_STR_MAPPER)
// Supports data-frame --> rgb image transform (geom_raster)
val jvmSvgStrMapper = SvgToString(RGBEncoderAwt(), useCssPixelatedImageRendering)
return PlotSvgExportPortable.buildSvgImageFromRawSpecs(plotSpec, plotSize, jvmSvgStrMapper)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ object PlotImageExport {
}
}

val svg = buildSvgImageFromRawSpecs(plotSpec)
val svg = buildSvgImageFromRawSpecs(plotSpec, useCssPixelatedImageRendering = false) // Batik transcoder supports SVG style, not CSS

val plotSize = fetchPlotSizeFromSvg(svg)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ object PlotReprGenerator {
}
}

fun generateSvg(plotSpecDict: CPointer<PyObject>?): CPointer<PyObject>? {
fun generateSvg(plotSpecDict: CPointer<PyObject>?, useCssPixelatedImageRendering: Int): CPointer<PyObject>? {
try {
val plotSpecMap = pyDictToMap(plotSpecDict)

@Suppress("UNCHECKED_CAST")
val svg = PlotSvgExportPortable.buildSvgImageFromRawSpecs(plotSpecMap as MutableMap<String, Any>)
val svg = PlotSvgExportPortable.buildSvgImageFromRawSpecs(plotSpecMap as MutableMap<String, Any>, useCssPixelatedImageRendering == 1)
val result = Py_BuildValue("s", svg);
return result
} catch (e: Throwable) {
Expand Down
11 changes: 8 additions & 3 deletions python-package/kotlin-bridge/lets_plot_kotlin_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ static PyObject* generate_html(PyObject* self, PyObject* rawPlotSpecDict) {
return html;
}

static PyObject* export_svg(PyObject* self, PyObject* rawPlotSpecDict) {
static PyObject* export_svg(PyObject* self, PyObject* args) {
T_(PlotReprGenerator) reprGen = __ kotlin.root.jetbrains.datalore.plot.pythonExtension.interop.PlotReprGenerator._instance();
PyObject* svg = __ kotlin.root.jetbrains.datalore.plot.pythonExtension.interop.PlotReprGenerator.generateSvg(reprGen, rawPlotSpecDict);

PyObject *rawPlotSpecDict;
int useCssPixelatedImageRendering; // 0 - false, 1 - true
PyArg_ParseTuple(args, "Op", &rawPlotSpecDict, &useCssPixelatedImageRendering);

PyObject* svg = __ kotlin.root.jetbrains.datalore.plot.pythonExtension.interop.PlotReprGenerator.generateSvg(reprGen, rawPlotSpecDict, useCssPixelatedImageRendering);
return svg;
}

Expand All @@ -45,7 +50,7 @@ static PyObject* export_html(PyObject* self, PyObject* args) {

static PyMethodDef module_methods[] = {
{ "generate_html", (PyCFunction)generate_html, METH_O, "Generates HTML and JS sufficient for buidling of interactive plot." },
{ "export_svg", (PyCFunction)export_svg, METH_O, "Generates SVG representing plot." },
{ "export_svg", (PyCFunction)export_svg, METH_VARARGS, "Generates SVG representing plot." },
{ "export_html", (PyCFunction)export_html, METH_VARARGS, "Generates HTML page showing plot." },
{ NULL }
};
Expand Down
4 changes: 2 additions & 2 deletions python-package/lets_plot/_kbridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ def _generate_dynamic_display_html(plot_spec: Dict) -> str:
return lets_plot_kotlin_bridge.generate_html(plot_spec)


def _generate_svg(plot_spec: Dict) -> str:
def _generate_svg(plot_spec: Dict, use_css_pixelated_image_rendering: bool = True) -> str:
plot_spec = _standardize_plot_spec(plot_spec)
return lets_plot_kotlin_bridge.export_svg(plot_spec)
return lets_plot_kotlin_bridge.export_svg(plot_spec, use_css_pixelated_image_rendering)


def _generate_static_html_page(plot_spec: Dict, iframe: bool) -> str:
Expand Down
3 changes: 2 additions & 1 deletion python-package/lets_plot/export/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ def export_png(plot: Union[PlotSpec, SupPlotsSpec, GGBunch], filename: str, scal
return None

from .. import _kbridge
svg = _kbridge._generate_svg(plot.as_dict())
# Use SVG image-rendering style as Cairo doesn't support CSS image-rendering style,
svg = _kbridge._generate_svg(plot.as_dict(), use_css_pixelated_image_rendering=False)

cairosvg.svg2png(bytestring=svg, write_to=filename, scale=scale)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@

package jetbrains.datalore.vis.svgToString

import jetbrains.datalore.vis.svg.SvgElement
import jetbrains.datalore.vis.svg.SvgImageElementEx
import jetbrains.datalore.vis.svg.*
import jetbrains.datalore.vis.svg.SvgImageElementEx.RGBEncoder
import jetbrains.datalore.vis.svg.SvgSvgElement
import jetbrains.datalore.vis.svg.SvgTextNode
import jetbrains.datalore.vis.svg.XmlNamespace.SVG_NAMESPACE_URI
import jetbrains.datalore.vis.svg.XmlNamespace.XLINK_NAMESPACE_URI
import jetbrains.datalore.vis.svg.XmlNamespace.XLINK_PREFIX

class SvgToString(private val rgbEncoder: RGBEncoder?) {

class SvgToString(
private val rgbEncoder: RGBEncoder?,
private val useCssPixelatedImageRendering: Boolean = true // true for browser, false for Batik.Transcoder or Cairo
) {
fun render(svg: SvgSvgElement): String {
val buffer = StringBuilder()
renderElement(svg, buffer, 0)
Expand Down Expand Up @@ -69,6 +68,15 @@ class SvgToString(private val rgbEncoder: RGBEncoder?) {
continue
}
}

if (childNode is SvgImageElement) {
val style = when (useCssPixelatedImageRendering) {
true -> "image-rendering: optimizeSpeed; image-rendering: pixelated"
false -> "image-rendering: optimizeSpeed"
}
childNode.setAttribute(SvgConstants.SVG_STYLE_ATTRIBUTE, style)
}

renderElement(
childNode as SvgElement,
buffer,
Expand Down

0 comments on commit bfde539

Please sign in to comment.