chalk-diagrams / chalk

A declarative drawing API in Python
MIT License
275 stars 13 forks source link

Unable to render latex in pdf and png backends #120

Open erdmann opened 1 year ago

erdmann commented 1 year ago

I just discovered this very cool library while following along with the raspy blog. Thanks for creating it!

I was unable to successfully generate latex-containing output in an SVG (no errors were shown, but all latex symbols were missing), so I slightly modified the latex.py example to instead output a PDF since I think latex-containing SVGs are generated by first generating a PDF and then calling pdf2svg if I understand correctly. As far as I can tell, I have installed all the prerequisites, but I can't really test that because the following code generates a TypeError:

from chalk import *
from colour import Color

grey = Color("#bbbbbb")
papaya = Color("#ff9700")

left_arrow = make_path([(0, 0), (1, 0)]).reflect_x().line_width(0.03).center_xy()
def box(t):
    return rectangle(1.5, 1).line_width(0.05).fill_color(papaya) + latex(t).scale(0.7)

def label(text):
    return latex(text).scale(0.5).pad(0.4)

def arrow(text, d=True):
    return label(text) // left_arrow

# Autograd 1
d = hcat([arrow(r"$f'_x(g(x))$"), box("$f$"), arrow(r"$f'_{g(x)}(g(x))$"), box("$g$"), arrow("1")], 0.2)
d.render_pdf("latex.pdf")
#d.render_png("latex.png") # also doesn't run; TypeError similar to above

The resulting error is as follows:

Traceback (most recent call last):
  File "latex.py", line 20, in <module>
    d.render_pdf("latex.pdf")
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 246, in render
    for x in to_tikz(diagram, pylatex, Style.root(max(height, width))):
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 207, in to_tikz
    return self.accept(ToTikZ(pylatex), style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 256, in accept
    return visitor.visit_apply_transform(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 86, in visit_apply_transform
    for x in diagram.diagram.accept(self, style=style):
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 256, in accept
    return visitor.visit_apply_transform(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 86, in visit_apply_transform
    for x in diagram.diagram.accept(self, style=style):
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 78, in visit_compose
    elems2 = diagram.diagram2.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 256, in accept
    return visitor.visit_apply_transform(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 86, in visit_apply_transform
    for x in diagram.diagram.accept(self, style=style):
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 245, in accept
    return visitor.visit_compose(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 77, in visit_compose
    elems1 = diagram.diagram1.accept(self, style=style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/core.py", line 221, in accept
    return visitor.visit_primitive(self, **kwargs)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/tikz.py", line 58, in visit_primitive
    inner = diagram.shape.accept(self.shape_renderer, style=style_new)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/shapes/latex.py", line 47, in accept
    return visitor.visit_latex(self, **kwargs)
TypeError: visit_latex() got an unexpected keyword argument 'style'

If instead I run the d.render_png("latex.png") line, the error is instead

Traceback (most recent call last):
  File "latex.py", line 21, in <module>
    d.render_png("latex.png", 100)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/cairo.py", line 230, in render
    render_cairo_prims(s, ctx, Style.root(max(width, height)))
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/backend/cairo.py", line 185, in render_cairo_prims
    prim.shape.accept(shape_renderer, ctx=ctx, style=prim.style)
  File "/opt/anaconda3/lib/python3.8/site-packages/chalk/shapes/latex.py", line 47, in accept
    return visitor.visit_latex(self, **kwargs)
TypeError: visit_latex() got an unexpected keyword argument 'ctx'
danoneata commented 1 year ago

Hello!

We currently don't have support for LaTeX in the PNG and PDF backends; but it should work using SVG. Here is a Colab example that renders the code above.

(Sorry for those uninformative error messages; there was a bug in the code, which should be fixed now.)

erdmann commented 1 year ago

Thanks for the quick response! The Euler identity equation renders fine for me, but the second example seems to be off, both in Colab and my local Jupyter installation (missing arrowheads maybe, wrong box calculations maybe):

image

Is this the intended appearance?

The problem that led me to looking for PDF/PNG output was that the result of doing d.render_svg('latex.svg') in the above arrow diagram leads to something that misses the latex when opened in Inkscape (also notice the odd bounding box, shown with the dashed line by selecting everything in Inkscape):

image

Similarly, saving the Euler identity with d.render_svg('euler.svg') also misses the latex bits when opened in Inkscape:

image
erdmann commented 1 year ago

As a follow-up in support of the observation that bounding boxes seem off, here is how the Euler identity looks in colab:

image

Whereas if I use text instead, it's properly centered:

image

I'm happy to open a separate issue for that if you'd prefer.

danoneata commented 1 year ago

Thanks for the report! I think I've fixed the issue: LaTeX should be properly centered now. As for the initial code, I think it's a bit dated, as it doesn't use the latest features (in particular, the arrows functionality: e.g., connect, connect_outside). I would probably rewrite it something like this:

from chalk import *
from colour import Color

papaya = Color("#ff9700")
black = Color("black")

SEP = 3
W = 3
H = 2
DY = -0.5

def box(t):
    return rectangle(W, H).line_width(0.05).fill_color(papaya) + latex(t)

def anchor():
    return circle(0.05).fill_color(black)

nodes = [
    anchor().named("inp"),
    box("$f$").named("f"),
    box("$g$").named("g"),
    anchor().named("out"),
]
d = hcat(nodes, sep=3)
d = d.connect_outside("inp", "f")
d = d.connect_outside("f", "g")
d = d.connect_outside("g", "out")

d = (
    d
    + latex(r"$f'_x(g(x))$").translate(SEP / 2, DY)
    + latex(r"$f'_{g(x)}(g(x))$").translate(3 * SEP / 2 + W, DY)
    + latex(r"$1$").translate(5 * SEP / 2 + 2 * W, DY)
)

d.render_svg("examples/output/latex.svg")

which renders this image o

Of course, this is still far from perfect. Some things to improve include:

As for the Inkscape rendering, I'm not sure what's happening. I usually use the browsers to view the SVGs, and they currently look fine for me. If it's a pressing issue I can investigate that as well.

erdmann commented 1 year ago

Great! Thanks very much.

As to Inkscape rendering, I lived my life up to this point without this great tool, so it's not pressing per se (I've been using TikZ for like 12 years, so I'll manage).

Inkscape is the de-facto open source SVG editing tool, though, and I supervise some PhD students who could make great use of chalk to make diagrams that then get incorporated into other figures (with images, e.g.) in Inkscape. Also, this is a backdoor way to generate PDF and PNG from latex-containing SVG files since Inkscape can do that (also from the CLI if memory serves). So I do think it's important enough to investigate even if it's not urgent for me.

Thanks again very much!

danoneata commented 1 year ago

Yes, the use-case you are describing makes a lot of sense! I'll keep it in mind and I'll take a look at the Inkscape issue when I get some time.

Also, thank you very much for the interest in our library—it's a much needed motivational boost, since, as you might imagine, it becomes a bit of challenge to actively maintain the side projects when other responsibilities take priority 🙂