Files
d3a9c2a195 fix(packages/excalidraw): complete cross-document runtime ownership (#11997)
* fix: complete cross-document runtime ownership

* fix: schedule cross-document animations in owner window

* fix: Prewarm the right font even if it's not Excalifont

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Hyperlink and tooltip host staleness issues

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Various hyperlink fixes

* feat: Host abstraction

* fix: exportToSvg support for document override

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Remove caches

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Refactor tests (cut non-critical)

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Break out host from App

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: hold after release fixes

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: repaint when link image decode is complete per app

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Font loading per-doc test

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Lint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: draw SVG frame labels without text measurement

Frame labels were baked into exports as measured, ellipsis-truncated text
elements, which made `exportToSvg` depend on a canvas (through the render
environment) for the only text it ever measures.

- add `getFrameLabel` (element package): a measurement-free label spec --
  position, font, color, arithmetic height, the frame's width as max width
- SVG: draw labels from the spec in the frame branch, full title clipped to
  the frame's width; extract `appendSvgTextLines` so text elements and
  labels produce identical markup
- canvas: unchanged output (measured, truncated text elements), now built
  from the same spec
- fit label boxes into the SVG export bounds explicitly (`extraBounds`)
- test: SVG export of a named frame succeeds with a render environment
  whose `createCanvas` throws and creates no canvas through `document`

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: measurement-free embed placeholders; no render environment in SVG export

The placeholder drawn for an embed that isn't rendered live was the last
text `exportToSvg` measured, which kept a canvas (via the render
environment) in the SVG path and made headless SVG export throw on any
scene with an embed unless the process had been polyfilled.

- show the placeholder as a single line capped at 30 characters
  (`getPlaceholderLinkText`: scheme and `www.` dropped, ellipsis) instead
  of wrapping the whole link: at or below that length the existing
  `width / length` font size fits the box by construction, so nothing is
  measured, wrapped or clipped, and output is identical on every host. The
  full link stays on the element's `<a href>` in SVG
- `newTextElement` accepts pre-computed `metrics` to skip measuring
- drop `renderEnvironment` from `SVGRenderConfig`, `exportToSvg` and the
  `@excalidraw/utils` signature: SVG export's only host dependency is its
  owner document
- throw a guided error from the default canvas/image factories when there
  is no `document`/`Image`, instead of a bare ReferenceError
- tests: placeholder text capping and layout; SVG export of an iframe and
  a long-link embed with a render environment whose `createCanvas` throws

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: scope top picks drag to source window

* feat: StickyNotes support

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Revert "fix: measurement-free embed placeholders; no render environment in SVG export"

This reverts commit 886bea142. Rich text makes SVG export measure text
anyway (explicit x per line, run boundaries, baselines), so placeholder
labels go back to being measured and wrapped like on canvas, through the
render environment.

Kept: the guided errors thrown by the default canvas/image factories when
there is no `document`/`Image`.

Claude Opus 5.5 (xhigh)

* Revert "fix: draw SVG frame labels without text measurement"

This reverts commit 295e7d88b. SVG export measures text again (rich text
needs it regardless), so both formats go back to master's single path:
frame labels baked in as measured, truncated text elements, measured
through the export's render environment. Drops `getFrameLabel` and the
SVG label renderer.

Claude Opus 5.5 (xhigh)

* export: one `config.environment` for every format, `document` for SVG

SVG exports take the same environment as canvas ones: they measure text
(frame labels, embed placeholders) through its `createCanvas`, and are
built in its `document` (was the SVG-only `ownerDocument`).

Claude Opus 5.5 (xhigh)

* RenderEnvironment: carry the `document`

The environment is the one host parameter: its `document` is what SVG
exports are built in (the editor's owner document by default), so export
call sites pass a single environment instead of an environment plus an
`ownerDocument`. `exportCanvas` drops its `ownerDocument` option, and
`config.environment.document` defaults to the environment's.

Rich text's DOM layout will read the same member, rather than threading
`ownerDocument` through the measuring functions next to the environment.

Claude Opus 5.5 (xhigh)

* drop the process-wide `setRenderEnvironment` / `resetRenderEnvironment`

Every render and export now takes its environment explicitly: exports
through `config.environment`, the editor through its `renderEnvironment`
prop. Text measured outside both (e.g. creating elements on a server) has
`setCustomTextMetricsProvider`. A second, global way to configure the same
thing would be state shared by every job in the process, which is what
per-call export environments avoid. The default is now a constant
(the browser's).

Claude Opus 5.5 (xhigh)

* render: log per-element failures on export too, rather than throwing

As in the editor (and on master): one element that fails to render is
logged and skipped, so the rest of the exported scene survives it rather
than the whole export failing.

Claude Opus 5.5 (xhigh)

* RenderEnvironment: carry the text metrics provider

`textMetrics` replaces the per-environment registry of
`setCustomTextMetricsProvider(provider, env)`, which looked providers up
by the environment's identity, so any rebuilt environment (as exports
build one from `config.environment`) silently lost them. It now travels
with the environment, and `config.environment.textMetrics` measures an
export's text. The global `setCustomTextMetricsProvider(provider)` stays
for text measured outside both (e.g. elements created on a server).

Exports use a complete `config.environment` as-is (the editor's keeps its
caches warm), and document that it needn't be the same object across
exports. Frame label truncation measures through the environment too,
rather than on a canvas of its own.

Claude Opus 5.5 (xhigh)

* render: restore the canvas state when an element fails to draw

The scene renderer logs a failing element and carries on, but the
`save()`s it had made were never restored: its transforms and frame clip
stayed on the context for every element after it (a failing freedraw at
(80, 30) moved the next rectangle from (10, 10) to (90, 40)). Every
`save()` around drawing now restores in a `finally` (`withSavedContext`).
Pre-existing on master, in the editor and on export.

Claude Opus 5.5 (xhigh)

* export: truncate frame names down to "...", or none, to fit the frame

When not even one character plus the ellipsis fit, the frame name kept
its full title (overflowing narrow frames into their neighbors) while
reporting the frame's width. It now shrinks to just "...", and is dropped
where even that doesn't fit. Pre-existing on master.

Claude Opus 5.5 (xhigh)

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-20 05:53:27 +02:00
..

@excalidraw/utils

Install

npm install @excalidraw/utils

If you prefer Yarn over npm, use this command to install the Excalidraw utils package:

yarn add @excalidraw/utils

API

serializeAsJSON

See serializeAsJSON for API and description.

exportToBlob (async)

Export an Excalidraw diagram to a Blob.

exportToSvg

Export an Excalidraw diagram to a SVGElement.

Usage

Excalidraw utils is published as a UMD (Universal Module Definition). If you are using a module bundler (for instance, Webpack), you can import it as an ES6 module:

import { exportToSvg, exportToBlob } from "@excalidraw/utils";

To use it in a browser directly:

<script src="https://unpkg.com/@excalidraw/utils@0.1.0/dist/excalidraw-utils.min.js"></script>
<script>
  // ExcalidrawUtils is a global variable defined by excalidraw.min.js
  const { exportToSvg, exportToBlob } = ExcalidrawUtils;
</script>

Here's the exportToBlob and exportToSvg functions in action:

const excalidrawDiagram = {
  type: "excalidraw",
  version: 2,
  source: "https://excalidraw.com",
  elements: [
    {
      id: "vWrqOAfkind2qcm7LDAGZ",
      type: "ellipse",
      x: 414,
      y: 237,
      width: 214,
      height: 214,
      angle: 0,
      strokeColor: "#000000",
      backgroundColor: "#15aabf",
      fillStyle: "hachure",
      strokeWidth: 1,
      strokeStyle: "solid",
      roughness: 1,
      opacity: 100,
      groupIds: [],
      roundness: null,
      seed: 1041657908,
      version: 120,
      versionNonce: 1188004276,
      isDeleted: false,
      boundElementIds: null,
    },
  ],
  appState: {
    viewBackgroundColor: "#ffffff",
    gridSize: null,
  },
};

// Export the Excalidraw diagram as SVG string
const svg = exportToSvg(excalidrawDiagram);
console.log(svg.outerHTML);

// Export the Excalidraw diagram as PNG Blob URL
(async () => {
  const blob = await exportToBlob({
    ...excalidrawDiagram,
    mimeType: "image/png",
  });

  const urlCreator = window.URL || window.webkitURL;
  console.log(urlCreator.createObjectURL(blob));
})();