在 Fabric.js 里加载 SVG,常见的做法是先用 loadSVGFromString,再用 util.groupSVGElements,它会按绘制的内容确定编组大小。SVG 的 viewBox 之外的元素,或者隐藏的元素,会让整幅图移动并改变大小。通过引擎做 Fabric.js 导入 SVG,则会保留文件自己的画框。
正在加载在线示例,代码在下方。
"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 方法里,安全限制见安全。