Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use QuickJS? #3

Closed
k-groenbroek opened this issue Sep 15, 2022 · 10 comments
Closed

Use QuickJS? #3

k-groenbroek opened this issue Sep 15, 2022 · 10 comments

Comments

@k-groenbroek
Copy link

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:

  • Set up a javascript project following NPM structure. It can have NPM packages installed and there is some main.js with entrypoint javascript code.
  • use esbuild to compile the whole thing into 1 minified javascript file.
  • Give that file to quickjs engine.

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
Copy link
Author

k-groenbroek commented Sep 16, 2022

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
Copy link
Author

k-groenbroek commented Sep 16, 2022

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

k-groenbroek commented Oct 4, 2022

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
Copy link
Author

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
Copy link
Owner

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

In no particular order some observations:

  • Ultimately this package should be submitted to conda-forge which would mean pyquickjs
    would also need to be submitted which wouldn't be to much of an issue.
  • Pyquickjs installed from source distribution on my machine fails to compile with obscure errors about ld shared objects.
  • Currently okab can be distributed as a standalone binary with no python/node dependencies, however with the pyquickjs any CLI will depend on a python distribution and these dependencies. Admittedly, the number of users interested in a standalone binary to convert vega/vega-lite specs to static figures is likely very small.
  • This example fails, likely because this relies on a data url (which I hadn't considered before) but seemingly just works with my current method. Not sure if this inherent to quickjs or if we could modify the js to make it work. Not having dynamically loaded data isn't a dealbreaker for me but others users may disagree.

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
Copy link
Author

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
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants