Fabric.js Document Engine

Search documentation

Find a page or section

中文

Save and load

View as Markdown

Serialize the canvas, store it anywhere, and load it back without surprises.

Saving is engine.toDocument(). A Fabric.js load from JSON is engine.loadDocument(json). Both work without any storage, so you can keep the JSON wherever you like.

Loading the live demo. Its code is below.

save-load-demo.tsx
"use client";
 
import type { FabricDocument } from "fabricjs-document-engine";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import { CircleIcon, FolderOpenIcon, SaveIcon, SquareIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
 
import {
  DemoCanvas,
  DemoFrame,
  newShape,
  seedShapes,
  Status,
  Toolbar,
  ToolButton,
} from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
 
export default function SaveLoadDemo() {
  const t = useTranslations("demos");
  const { elementRef, canvas } = useFabricCanvas();
  const engine = useDocumentEngine(canvas);
  const [saved, setSaved] = useState<FabricDocument>();
 
  useEffect(() => {
    if (!engine) return;
    engine.canvas.add(...seedShapes());
    engine.clearHistory();
  }, [engine]);
 
  function save() {
    // The whole canvas as plain JSON. Store it anywhere.
    setSaved(engine?.toDocument());
  }
 
  async function load() {
    if (saved) await engine?.loadDocument(saved);
  }
 
  return (
    <DemoFrame>
      <DemoCanvas elementRef={elementRef} label={t("canvas")} />
 
      <Toolbar>
        <ToolButton
          icon={SquareIcon}
          label={t("square")}
          onClick={() => canvas?.add(newShape("square"))}
        />
        <ToolButton
          icon={CircleIcon}
          label={t("circle")}
          onClick={() => canvas?.add(newShape("circle"))}
        />
        <span className="ml-auto flex gap-1.5">
          <ToolButton
            icon={SaveIcon}
            label={t("saveLoad.save")}
            primary
            disabled={!engine}
            onClick={save}
          />
          <ToolButton
            icon={FolderOpenIcon}
            label={t("saveLoad.load")}
            disabled={!saved}
            onClick={() => void load()}
          />
        </span>
      </Toolbar>
 
      <Status tone={saved ? "ok" : "idle"}>
        {saved
          ? t("saveLoad.saved", { count: saved.objects.length })
          : t("saveLoad.hint")}
      </Status>
 
      <pre className="h-32 overflow-auto rounded-lg border bg-muted p-3 font-mono text-xs">
        {saved ? JSON.stringify(saved, null, 2) : "{}"}
      </pre>
    </DemoFrame>
  );
}

Save the canvas

const document = engine.toDocument();
await fetch(`/api/documents/${document.id}`, {
  method: "PUT",
  body: JSON.stringify(document),
});

The result is a plain FabricDocument object. It holds the canvas size and background, every object in stacking order, the images and fonts the document needs, and your own metadata. The document format page describes each field.

Compared with Fabric.js toJSON, this serialization adds:

  • a stable id on every object, including children of groups,
  • the properties of your custom objects, without listing them on each call,
  • a schemaVersion, so future versions can upgrade old files.

Fabric.js load from JSON

const response = await fetch("/api/documents/plan-42");
await engine.loadDocument(await response.json());

Before the canvas is touched, the engine:

  1. validates the document and reports the exact path of any problem,
  2. upgrades older documents and plain Fabric.js JSON,
  3. refuses unknown object types, instead of turning them into something else,
  4. loads every image and font, and fails if an image is missing.

Only then does it turn the JSON to canvas objects. A failed load throws a DocumentEngineError and leaves the current canvas exactly as it was. With plain loadFromJSON, the canvas may already be cleared when an image fails.

When loads overlap

If the user opens document A and then document B before A finishes, B wins. The load of A is cancelled with LOAD_ABORTED, so it can never replace B:

engine.on("load:error", ({ error }) => {
  if (error.code === "LOAD_ABORTED") return;
  showMessage(`The document could not be opened: ${error.message}`);
});

Save and load with storage

With a storage adapter, you work by id instead of by JSON:

await engine.load("plan-42");
await engine.save();

This also turns on autosave and save conflict checks. See storage adapters for the built-in options.

Unsaved work is never replaced

With a storage adapter, load, loadDocument, importFabricJson and newDocument refuse with UNSAVED_CHANGES while there are unsaved changes. Save first, or pass { discardUnsavedChanges: true } when the user chose to throw the changes away.

Open old Fabric.js JSON

A Fabric.js load from JSON also works for plain canvas.toJSON() output from Fabric.js 5, 6 or 7. It is upgraded when it is opened. See migration.

Next steps