How to pre-render maths to images?

I am planning to use Typst to emit EPUB-ready XHTML once the HTML support is ready. However, since the ecosystem around MathML and E-Reader support for maths rendering is so haphazard, the only reliable way to include equations in an EPUB is embedding a pre-rendered image. I would like to know if there is a way to do this in Typst. Writing Rust is also not an issue, since I am proficient in the language.

1 Like

You mean this?

#set page(width: auto, height: auto, margin: 0pt)
#show math.equation: it => pagebreak(weak: true) + it

$ x = y $

$ a = b $

typst c file.typ 'equation{0p}.png' # svg

Typst supports emitting equations as svg using html.frame already in Typst 0.13 (experimentally). You could potentially test it already now, but I suppose this (svg) is not well enough supported by EPUB?

Sort of, but I want typst to do it automatically within a larger document.

Yes, I’m quite sure that anything except rasterised images will break across all the different E-readers. It’s quite a shame actually, that everyone in the industry cannot seem to properly implement the standard and Amazon ignores it completely.

Which part? How would you define a “larger document”? You can do

#show math.equation: it => {
  if counter(page).final().first() >= 10 { pagebreak(weak: true) }
  it
}

If I understand you correctly, you’d basically want to

  • for PDF export, have regular equations
  • for HTML export, separately render the equations to images and include them as such in the document

This sounds similar to a use case I described for Prequery:

That example shows how to extract Python sources from the file, and use the stdout of the executed scripts as content to re-insert into the document. Here, you would instead extract Typst source code for the equations*, render them by executing the Typst CLI and ignore the stdout. You can then use the numbering scheme 'equation{p}.png' for the output files to infer what filenames you have to use in the document.

I don’t want to go through the whole process, so these are just some pointers. If that sounds what you want and can’t get it to work, lmk and I’ll have a closer look.

The Typst part would look roughly like this:

#import "@preview/bullseye:0.1.0": target
#import "@preview/prequery:0.2.0"

#let eq-counter = counter("eq")

#let eq(code) = prequery.prequery(
  (data: code.text),
  <eq>,
  () => {
    eq-counter.step()
    context if target() == "paged" {
      eval(code.text, mode: "markup")
    } else {
      image("equation" + str(eq-counter.get().first()) + ".png")
    }
  },
  fallback: {
    // in fallback mode, render directly
    eval(code.text, mode: "markup")
  },
)

#eq(`$ a = b $`)

Let #eq(`$x = 1$`). Then ...

The eq function is a prequery, implemented like described here—but ignoring the output file configuration, since we just use increasing numbers.

If you query this, you can see that you get the data:

$ typst query --feature html main.typ '<eq>' --field value
[{"data":"$ a = b $"},{"data":"$x = 1$"}]

You should be able to use a script (see here about using a script as the thing that Prequery executes) to render these equations in a similar fashion as @Andrew described, and when you export to HTML, these files should then be picked up by Typst.

*in principle it should be possible to specify the equations instead of their source code (e.g. #eq($ a = b $) instead of #eq(`$ a = b $`)), making a show rule feasible. But that means that you’ll have to do some extra work to convert the content back to source code for the rendering step. The big advantage would be that you’re not limited by eval, meaning you can use self-defined variables in your equations.

What won’t work either way is honoring show rules that are active for particular equations, since you’re pulling the equations out of their context.
Oh, and context inside equations also won’t work :stuck_out_tongue:

Is it possible to somehow intercept the document before the final render stage (where typst converts its internal structure to a PDF or HTML file)?

@SillyFreak, I’d prefer not to rip the equations out of their context and avoid forcing the use of #eq everywhere. Isn’t there a way to intercept the document right before the final render?

I don’t think so. This is basically what html.frame() does in a way; maybe it could be generalized? Or maybe you can get typst to output HTML that is fit for you extracting the resulting SVGs, and then you can rasterize them? But Typst definitely doesn’t have a hook to go in at the last second and extract the layouted content (that I know of).