Fabric.js Document Engine

搜索文档

查找页面或章节

EN

SVG 导入

查看 Markdown

SVG 图形落在文件指定的位置,只算一步撤销。

在 Fabric.js 里加载 SVG,常见的做法是先用 loadSVGFromString,再用 util.groupSVGElements,它会按绘制的内容确定编组大小。SVG 的 viewBox 之外的元素,或者隐藏的元素,会让整幅图移动并改变大小。通过引擎做 Fabric.js 导入 SVG,则会保留文件自己的画框。

正在加载在线示例,代码在下方。

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>
  );
}

导入 SVG 文件

const { objects, viewport, warnings } = await engine.importSvg(svgText, {
  left: 40,
  top: 40,
});
  • 元素落在 SVG 放置它们的位置,已经应用了 viewBox 和 preserveAspectRatio。
  • 结果是一个固定布局、大小等于视口的编组,里面的任何内容都不会让它移动。
  • 导入只算一步撤销,每个对象都有 id。
  • viewport 是 SVG 画框的大小,想按指定尺寸放进画布时很有用。

打开用户选择的 SVG 文件:

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

放置和缩放图形

配置项取值默认值
left, top视口左上角落在画布上的位置0
fit{ width, height, mode },mode 为 'contain'、'cover' 或 'fill'无
offscreen'keep'、'drop' 或 'clip''keep'
as'group' 或 'objects''group'
viewport'preserve' 或 'content''preserve'
  • offscreen: "drop" 会略去完全在视口之外的元素,"clip" 会把视口外的部分裁掉。
  • as: "objects" 会在相同位置添加分开的对象,适合用户需要拆开编辑的文件。
  • viewport: "content" 按绘制内容的范围计算,和 Fabric 自己的编组一样。没有尺寸的 SVG 总是使用这种方式。

不可信的文件

每次 Fabric.js 导入 SVG 都会在 Fabric 解析之前先清理文件:

  • 移除脚本、onload 之类的事件处理器、foreignObject 和指向其他文件的链接。
  • limits.isAllowedUrl 拒绝的图片地址会被略去。
  • 元素过多或嵌套过深的 SVG 会以 UNSAFE_DOCUMENT 被拒绝,限制和文档使用的 limits 相同。
  • 不是 SVG 的文本会以 SVG_IMPORT_FAILED 拒绝。

warnings 会列出移除或略去的内容,代码有 SVG_CONTENT_REMOVED、SVG_IMAGE_BLOCKED 和 SVG_OFFSCREEN_DROPPED。要把画布导出回 SVG,请看 SVG 导出。配置项列在 DocumentEngine 方法里,安全限制见安全。