daylinmorgan / okab

pip-installable svg/png rendering backend for Vega-Altair
MIT License
4 stars 0 forks source link

Use QuickJS? #3

Closed k-groenbroek closed 2 years ago

k-groenbroek commented 2 years ago

Thanks for working on this! I haven't put in a lot of effort thinking about it, but maybe QuickJS can be used to make an even smaller rendering engine? It's a whole Javascript engine in <1mb, see https://github.com/bellard/quickjs and https://bellard.org/quickjs/. There are precompiled binaries for most common platforms.

I've done something like:

From Python perspective, the quickjs engine and javascript file would be static assets. The python code is then responsible for running quickjs with the javascript file. That may be further simplified with https://github.com/PetterS/quickjs.

What are your thoughts?

k-groenbroek commented 2 years ago

I played around a bit with this example:

import { View, parse } from "vega"
import { compile } from "vega-lite"

let specVegaLite = {
    $schema: "https://vega.github.io/schema/vega-lite/v5.json",
    description: "A simple bar chart with embedded data.",
    data: {
      values: [
        {a: "A", b: 28}, {a: "B", b: 55}, {a: "C", b: 43},
        {a: "D", b: 91}, {a: "E", b: 81}, {a: "F", b: 53},
        {a: "G", b: 19}, {a: "H", b: 87}, {a: "I", b: 52}
      ]
    },
    mark: "bar",
    encoding: {
      x: {field: "a", type: "nominal", axis: {labelAngle: 0}},
      y: {field: "b", type: "quantitative"}
    }
}
let specVega = compile(specVegaLite).spec
let view = new View(parse(specVega), {renderer: "none"})
view.toSVG().then(content => {
    let f = std.open("output.svg", "w")
    f.puts(content)
    f.close()
})

Exporting to svg works fine with QuickJS. But exporting to png needs a drawing canvas and getting node-canvas package to work with QuickJS is difficult.

QuickJs does give a very lightweight standalone vegalite compiler/parser (<2mb) that can be called from python, interesting to explore further.

k-groenbroek commented 2 years ago

Just found out that you used resvg instead of node-canvas. Nice, now we're getting somewhere!

I've attached a minimal standalone example: vegalite example win x64.zip. Run it via

qjs --std index.js
resvg output.svg output.png

So next steps could be:

The python wheel gets a dependency on quickjs, and bundles the js file and relevant resvg binary. The whole thing probably stays under 5mb uncompressed 😎

daylinmorgan commented 2 years ago

I'm partial to the option the requires fewer subprocess and IO calls. By using resvg-js it negates the need to ever save the svg and can instead directly convert the svg string. Currently the altair_saver method already saves the spec to a json file in order to get around windows issues with stream sizes.

Additionally, the pre-compiled node addons are simple to include with pkg. Albeit I kinda force it to include only the one that I want for a given platform.

I hadn't heard of quickjs and it doesn't seem to be a very active project not to mention quite niche. I'd be concerned about a lack of support if any bugs or edgecases come up.

For what it's worth okab is already doing significantly better than kaleido in terms of on disk space usage from the installed package.

For instance on linux:

221M    kaleido
54M     okab

I don't think something <60 mb is prohibitively large and could be worth the size cost for performance/stability.

k-groenbroek commented 2 years ago

Up to you of course! I agree that fewer subprocesses and less IO calls is good. Using https://github.com/PetterS/quickjs there is no subprocess for javascript engine though. I'll see if I can get resvg-js to work, to skip the svg file IO.

Also about QuickJS: It's written by Fabrice Bellard (also creator of ffmpeg) so that should earn it some credits ;)

daylinmorgan commented 2 years ago

In order to use resvg-js you'll need to also package the native node add-on for each platform (see here for how it's loaded).

All the usage examples for PetterS/quickjs seem simple. Can it handle loading all the vega/d3 bundled code? I've also combined the CLI of both vega/vega-lite and will expose these options which I don't think are currently well-supported in the selenium or node altair_saver methods.

Here are all the current options if you invoke okab from the command line after installing through pip: image

k-groenbroek commented 2 years ago

Thanks! I've played around a bit more and I think I have some interesting results.

  1. Use quickjs to set up a Javascript context in Python. Now we can evaluate js code.
  2. Run the production code for vega-lite and vega. Create a small function jsonToSvg inside the javascript context for converting vega-lite json string to svg string. No need for creating our own bundled js code.
  3. From Python, we give altair's json to jsonToSvg to get our svg content.
  4. Using Python libraries svglib and reportlab, the svg is converted to .png output (or .jpg, .pdf, etcetera).

We get a compact python package with dependencies on quickjs, svglib, reportlab. There are no subprocess calls or intermediate file IO or packaging binaries. 😎 I have attached a worked out example.zip.

It works and outputs this png: output

Thoughts?

k-groenbroek commented 2 years ago

Dependency graph looks like:

quickjs 1.19.2 Wrapping the quickjs C library.
reportlab 3.6.11 The Reportlab Toolkit
└── pillow >=9.0.0
svglib 1.4.1 A pure-Python library for reading and converting SVG
├── cssselect2 >=0.2.0
│   ├── tinycss2 *
│   │   └── webencodings >=0.4
│   └── webencodings * (circular dependency aborted here)
├── lxml *
├── reportlab *
│   └── pillow >=9.0.0
└── tinycss2 >=0.6.0
    └── webencodings >=0.4
daylinmorgan commented 2 years ago

Thanks for looking into this more and I played around with the example you attached.

In no particular order some observations:

Finally I stumbled on the biggest issue with using svglib and reportlab that I think excludes them all together.

As you can see with for example this chart that it fails to render gradients. Which seems like a pretty common use case.

Hexbin Svg: chart

Hexbin Png w/Pyquickjs & svglib: hexchart

PNG from Current Version of Okab: example-hexbin

k-groenbroek commented 2 years ago

Thanks! Too bad.. indeed you are right that svglib + reportlab don't support rendering gradients, so that won't work for us. I saw that resvg uses the tiny-skia rendering engine. I've tried to play around with skia-python but couldn't get any fonts to render.

Then I saw there is also a quickjs-rs. So we could write a small wrapper in Rust that parses vega to svg via quickjs, then renders svg via resvg. That can be compiled to a standalone binary, or made into a Python package via pyo3. I'm pretty sure it ticks all the boxes.

Haha, and then I found this https://github.com/vega/vl-convert, which does exactly that but using deno instead of quickjs. I'll open an issue over there to link the discussions. Do you think there is value in combining forces?

k-groenbroek commented 2 years ago

I'll close this in favor of the issues linked above. Thanks!