Fabric.js Document Engine

Search documentation

Find a page or section

中文

SVG artwork that lands where the file says, as one undo step.

The usual way to load SVG into Fabric.js is loadSVGFromString followed by util.groupSVGElements, which sizes the group to what is drawn. An element outside the SVG's viewBox, or a hidden one, then moves and resizes the whole artwork. A Fabric.js import SVG through the engine keeps the file's own frame instead.

Loading the live demo. Its code is below.

svg-import-demo.tsx
"use client";
 
import { type FabricObject, loadSVGFromString, util } from "fabric";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import { FileUpIcon, ImportIcon, ScissorsIcon, ShapesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRef, useState } from "react";
 
import { DemoCanvas, DemoFrame, Status, Toolbar, ToolButton } from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
 
/**
 * A badge drawn in a 240 × 150 viewBox, plus one shape far outside it and
 * one hidden shape. Grouping by content lets those two move the badge.
 */
const SAMPLE = `<svg xmlns="http://www.w3.org/2000/svg" width="240" height="150" viewBox="0 0 240 150">
  <rect x="10" y="10" width="220" height="130" rx="18" fill="#ffd666"/>
  <circle cx="70" cy="75" r="36" fill="#f2836f"/>
  <rect x="124" y="52" width="86" height="16" rx="8" fill="#0d0d0d"/>
  <rect x="124" y="82" width="60" height="16" rx="8" fill="#6f00ff"/>
  <circle cx="900" cy="600" r="40" fill="#5fe6c4"/>
  <rect x="-500" y="-300" width="40" height="40" fill="#7ad9ec" style="display:none"/>
</svg>`;
 
export default function SvgImportDemo() {
  const t = useTranslations("demos");
  const { elementRef, canvas } = useFabricCanvas();
  const engine = useDocumentEngine(canvas);
  const fileRef = useRef<HTMLInputElement>(null);
  const [message, setMessage] = useState("");
 
  function clear() {
    if (!engine) return;
    engine.canvas.remove(...engine.canvas.getObjects());
  }
 
  // The usual way: Fabric's parser, then a group sized to the content.
  async function importWithFabric() {
    if (!engine) return;
    clear();
    const { objects, options } = await loadSVGFromString(SAMPLE);
    const group = util.groupSVGElements(
      objects.filter(Boolean) as FabricObject[],
      options
    );
    group.set({ left: 120, top: 75, originX: "left", originY: "top" });
    engine.canvas.add(group);
    setMessage(t("svgImport.fabric"));
  }
 
  async function importSvg(svg: string, offscreen: "keep" | "clip") {
    if (!engine) return;
    clear();
    try {
      const { warnings } = await engine.importSvg(svg, {
        left: 120,
        top: 75,
        offscreen,
      });
      setMessage(
        warnings.length > 0
          ? t("svgImport.warnings", { count: warnings.length })
          : t("svgImport.engine")
      );
    } catch (error) {
      setMessage(t("svgImport.failed", { message: String(error) }));
    }
  }
 
  async function openFile(file: File | undefined) {
    if (!engine || !file) return;
    clear();
    try {
      await engine.importSvg(await file.text(), {
        left: 20,
        top: 20,
        fit: { width: 440, height: 260 },
      });
      setMessage(t("svgImport.file", { name: file.name }));
    } catch (error) {
      setMessage(t("svgImport.failed", { message: String(error) }));
    }
  }
 
  return (
    <DemoFrame>
      <DemoCanvas elementRef={elementRef} label={t("canvas")} />
 
      <Toolbar>
        <ToolButton
          icon={ImportIcon}
          label={t("svgImport.withEngine")}
          primary
          disabled={!engine}
          onClick={() => void importSvg(SAMPLE, "keep")}
        />
        <ToolButton
          icon={ShapesIcon}
          label={t("svgImport.withFabric")}
          disabled={!engine}
          onClick={() => void importWithFabric()}
        />
        <ToolButton
          icon={ScissorsIcon}
          label={t("svgImport.clip")}
          disabled={!engine}
          onClick={() => void importSvg(SAMPLE, "clip")}
        />
        <ToolButton
          icon={FileUpIcon}
          label={t("svgImport.open")}
          disabled={!engine}
          onClick={() => fileRef.current?.click()}
        />
        <input
          ref={fileRef}
          type="file"
          accept=".svg,image/svg+xml"
          className="sr-only"
          tabIndex={-1}
          aria-hidden
          onChange={(event) => {
            void openFile(event.target.files?.[0]);
            event.target.value = "";
          }}
        />
      </Toolbar>
 
      <Status tone={message ? "ok" : "idle"}>
        {message || t("svgImport.hint")}
      </Status>
    </DemoFrame>
  );
}

Import an SVG file

const { objects, viewport, warnings } = await engine.importSvg(svgText, {
  left: 40,
  top: 40,
});
  • Elements land where the SVG puts them, after viewBox and preserveAspectRatio.
  • The result is one group with a fixed layout the size of the viewport, so nothing inside can move it.
  • The import is one undo step, and every object gets an id.
  • viewport is the size of the SVG's frame, which helps when you add SVG to canvas at a chosen size.

To open a file the user picks:

input.addEventListener("change", async () => {
  const file = input.files?.[0];
  if (file) await engine.importSvg(await file.text(), { fit: { width: 400, height: 300 } });
});

Place and fit the artwork

OptionValuesDefault
left, topWhere the viewport's top-left corner lands0
fit{ width, height, mode }, mode is 'contain', 'cover' or 'fill'none
offscreen'keep', 'drop' or 'clip''keep'
as'group' or 'objects''group'
viewport'preserve' or 'content''preserve'
  • offscreen: "drop" leaves out elements entirely outside the viewport, and "clip" hides what lies outside it.
  • as: "objects" adds separate objects in the same places, for files the user will take apart.
  • viewport: "content" uses the bounds of what is drawn, like Fabric's own grouping. An SVG with no size always uses it.

Untrusted files

Every Fabric.js import SVG call cleans the file before Fabric parses it:

  • Scripts, event handlers such as onload, foreignObject and links to other files are removed.
  • Image addresses that limits.isAllowedUrl refuses are left out.
  • An SVG with too many elements, or nested too deep, is refused with UNSAFE_DOCUMENT, using the same limits as documents.
  • Text that is not an SVG rejects with SVG_IMPORT_FAILED.

warnings lists what was removed or dropped, with the codes SVG_CONTENT_REMOVED, SVG_IMAGE_BLOCKED and SVG_OFFSCREEN_DROPPED. To export the canvas back to SVG, see SVG export. The options are listed with the DocumentEngine methods, and the safety limits on the security page.