# Export

> Fabric.js export image files as PNG, JPEG or WebP, export SVG or export to JSON. Pick the area and scale, and find broken images before export fails.

Source: https://fabricjs-document-engine.jscrate.dev/docs/guides/export
Last updated: 2026-09-28

One call handles every Fabric.js export image format, plus SVG and JSON. You choose the area, the scale and the background. The current zoom and pan do not matter, because exports always use document coordinates.

```tsx
"use client";

import { Textbox } from "fabric";
import { downloadExport, type ExportFormat } from "fabricjs-document-engine";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import { BracesIcon, FileCodeIcon, ImageIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";

import {
  DemoCanvas,
  DemoFrame,
  seedShapes,
  Status,
  Toolbar,
  ToolButton,
} from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";

const FORMATS = [
  { format: "png", icon: ImageIcon },
  { format: "svg", icon: FileCodeIcon },
  { format: "json", icon: BracesIcon },
] as const;

export default function ExportDemo() {
  const t = useTranslations("demos");
  const { elementRef, canvas } = useFabricCanvas();
  const engine = useDocumentEngine(canvas);
  const [message, setMessage] = useState("");
  const title = t("export.title");

  // A small drawing to export. Text is measured once its font has loaded.
  useEffect(() => {
    if (!engine) return;
    document.fonts.load("600 32px Yellix").finally(() => {
      engine.canvas.add(
        ...seedShapes(),
        new Textbox(title, {
          originX: "left",
          originY: "top",
          left: 48,
          top: 108,
          width: 176,
          fontFamily: "Yellix",
          fontSize: 26,
          fontWeight: 600,
          textAlign: "center",
          fill: "#0d0d0d",
        })
      );
      engine.clearHistory();
    });
  }, [engine, title]);

  async function download(format: ExportFormat) {
    if (!engine) return;
    try {
      const result = await engine.export({ format, scale: 2 });
      downloadExport(result, `drawing.${format}`);
      setMessage(
        t("export.done", {
          width: result.width,
          height: result.height,
          format: format.toUpperCase(),
        })
      );
    } catch (error) {
      setMessage(t("export.failed", { message: String(error) }));
    }
  }

  return (
    <DemoFrame>
      <DemoCanvas elementRef={elementRef} label={t("canvas")} />

      <Toolbar>
        {FORMATS.map(({ format, icon }) => (
          <ToolButton
            key={format}
            icon={icon}
            label={format.toUpperCase()}
            disabled={!engine}
            onClick={() => void download(format)}
          />
        ))}
      </Toolbar>

      <Status tone={message ? "ok" : "idle"}>
        {message || t("export.hint")}
      </Status>
    </DemoFrame>
  );
}
```

```tsx
import { Canvas } from "fabric";
import { useLayoutEffect, useRef, useState } from "react";

/**
 * The page every demo draws on. The document keeps this size; CSS scales the
 * canvas down to fit its card, so saved documents and exports never depend on
 * the screen.
 */
export const PAGE = { width: 480, height: 300 };

// Selection handles in the site's style. Fabric does not save these.
const SELECTION = {
  borderColor: "#6f00ff",
  borderScaleFactor: 1.5,
  borderOpacityWhenMoving: 0.5,
  cornerStyle: "circle",
  cornerSize: 11,
  cornerColor: "#ffffff",
  cornerStrokeColor: "#6f00ff",
  transparentCorners: false,
} as const;

export function styleSelection(canvas: Canvas) {
  canvas.set({
    selectionColor: "rgba(111, 0, 255, 0.06)",
    selectionBorderColor: "#6f00ff",
    selectionLineWidth: 1,
  });
  const style = () => canvas.getActiveObject()?.set(SELECTION);
  canvas.on("selection:created", style);
  canvas.on("selection:updated", style);
}

/**
 * Creates a Fabric.js canvas once the `<canvas>` element exists, and disposes
 * it on unmount. The engine is created from the returned `canvas`.
 */
export function useFabricCanvas({ width, height } = PAGE) {
  const elementRef = useRef<HTMLCanvasElement>(null);
  const [canvas, setCanvas] = useState<Canvas | null>(null);

  // A layout effect creates the canvas before the first paint.
  useLayoutEffect(() => {
    const created = new Canvas(elementRef.current!, {
      width,
      height,
      backgroundColor: "#ffffff",
    });
    styleSelection(created);
    setCanvas(created);

    return () => {
      // Dispose after the engine's own cleanup, so its last recovery copy
      // still sees the objects.
      setTimeout(() => created.dispose().catch(() => undefined));
    };
  }, [width, height]);

  return { elementRef, canvas };
}
```

```tsx
import { Circle, type FabricObject, Rect, Triangle } from "fabric";
import type { LucideIcon } from "lucide-react";

import { cn } from "@/lib/utils";

import { PAGE } from "./use-fabric-canvas";

export const COLORS = ["#f2836f", "#5fe6c4", "#7ad9ec", "#6f00ff", "#ffd666"];

// Demo shapes are placed by their top-left corner, like a design tool.
const CORNER = { originX: "left", originY: "top" } as const;

/** A new shape at a random spot on the page. */
export function newShape(kind: "square" | "circle", fill?: string) {
  const left = 40 + Math.random() * (PAGE.width - 160);
  const top = 30 + Math.random() * (PAGE.height - 120);
  const color = fill ?? COLORS[Math.floor(Math.random() * COLORS.length)];
  return kind === "square"
    ? new Rect({
        ...CORNER,
        left,
        top,
        width: 88,
        height: 64,
        rx: 10,
        ry: 10,
        fill: color,
      })
    : new Circle({ ...CORNER, left, top, radius: 34, fill: color });
}

/** A small composed drawing, so no demo starts on an empty page. */
export function seedShapes(): FabricObject[] {
  return [
    new Rect({
      ...CORNER,
      left: 48,
      top: 56,
      width: 176,
      height: 124,
      rx: 16,
      ry: 16,
      fill: COLORS[4],
    }),
    new Circle({ ...CORNER, left: 268, top: 44, radius: 58, fill: COLORS[0] }),
    new Rect({
      ...CORNER,
      left: 256,
      top: 196,
      width: 176,
      height: 52,
      rx: 26,
      ry: 26,
      fill: COLORS[1],
    }),
    new Triangle({
      ...CORNER,
      left: 96,
      top: 196,
      width: 72,
      height: 60,
      fill: COLORS[3],
    }),
  ];
}

export function DemoFrame({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      className={cn(
        "flex w-full max-w-md flex-col gap-3 text-left outline-none",
        className
      )}
      {...props}
    />
  );
}

/**
 * Scales the fixed-size Fabric canvas to the frame's width. The rules are
 * `!important` because Fabric writes its sizes as inline styles.
 */
export function DemoCanvas({
  elementRef,
  label,
  page = PAGE,
  children,
}: {
  elementRef: React.Ref<HTMLCanvasElement>;
  label: string;
  page?: { width: number; height: number };
  children?: React.ReactNode;
}) {
  return (
    <div
      className="relative overflow-hidden rounded-lg border bg-white shadow-xs [&_.canvas-container]:aspect-(--page)! [&_.canvas-container]:h-auto! [&_.canvas-container]:w-full! [&_canvas]:h-full! [&_canvas]:w-full!"
      style={
        { "--page": `${page.width} / ${page.height}` } as React.CSSProperties
      }
    >
      <div>
        <canvas ref={elementRef} aria-label={label} />
      </div>
      {children}
    </div>
  );
}

export function Toolbar({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      className={cn("flex flex-wrap items-center gap-1.5", className)}
      {...props}
    />
  );
}

export function ToolButton({
  icon: Icon,
  label,
  iconOnly = false,
  primary = false,
  className,
  ...props
}: React.ComponentProps<"button"> & {
  icon: LucideIcon;
  label: string;
  iconOnly?: boolean;
  primary?: boolean;
}) {
  return (
    <button
      type="button"
      title={label}
      aria-label={iconOnly ? label : undefined}
      className={cn(
        "inline-flex h-8 min-w-0 items-center justify-center gap-1.5 rounded-md border px-2.5 text-[13px] font-medium whitespace-nowrap transition-colors disabled:pointer-events-none disabled:opacity-40 [&_svg]:size-3.5 [&_svg]:shrink-0",
        primary
          ? "border-transparent bg-foreground text-background hover:bg-foreground/85"
          : "bg-background hover:bg-muted",
        iconOnly && "w-8 px-0",
        className
      )}
      {...props}
    >
      <Icon aria-hidden />
      {!iconOnly && <span className="truncate">{label}</span>}
    </button>
  );
}

const TONES = {
  idle: "bg-zinc-400",
  busy: "bg-amber-400",
  ok: "bg-emerald-500",
  error: "bg-red-500",
};

/** One line of status text. Its height never changes, so the card never jumps. */
export function Status({
  tone = "idle",
  children,
}: {
  tone?: keyof typeof TONES;
  children: React.ReactNode;
}) {
  return (
    <p
      role="status"
      className="flex h-5 min-w-0 items-center gap-2 text-[13px] text-muted-foreground"
    >
      <span
        aria-hidden
        className={cn("size-1.5 shrink-0 rounded-full", TONES[tone])}
      />
      <span className="truncate">{children}</span>
    </p>
  );
}
```

## Export and download

```ts
import { downloadExport } from "fabricjs-document-engine";

const result = await engine.export({ format: "png", scale: 2 });
downloadExport(result, "poster.png");
```

`result` is `{ format, mimeType, blob, width, height, warnings }`. Upload the `blob`, show it in an `<img>`, or download it with `downloadExport`.

## Options

| Option       | Values                                                                   | Default    |
| ------------ | ------------------------------------------------------------------------ | ---------- |
| `format`     | `'png'`, `'jpeg'`, `'webp'`, `'svg'` or `'json'`                         | required   |
| `scale`      | Output size multiplier, such as `2` for retina                           | `1`        |
| `quality`    | 0 to 1, for JPEG and WebP                                                | `0.92`     |
| `area`       | `'canvas'`, `'content'`, `'selection'` or `{ left, top, width, height }` | `'canvas'` |
| `padding`    | Extra space around `content` or `selection`                              | `0`        |
| `background` | `'keep'`, `'transparent'` or any CSS color                               | `'keep'`   |
| `signal`     | An `AbortSignal` to cancel                                               | —          |

- `'content'` crops to the objects, which is handy for a canvas to image thumbnail.
- A JPEG has no transparency, so an empty or transparent background becomes white, not black.
- The export never changes the canvas, the undo history or the unsaved state.

## Export SVG

```ts
const svg = await engine.export({
  format: "svg",
  area: "content",
  padding: 20,
});
```

To export SVG, the engine escapes all text, so a text box that contains `<script>` stays text.

## Export to JSON

```ts
const { document } = await engine.export({ format: "json" });
```

A JSON export is the same portable document a save produces. When `assets.upload` is set, tab-only images are uploaded first, so the file opens on any device.

## Check before exporting

A tainted canvas is the classic reason a Fabric.js export image call fails. Before rendering, the engine checks every object:

- `MISSING_IMAGE`: an image did not load.
- `CROSS_ORIGIN_IMAGE`: an image from another site without CORS would make the browser block a PNG, JPEG or WebP export. SVG and JSON are not affected.
- `MISSING_FONT`: a font is not available and `assets.requireFonts` is on.

If anything is found, `export` rejects with `EXPORT_BLOCKED` and `error.problems` lists each `{ code, message, url, objectIds }`. Run the same check first to show it in your menu:

```ts
const check = await engine.preflightExport({ format: "png" });
if (!check.ok) showProblems(check.problems);
```

Fixes for each problem are in [images and fonts](https://fabricjs-document-engine.jscrate.dev/docs/guides/assets-and-fonts). A PDF export is not included; render a Fabric.js export image and place it in a PDF library of your choice.
