A plain Fabric.js export SVG call has two weak spots. Text on a path comes out in the wrong place, and the file links to its images by URL, so it shows empty boxes once those links stop working. The engine's SVG export fixes both, and every other object stays exactly as Fabric writes it.
Loading the live demo. Its code is below.
"use client";
import { FabricText, Path } from "fabric";
import { downloadExport } from "fabricjs-document-engine";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import { DownloadIcon, EyeIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import {
COLORS,
DemoCanvas,
DemoFrame,
Status,
Toolbar,
ToolButton,
} from "./demo-ui";
import { PAGE, useFabricCanvas } from "./use-fabric-canvas";
type Output = "vector" | "fabric";
export default function SvgExportDemo() {
const t = useTranslations("demos");
const { elementRef, canvas } = useFabricCanvas();
const engine = useDocumentEngine(canvas);
const [preview, setPreview] = useState<{
url: string;
output: Output;
} | null>(null);
const [message, setMessage] = useState("");
const words = t("svg.words");
// Text that follows a curve, centred on it, with a highlight behind it.
useEffect(() => {
if (!engine) return;
engine.canvas.add(
new FabricText(words, {
path: new Path("M 40 230 Q 240 20 440 230", {
fill: "",
stroke: "#d4d4d8",
strokeWidth: 2,
strokeDashArray: [6, 6],
}),
left: PAGE.width / 2,
top: PAGE.height / 2,
fontFamily: "sans-serif",
fontSize: 28,
fill: "#0d0d0d",
pathAlign: "center",
textAlign: "center",
textBackgroundColor: COLORS[4],
underline: true,
})
);
engine.clearHistory();
}, [engine, words]);
// The preview URL belongs to this component; free it when it changes.
useEffect(
() => () => {
if (preview) URL.revokeObjectURL(preview.url);
},
[preview]
);
async function render(output: Output, download: boolean) {
if (!engine) return;
const result = await engine.export({
format: "svg",
svg: { textOnPath: output, embedImages: true },
});
if (download) {
downloadExport(result, "curved-text.svg");
setMessage(t("svg.downloaded"));
return;
}
setPreview({ url: URL.createObjectURL(result.blob), output });
setMessage(
t(output === "vector" ? "svg.previewVector" : "svg.previewFabric")
);
}
return (
<DemoFrame>
<DemoCanvas elementRef={elementRef} label={t("canvas")} />
{preview && (
<figure className="overflow-hidden rounded-lg border bg-white">
{/* eslint-disable-next-line @next/next/no-img-element -- a blob URL of the export itself */}
<img
src={preview.url}
alt={t("svg.previewAlt")}
className="block w-full"
/>
</figure>
)}
<Toolbar>
<ToolButton
icon={DownloadIcon}
label={t("svg.download")}
primary
disabled={!engine}
onClick={() => void render("vector", true)}
/>
<ToolButton
icon={EyeIcon}
label={t("svg.showVector")}
disabled={!engine}
onClick={() => void render("vector", false)}
/>
<ToolButton
icon={EyeIcon}
label={t("svg.showFabric")}
disabled={!engine}
onClick={() => void render("fabric", false)}
/>
</Toolbar>
<Status tone={message ? "ok" : "idle"}>{message || t("svg.hint")}</Status>
</DemoFrame>
);
}Curved text
Fabric.js has no <textPath> in its SVG. It writes text on a path as rotated letters, and that output ignores pathAlign, moves raised letters the wrong way, and draws text backgrounds and underlines as straight boxes. When the text contains a space, Fabric 6 and 7 even write invalid XML, which browsers and Illustrator refuse to open.
const result = await engine.export({ format: "svg" });Every SVG export now writes each letter of curved text where the canvas draws it, with its background and decorations in the same place:
pathAlign,pathSide,pathStartOffsetandtextAlignalong the path- letter styles, raised and lowered letters (
deltaY), outlines andpaintFirst - text backgrounds, underlines, overlines and line-throughs
- the guide path itself, when it is visible
Each letter is plain <text> with a transform, which every SVG reader understands, and it stays editable text. Pass svg: { textOnPath: "fabric" } to keep Fabric's own output instead.
Embed images and fonts
const result = await engine.export({
format: "svg",
svg: {
embedImages: true,
embedFonts: { "Brand Sans": "/fonts/brand-sans.woff2" },
},
});embedImages: trueputs every image into the file as data, including pattern fills and images in groups. An image from another site without CORS stays a link, with anIMAGE_NOT_EMBEDDEDwarning that names the objects.embedImages: "require"blocks the export withEXPORT_BLOCKEDinstead, when an image cannot be embedded.maxEmbeddedImageBytesleaves out images larger than the limit (25 MB by default).embedFontsmaps a font family to a file: a URL or the file's bytes. Fonts set on single letters are embedded too, which Fabric's own@font-faceoutput misses.
When to use which option
Every Fabric.js export SVG call accepts these options, so pick them by where the file is going:
| Need | Option |
|---|---|
| SVG export that looks like the canvas | the defaults |
| A file for Illustrator or another device | embedImages: true |
| Brand fonts on any computer | embedFonts |
| Fabric's own output, for comparison | textOnPath: "fabric" |
For text on a path inside a group, set objectCaching: false on the group. With caching on, Fabric clips letters that rise above the path on the canvas, while the SVG shows them. More export formats are on the export page, and the PDF export uses the same curved text fixes.