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>
luatex
level, not requiring other dependencies such aspdftocairo
ordvisvgm
. In additionluatex
could labelsvg
path according to its mathematical meaning.