14

Trying to find some ways to automate generation of svg output for TeX math formulas, I thought about two possibilities with luatex.

  1. Use lua inside a callback to parse the node tree and output svg (someone had the very idea some time ago).

  2. Use luatex itself to generate svg instead of pdf. As far as I can see, cairo library is included in luatex source code and cairo can output svg.

My questions are:

  1. What is the current status of svg output support with luatex (the program itself or "third parties" projects).

  2. Could I use mplib's metapost svg output for that matter? EDIT It seems that mplib does not support svg anymore.

  • 2
    it would be interesting generally to get xml out of the luatex back end (context is ahead of latex in this currently I suspect) but for this particular use is there a problem with using latex +dvisvgm or are you just wanting to experiment with other alternatives? – David Carlisle Apr 3 '17 at 15:36
  • @DavidCarlisle The answer given on the blender site is fine. I was just wondering if the conversion could be done at luatex level, not requiring other dependencies such as pdftocairo or dvisvgm. In addition luatex could label svg path according to its mathematical meaning. – cjorssen Apr 3 '17 at 15:41
  • 1
    Yes I suspect it could: I had a quick look for what context can do in this area, but searching for "context" is even more useless than searching for "latex" why can't we pick sensible program names:( – David Carlisle Apr 3 '17 at 15:44
  • @DavidCarlisle: I normally restrict context searches to the context garden and the mailing list: mail-archive.com/… (it doesn't look as if there a simple and obvious svg export). – Ulrike Fischer Apr 3 '17 at 16:21
  • 1
    I guess the main problem with this would be how to include fonts - I don't think you can get actual font data from Lua callbacks. – michal.h21 Apr 3 '17 at 16:26
11
+600

Use dvisvgm

If I understand the question correctly from the thread you linked about Blender, you don't care about fonts at all an are simply interested in getting SVG paths describing the math equations. In this case you can use dvisvgm.

Suppose you have the file test.tex with the contents

\documentclass{standalone}
\begin{document}
$x^2$
\end{document}

Then you can convert this to SVG using

latex test.tex
dvisvgm --no-fonts test

The --no-fonts option tells dvisvgm to convert text to SVG paths. You will not be able to copy and paste from the resulting SVG, but you only want to render the formula in a animation anyway.

This is the resulting SVG.

<?xml version='1.0' encoding='UTF-8'?>
<!-- This file was generated by dvisvgm 2.3.5 -->
<svg height='8.109622pt' version='1.1' viewBox='-72.000004 -72.000007 9.665173 8.109622' width='9.665173pt' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
<defs>
<path d='M3.521793 -1.26924H3.284682C3.263761 -1.115816 3.194022 -0.704359 3.103362 -0.63462C3.047572 -0.592777 2.510585 -0.592777 2.412951 -0.592777H1.129763C1.862017 -1.241345 2.106102 -1.436613 2.524533 -1.764384C3.040598 -2.175841 3.521793 -2.608219 3.521793 -3.270735C3.521793 -4.11457 2.782565 -4.630635 1.889913 -4.630635C1.025156 -4.630635 0.439352 -4.02391 0.439352 -3.382316C0.439352 -3.02665 0.739228 -2.991781 0.808966 -2.991781C0.976339 -2.991781 1.17858 -3.110336 1.17858 -3.361395C1.17858 -3.486924 1.129763 -3.731009 0.767123 -3.731009C0.983313 -4.226152 1.457534 -4.379577 1.785305 -4.379577C2.48269 -4.379577 2.84533 -3.835616 2.84533 -3.270735C2.84533 -2.66401 2.412951 -2.182814 2.189788 -1.931756L0.509091 -0.27198C0.439352 -0.209215 0.439352 -0.195268 0.439352 0H3.312578L3.521793 -1.26924Z' id='g1-50'/>
<path d='M3.327522 -3.008717C3.387298 -3.267746 3.616438 -4.184309 4.313823 -4.184309C4.363636 -4.184309 4.60274 -4.184309 4.811955 -4.054795C4.533001 -4.004981 4.333748 -3.755915 4.333748 -3.516812C4.333748 -3.35741 4.443337 -3.16812 4.712329 -3.16812C4.931507 -3.16812 5.250311 -3.347447 5.250311 -3.745953C5.250311 -4.26401 4.662516 -4.403487 4.323786 -4.403487C3.745953 -4.403487 3.39726 -3.875467 3.277709 -3.646326C3.028643 -4.303861 2.49066 -4.403487 2.201743 -4.403487C1.165629 -4.403487 0.597758 -3.118306 0.597758 -2.86924C0.597758 -2.769614 0.697385 -2.769614 0.71731 -2.769614C0.797011 -2.769614 0.826899 -2.789539 0.846824 -2.879203C1.185554 -3.935243 1.843088 -4.184309 2.181818 -4.184309C2.371108 -4.184309 2.719801 -4.094645 2.719801 -3.516812C2.719801 -3.20797 2.550436 -2.540473 2.181818 -1.145704C2.022416 -0.52802 1.673724 -0.109589 1.235367 -0.109589C1.175592 -0.109589 0.946451 -0.109589 0.737235 -0.239103C0.986301 -0.288917 1.205479 -0.498132 1.205479 -0.777086C1.205479 -1.046077 0.986301 -1.125778 0.836862 -1.125778C0.537983 -1.125778 0.288917 -0.86675 0.288917 -0.547945C0.288917 -0.089664 0.787049 0.109589 1.225405 0.109589C1.882939 0.109589 2.241594 -0.587796 2.271482 -0.647572C2.391034 -0.278954 2.749689 0.109589 3.347447 0.109589C4.373599 0.109589 4.941469 -1.175592 4.941469 -1.424658C4.941469 -1.524284 4.851806 -1.524284 4.821918 -1.524284C4.732254 -1.524284 4.712329 -1.484433 4.692403 -1.414695C4.363636 -0.348692 3.686177 -0.109589 3.367372 -0.109589C2.978829 -0.109589 2.819427 -0.428394 2.819427 -0.767123C2.819427 -0.986301 2.879203 -1.205479 2.988792 -1.643836L3.327522 -3.008717Z' id='g0-120'/>
</defs>
<g id='page1'>
<use x='-72.000004' xlink:href='#g0-120' y='-63.890385'/>
<use x='-66.306072' xlink:href='#g1-50' y='-67.505749'/>
</g>
</svg>

Call dvisvgm from within LuaTeX

If you don't want to execute dvisvgm manually, you can also tell LuaTeX to execute it within the new wrapup_run callback (requires fairly new LuaTeX ≥ 1.08.0, I think). Just set \outputmode=0 to force LuaTeX to output DVI instead of PDF and make sure --shell-escape is enabled.

\outputmode=0 % write DVI instead of PDF
\documentclass{standalone}
\directlua{
if status.shell_escape \string~= 1 then
    error("dvisvgm requires shell-escape")
end

local function dvisvgm()
    os.execute("dvisvgm --no-fonts \jobname.dvi")
end

% luatexbase does not know the wrapup_run callback yet
luatexbase.callbacktypes.wrapup_run = 1 % simple
luatexbase.add_to_callback("wrapup_run", dvisvgm, "dvisvgm")
}
\begin{document}
$x^2$
\end{document}

The wrapup_run callback is actually essential for this because it is triggered after the output file has already been closed, so we can be sure that it is safe to read from the output file and there are no more pending writes.

Use Poppler, Cairo, and GLib via LuaTeX FFI

You could also use the FFI to render the resulting PDF into an SVG using Poppler, Cairo, and GLib. I have annotated the source with the packages you need to install on your system for this to work. Keep in mind that Poppler and Cairo are no longer bundled with LuaTeX, because with version 1.08.0 the PDF library was switched to pplib.

Currently the code is limited to converting the first page of the resulting PDF to SVG but that can be trivially extended. I borrowed a lot from this file:

https://github.com/dawbarton/pdf2svg/blob/master/pdf2svg.c

I have also made the Lua part of this code available on GitHub

https://gist.github.com/hmenke/9facc3fe5ede9ed46c1838a919f7376f#file-pdf-to-svg-lua

FFI requires --shell-escape.

\documentclass{standalone}
\usepackage{luacode}
\begin{luacode}
local ffi = require"ffi"

ffi.cdef[[
// Cairo types
typedef struct _cairo_surface cairo_surface_t;
typedef int cairo_status_t;
typedef struct _cairo cairo_t;

// Poppler types
typedef struct _PopplerPage PopplerPage;
typedef struct _PopplerDocument PopplerDocument;

// Glib types
typedef struct {
    int domain;
    int code;
    char *message;
} GError;

// Cairo functions
cairo_surface_t * cairo_svg_surface_create(const char *filename,
                                           double width_in_points,
                                           double height_in_points);
cairo_status_t cairo_surface_status(cairo_surface_t *surface);
cairo_t * cairo_create(cairo_surface_t *);
cairo_status_t cairo_status(cairo_t *cr);
void cairo_show_page(cairo_t *cr);
void cairo_destroy(cairo_t *);
void cairo_surface_destroy(cairo_surface_t *);

// Poppler functions
PopplerDocument * poppler_document_new_from_file(const char *uri,
                                                 const char *password,
                                                 GError **error);
int poppler_document_get_n_pages(PopplerDocument *document);
PopplerPage * poppler_document_get_page(PopplerDocument *document,
                                        int index);
void poppler_page_get_size(PopplerPage *page,
                           double *width,
                           double *height);
void poppler_page_render_for_printing(PopplerPage *page,
                                      cairo_t *cairo);

// Glib functions
char * g_get_current_dir(void);
char * g_build_filename(const char *first_element, ...);
char * g_filename_to_uri(const char *filename,
                         const char *hostname,
                         GError **error);
void g_free(void *);
void g_object_unref(void *);
]]

local POPPLER = ffi.load("poppler-glib") -- libpoppler-glib-dev
local CAIRO = ffi.load("cairo")          -- libcairo2-dev
local GLIB = ffi.load("gobject-2.0")     -- libglib2.0-dev

local CAIRO_STATUS_SUCCESS = 0

local errmessage = tex.error

local function page_to_svg(pdfname, svgname, idx)
    -- Allocate an error object
    local err = ffi.new("GError*[1]", ffi.NULL)

    -- Convert relative path to absolute path
    local currentdir = GLIB.g_get_current_dir()
    local absolutefilename = GLIB.g_build_filename(currentdir, pdfname, ffi.NULL)
    GLIB.g_free(currentdir)

    -- Convert path to URI
    local filename_uri = GLIB.g_filename_to_uri(absolutefilename, ffi.NULL, err)
    GLIB.g_free(absolutefilename)
    if filename_uri == ffi.NULL then
        errmessage(ffi.string(err[0].message))
    end

    -- Open PDF file
    local pdffile = POPPLER.poppler_document_new_from_file(filename_uri, ffi.NULL, err)
    GLIB.g_free(filename_uri)
    if pdffile == ffi.NULL then
        errmessage(ffi.string(err[0].message))
    end

    -- Test page count and get page
    local pagecount = POPPLER.poppler_document_get_n_pages(pdffile)
    if not (idx < pagecount) then
        errmessage("Page out of range (index " .. idx .. " >= " .. pagecount .. " pages)")
    end
    local page = POPPLER.poppler_document_get_page(pdffile, idx)

    -- Get page dimensions
    local width = ffi.new("double[1]")
    local height = ffi.new("double[1]")
    POPPLER.poppler_page_get_size(page, width, height)

    -- Open Cairo surface
    local surface = CAIRO.cairo_svg_surface_create(svgname, width[0], height[0]);
    local status = CAIRO.cairo_surface_status(surface)
    if status ~= CAIRO_STATUS_SUCCESS then
        errmessage("Cairo surface error (code " .. status .. ")")
    end

    -- Open Cairo context
    local cr = CAIRO.cairo_create(surface)
    local status = CAIRO.cairo_status(cr)
    if status ~= CAIRO_STATUS_SUCCESS then
        errmessage("Cairo error (code " .. status .. ")")
    end

    -- Render PDF in Cairo context
    POPPLER.poppler_page_render_for_printing(page, cr)
    CAIRO.cairo_show_page(cr)

    -- Clean up
    if (cr ~= ffi.NULL) then CAIRO.cairo_destroy(cr) end
    if (surface ~= ffi.NULL) then CAIRO.cairo_surface_destroy(surface) end
    if (page ~= ffi.NULL) then GLIB.g_object_unref(page) end
    if (pdffile ~= ffi.NULL) then GLIB.g_object_unref(pdffile) end
    if (err[0] ~= ffi.NULL) then GLIB.g_object_unref(err[0]) end
end

local function wrapup()
    page_to_svg("\jobname.pdf", "\jobname.svg", 0)
end

-- luatexbase does not know the wrapup_run callback yet
luatexbase.callbacktypes.wrapup_run = 1 -- simple
luatexbase.add_to_callback("wrapup_run", wrapup, "wrapup")
\end{luacode}
\begin{document}
$x^2$
\end{document}

Use MetaPost

If you only want to convert a single equation and don't require a lot of TeX's algorithms, then you could just use MetaPost and set the output format to SVG.

prologues := 3;
outputformat := "svg";
outputtemplate := "%j%c.svg";

beginfig(1)
  label(btex $x^2$ etex, origin);
endfig;
end

Running mpost test.mp generates test1.svg with the following content:

<?xml version="1.0"?>
<!-- Created by MetaPost 2.00 on 2019.02.14:1518 -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="10.163300" height="8.109604" viewBox="0 0 10.163300 8.109604">
<!-- Original BoundingBox: -5.081650 -4.054810 5.081650 4.054794 -->
  <defs>
    <g transform="scale(0.009963,0.009963)" id="GLYPHcmmi10_120">
      <path style="fill-rule: evenodd;" d="M334.000000 -302.000000C340.000000 -328.000000,363.000000 -420.000000,433.000000 -420.000000C438.000000 -420.000000,462.000000 -420.000000,483.000000 -407.000000C455.000000 -402.000000,435.000000 -377.000000,435.000000 -353.000000C435.000000 -337.000000,446.000000 -318.000000,473.000000 -318.000000C495.000000 -318.000000,527.000000 -336.000000,527.000000 -376.000000C527.000000 -428.000000,468.000000 -442.000000,434.000000 -442.000000C376.000000 -442.000000,341.000000 -389.000000,329.000000 -366.000000C304.000000 -432.000000,250.000000 -442.000000,221.000000 -442.000000C117.000000 -442.000000,60.000000 -313.000000,60.000000 -288.000000C60.000000 -278.000000,70.000000 -278.000000,72.000000 -278.000000C80.000000 -278.000000,83.000000 -280.000000,85.000000 -289.000000C119.000000 -395.000000,185.000000 -420.000000,219.000000 -420.000000C238.000000 -420.000000,273.000000 -411.000000,273.000000 -353.000000C273.000000 -322.000000,256.000000 -255.000000,219.000000 -115.000000C203.000000 -53.000000,168.000000 -11.000000,124.000000 -11.000000C118.000000 -11.000000,95.000000 -11.000000,74.000000 -24.000000C99.000000 -29.000000,121.000000 -50.000000,121.000000 -78.000000C121.000000 -105.000000,99.000000 -113.000000,84.000000 -113.000000C54.000000 -113.000000,29.000000 -87.000000,29.000000 -55.000000C29.000000 -9.000000,79.000000 11.000000,123.000000 11.000000C189.000000 11.000000,225.000000 -59.000000,228.000000 -65.000000C240.000000 -28.000000,276.000000 11.000000,336.000000 11.000000C439.000000 11.000000,496.000000 -118.000000,496.000000 -143.000000C496.000000 -153.000000,487.000000 -153.000000,484.000000 -153.000000C475.000000 -153.000000,473.000000 -149.000000,471.000000 -142.000000C438.000000 -35.000000,370.000000 -11.000000,338.000000 -11.000000C299.000000 -11.000000,283.000000 -43.000000,283.000000 -77.000000C283.000000 -99.000000,289.000000 -121.000000,300.000000 -165.000000"></path>
    </g>
    <g transform="scale(0.006974,0.006974)" id="GLYPHcmr7_50">
      <path style="fill-rule: evenodd;" d="M505.000000 -182.000000L471.000000 -182.000000C468.000000 -160.000000,458.000000 -101.000000,445.000000 -91.000000C437.000000 -85.000000,360.000000 -85.000000,346.000000 -85.000000L162.000000 -85.000000C267.000000 -178.000000,302.000000 -206.000000,362.000000 -253.000000C436.000000 -312.000000,505.000000 -374.000000,505.000000 -469.000000C505.000000 -590.000000,399.000000 -664.000000,271.000000 -664.000000C147.000000 -664.000000,63.000000 -577.000000,63.000000 -485.000000C63.000000 -434.000000,106.000000 -429.000000,116.000000 -429.000000C140.000000 -429.000000,169.000000 -446.000000,169.000000 -482.000000C169.000000 -500.000000,162.000000 -535.000000,110.000000 -535.000000C141.000000 -606.000000,209.000000 -628.000000,256.000000 -628.000000C356.000000 -628.000000,408.000000 -550.000000,408.000000 -469.000000C408.000000 -382.000000,346.000000 -313.000000,314.000000 -277.000000L73.000000 -39.000000C63.000000 -30.000000,63.000000 -28.000000,63.000000 -0.000000L475.000000 -0.000000"></path>
    </g>
  </defs>
  <g transform="translate(-0.081650 8.054810)" style="fill: rgb(0.000000%,0.000000%,0.000000%);">
    <use xlink:href="#GLYPHcmmi10_120"></use>
  </g>
  <g transform="translate(5.612244 4.439407)" style="fill: rgb(0.000000%,0.000000%,0.000000%);">
    <use xlink:href="#GLYPHcmr7_50"></use>
  </g>
</svg>
  • Thanks Henri (+1). The idea I had in mind was to avoid any other tools than luatex and hoping that luatex could be included to blender as a library. – cjorssen Feb 12 at 21:36
  • @cjorssen The only way to avoid other tools would be to have an svg output driver included LuaTeX (in addition to the DVI and PDF driver). However, judging from the complexity of dvisvgm, this is not an easy task. – Henri Menke Feb 12 at 22:58
  • I see. So no way to use some ffi magics or your other hack of '\shipout', even for a subset of a real page (eg one formula)? – cjorssen Feb 13 at 6:27
  • @cjorssen It might be possible but that would be an unreasonable amount of work. Especially when there is a solution which involves only two program calls. – Henri Menke Feb 13 at 7:07
  • Is it cairo or poppler that takes care of the "drawing" of fonts? – cjorssen Feb 14 at 12:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.