{"_id":"@acetrumtech/svg-to-fabric","name":"@acetrumtech/svg-to-fabric","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@acetrumtech/svg-to-fabric","version":"0.1.0","description":"Turn SVG files into editable, named Fabric.js layers — SVG to Fabric.js JSON with the group hierarchy preserved.","homepage":"https://acetrum.com","author":{"name":"avneesh","email":"info.acetrum@gmail.com","url":"https://acetrum.com"},"repository":{"type":"git","url":"git+https://github.com/acetrumtech/svg-to-fabric.git"},"bugs":{"url":"https://github.com/acetrumtech/svg-to-fabric/issues","email":"info.acetrum@gmail.com"},"type":"module","license":"MIT","sideEffects":false,"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"types":"./dist/index.d.ts","module":"./dist/index.js","scripts":{"build":"vite build && npm run build:types","build:types":"tsc -p tsconfig.build.json","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","demo":"vite --config demo/vite.config.ts","demo:build":"vite build --config demo/vite.config.ts","prepack":"npm run build","prepublishOnly":"npm run typecheck && npm test && npm run build"},"peerDependencies":{"fabric":">=7 <8"},"devDependencies":{"@types/node":"^24.0.0","@types/react":"^19.2.18","@types/react-dom":"^19.2.4","fabric":"^7.4.0","jsdom":"^26.0.0","react":"^19.2.8","react-dom":"^19.2.8","typescript":"^5.7.0","vite":"^7.0.0","vitest":"^3.0.0"},"publishConfig":{"access":"public"},"engines":{"node":">=20"},"keywords":["svg","fabric","fabricjs","canvas","converter","layers","editor","design"],"gitHead":"77591e2bbb9b7253412ace40e576f71ff30179e6","_id":"@acetrumtech/svg-to-fabric@0.1.0","_nodeVersion":"24.13.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-FnVSSlqOE61fEjHKLs3egFFg9kfsP1sVvgBQ2MwcWFkVpgZ8ek73VzzGttefm06LM5799LkxIIh5joGBNI+coA==","shasum":"6ef9b91aa8170877a9e6676343e4019efd72f47d","tarball":"https://registry.npmjs.org/@acetrumtech/svg-to-fabric/-/svg-to-fabric-0.1.0.tgz","fileCount":23,"unpackedSize":91567,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGk580oOPaNLRn3r2no4M8hXdp/s6YmDytRaZFIrCt94AiEAs72qCFJIm38CdOCV+85pAhuSVsKmRcHTme/1zM11aZQ="}]},"_npmUser":{"name":"acetrumtech","email":"info.acetrum@gmail.com"},"directories":{},"maintainers":[{"name":"acetrumtech","email":"info.acetrum@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/svg-to-fabric_0.1.0_1786880019304_0.0281830454295664"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-16T11:33:39.163Z","0.1.0":"2026-08-16T11:33:39.470Z","modified":"2026-08-16T11:33:39.617Z"},"maintainers":[{"name":"acetrumtech","email":"info.acetrum@gmail.com"}],"description":"Turn SVG files into editable, named Fabric.js layers — SVG to Fabric.js JSON with the group hierarchy preserved.","homepage":"https://acetrum.com","keywords":["svg","fabric","fabricjs","canvas","converter","layers","editor","design"],"repository":{"type":"git","url":"git+https://github.com/acetrumtech/svg-to-fabric.git"},"author":{"name":"avneesh","email":"info.acetrum@gmail.com","url":"https://acetrum.com"},"bugs":{"url":"https://github.com/acetrumtech/svg-to-fabric/issues","email":"info.acetrum@gmail.com"},"license":"MIT","readme":"# @acetrumtech/svg-to-fabric\n\nTurn an SVG into editable, **named** Fabric.js layers.\n\nFabric already ships an SVG parser, and it is a good one — it resolves the CSS\ncascade, `<use>` references, gradient units, nested transforms and the viewBox.\nWhat it hands back is a flat list of objects with every ancestor transform baked\nin. That is exactly right for drawing, and useless for a layers panel: the `<g>`\nstructure the designer built is gone, and so are the names.\n\nThis package pairs that flat list back up with the elements it came from,\nrebuilds the group hierarchy, names every layer the way its exporter meant it to\nbe named, and hands you either a nested Fabric `Group` tree or a flat list that\nstill knows where it came from.\n\n```bash\nnpm install @acetrumtech/svg-to-fabric fabric\n```\n\n---\n\n## Contents\n\n- [At a glance](#at-a-glance)\n- [Try it](#try-it)\n- [Getting started](#getting-started)\n  - [One-time setup](#one-time-setup)\n  - [React / Vite](#react--vite)\n  - [Next.js](#nextjs)\n  - [Plain JavaScript](#plain-javascript)\n- [The layer tree](#the-layer-tree)\n  - [Where names come from](#where-names-come-from)\n  - [Reading the tree without converting](#reading-the-tree-without-converting)\n- [Flat or nested](#flat-or-nested)\n- [API reference](#api-reference)\n  - [convertSvgToFabric](#convertsvgtofabricinput-options)\n  - [parseSvg](#parsesvginput-options)\n  - [ConvertOptions](#convertoptions)\n  - [ConversionResult](#conversionresult)\n  - [SvgNode / SvgDesign](#svgnode--svgdesign)\n  - [Metadata](#metadata)\n  - [Canvas helpers](#canvas-helpers)\n  - [Tree helpers](#tree-helpers)\n  - [Persistence helpers](#persistence-helpers)\n  - [Warnings](#warnings)\n- [Recipes](#recipes)\n- [Behaviour worth knowing](#behaviour-worth-knowing)\n- [Security](#security)\n- [Limitations](#limitations)\n- [Compatibility](#compatibility)\n- [Development](#development)\n- [Support](#support)\n\n---\n\n## At a glance\n\n```ts\nimport { convertSvgToFabric, addToFabric } from '@acetrumtech/svg-to-fabric';\n\nconst result = await convertSvgToFabric(file);\n\nawait addToFabric(canvas, result);   // objects onto your existing canvas\nresult.document.children;            // the layer tree, named and nested\nresult.fabricJson;                   // plain JSON you can store\nresult.warnings;                     // what could not be represented, and why\n```\n\n| | |\n|---|---|\n| **Input** | `File`, `Blob`, `string`, `ArrayBuffer`, `Uint8Array` |\n| **Output** | Fabric JSON + a normalized document tree + warnings |\n| **Runtime** | Browser. `parseSvg` also runs on a server. |\n| **Bundle** | ~26 kB, ~9 kB gzipped. `fabric` stays external. |\n| **Peer dep** | `fabric` ≥ 7 < 8 |\n\n---\n\n## Try it\n\n```bash\nnpm install && npm run demo\n```\n\nDrop an SVG — or click one of the built-in samples — and you get the canvas, the\nlayer tree, the warnings and the Fabric JSON side by side, with the options\nswitchable live. The demo aliases the package to `src/`, so it hot-reloads on\nlibrary edits.\n\nThe samples are chosen to show something specific: a Figma export whose names\nlive in `data-name`, an Illustrator export whose names live in `id`, an Inkscape\nexport using `inkscape:label`, an icon with no width/height to try `scale` on,\nand a hostile file carrying a script, an inline handler, a `javascript:` link, a\ntracking pixel and a `<foreignObject>` — all of which come back stripped, with\nthe artwork intact.\n\n---\n\n## Getting started\n\n### One-time setup\n\nFabric restores unknown properties on load but drops them on `toObject()`.\nWithout this call your layer names and metadata survive the import and then\nvanish the first time the editor saves — the panel works until the user\nreloads, which is the worst way to find out.\n\n```ts\nimport { FabricObject } from 'fabric';\nimport { registerAcetrumProperties } from '@acetrumtech/svg-to-fabric';\n\nregisterAcetrumProperties(FabricObject); // also registers `name`\n```\n\nCall it once, at editor start-up.\n\n### React / Vite\n\n```tsx\nimport { useRef } from 'react';\nimport type { Canvas } from 'fabric';\nimport { convertSvgToFabric, addToFabric } from '@acetrumtech/svg-to-fabric';\n\nexport function ImportSvgButton({ canvas }: { canvas: Canvas }) {\n  const input = useRef<HTMLInputElement>(null);\n\n  const onPick = async (event: React.ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0];\n    if (!file) return;\n    event.target.value = ''; // let the same file be picked twice\n\n    try {\n      const result = await convertSvgToFabric(file);\n      await addToFabric(canvas, result);\n\n      for (const warning of result.warnings) {\n        console.warn(`[${warning.code}]`, warning.message);\n      }\n    } catch (error) {\n      // Thrown only for input that should not be accepted at all —\n      // see \"Warnings\" for the difference.\n      alert(error instanceof Error ? error.message : String(error));\n    }\n  };\n\n  return (\n    <>\n      <button onClick={() => input.current?.click()}>Import SVG</button>\n      <input\n        ref={input}\n        type=\"file\"\n        accept=\".svg,image/svg+xml\"\n        hidden\n        onChange={onPick}\n      />\n    </>\n  );\n}\n```\n\n### Next.js\n\nImporting this package on the server is safe — `fabric` is loaded with a dynamic\n`import()`, so nothing browser-only is evaluated until you actually convert. The\nconversion itself needs a DOM, so keep the component that owns the canvas\nclient-side:\n\n```tsx\n'use client';\n\nimport { convertSvgToFabric, addToFabric } from '@acetrumtech/svg-to-fabric';\n// …\n```\n\nOr, if the editor component itself must not be server-rendered:\n\n```tsx\nimport dynamic from 'next/dynamic';\n\nconst Editor = dynamic(() => import('./Editor'), { ssr: false });\n```\n\n`parseSvg` has no such restriction — it never touches Fabric or a canvas, so a\nserver route can read an uploaded file's layer names before anything reaches the\nbrowser:\n\n```ts\n// app/api/inspect/route.ts\nimport { parseSvg } from '@acetrumtech/svg-to-fabric';\n\nexport async function POST(request: Request) {\n  const design = await parseSvg(await request.text());\n  return Response.json({ layers: design.children.length });\n}\n```\n\nNode has no `DOMParser`, so install `jsdom` and assign the globals once at\nstart-up if you take this route:\n\n```ts\nimport { JSDOM } from 'jsdom';\nconst dom = new JSDOM();\nglobalThis.DOMParser = dom.window.DOMParser;\nglobalThis.XMLSerializer = dom.window.XMLSerializer;\n```\n\n### Plain JavaScript\n\n```html\n<script type=\"module\">\n  import { Canvas, FabricObject } from 'fabric';\n  import {\n    convertSvgToFabric,\n    loadIntoFabric,\n    registerAcetrumProperties,\n  } from '@acetrumtech/svg-to-fabric';\n\n  registerAcetrumProperties(FabricObject);\n\n  const canvas = new Canvas('c');\n  const result = await convertSvgToFabric(await fetch('/logo.svg').then((r) => r.text()));\n  await loadIntoFabric(canvas, result);\n</script>\n```\n\n---\n\n## The layer tree\n\n`result.document` is the SVG's real structure — every `<g>` is a group the\nauthor made on purpose, which is more than a PSD or an `.ai` file can promise.\n\n```\nCard                 ← <g data-name=\"Card\">\n├─ Background        ← <rect>\n├─ Badge             ← <g data-name=\"Badge\">\n│  ├─ Dot            ← <circle>\n│  └─ Tick           ← <path>\n└─ Bars              ← <g data-name=\"Bars\">\n   ├─ Bar 1\n   ├─ Bar 2\n   └─ Bar 3\n```\n\nThe tree is in **paint order** — first entry drawn first, so furthest back.\nA layers panel conventionally shows the top layer first, so reverse it for\ndisplay.\n\n### Where names come from\n\nThe order is not arbitrary — it is what the three exporters that matter actually\nwrite:\n\n| Priority | Attribute | Who writes it | Why it has to win where it does |\n|---|---|---|---|\n| 1 | `data-name` | Figma | Figma mangles `id` into `Vector_3` |\n| 2 | `inkscape:label` | Inkscape | `id` is a generated `path1234` |\n| 3 | `id` | Illustrator | the layer name goes straight in |\n| 4 | `<title>` | any | accessibility text, often a sentence |\n| 5 | `aria-label` | any | last resort |\n| 6 | `Path 3`, `Group 2` | — | generated fallback |\n\nNames are stripped of control characters and capped at 256 characters — they\ncome from an untrusted file and end up in your DOM.\n\n### Reading the tree without converting\n\n`parseSvg` walks the DOM only. No Fabric import, no canvas, no measurement — so\nit is fast and it runs on a server. The trade is that every node reports zero\nbounds.\n\n```ts\nconst design = await parseSvg(svgString);\ndesign.children;  // same shape, bounds all zero\ndesign.width;     // resolved the same way\n```\n\n---\n\n## Flat or nested\n\n**Flat is the default**, matching what most editors expect from an import:\n\n```ts\nresult.fabricJson.objects; // [Rect, Circle, Path, …] in paint order\n```\n\nThe hierarchy is not lost. Every object carries its ancestor ids:\n\n```ts\nobject.acetrum.sourcePath; // ['layer-0', 'layer-0.1']\n```\n\n…so a host that only kept the Fabric JSON can still rebuild the tree. Or ask for\nreal Fabric groups:\n\n```ts\nawait convertSvgToFabric(file, { preserveGroups: true });\n// objects: [Group { objects: [Rect, Group { objects: [Circle, Path] }] }]\n```\n\nGrouping does not move anything. Fabric's parser has already pushed ancestor\ntransforms down onto the leaves, so `new Group(children)` measures what it is\ngiven and re-parents it in place — the canvas renders pixel-identically either\nway.\n\n---\n\n## API reference\n\n### `convertSvgToFabric(input, options?)`\n\n```ts\nfunction convertSvgToFabric(\n  input: string | ArrayBuffer | Uint8Array | Blob | File,\n  options?: ConvertOptions,\n): Promise<ConversionResult>;\n```\n\nConverts an SVG into Fabric JSON with its group hierarchy intact. Runs in the\nbrowser; needs a DOM.\n\n**Throws** for input that should not be accepted at all — over the byte limit,\nover the pixel limit, over the element limit, not valid XML, or not an `<svg>`\nroot. Everything else becomes a warning.\n\n### `parseSvg(input, options?)`\n\n```ts\nfunction parseSvg(\n  input: string | ArrayBuffer | Uint8Array | Blob | File,\n  options?: Pick<\n    ConvertOptions,\n    'maxFileBytes' | 'sanitize' | 'allowExternalResources' | 'fallbackSize' | 'maxDocumentPixels'\n  >,\n): Promise<SvgDesign>;\n```\n\nReads the layer structure without converting anything. Never imports Fabric,\nnever needs a canvas. All bounds are zero.\n\n### `ConvertOptions`\n\n| Option | Type | Default | What it does |\n|---|---|---|---|\n| `preserveGroups` | `boolean` | `false` | Emit Fabric `Group`s instead of a flat list |\n| `includeHidden` | `boolean` | `true` | Keep hidden elements, as `visible: false` |\n| `sanitize` | `boolean` | `true` | Strip scripts, handlers, `javascript:` URLs |\n| `allowExternalResources` | `boolean` | `false` | Permit `<image>`/`<use>` pointing at another origin |\n| `scale` | `number` | `1` | Scale the document at parse time |\n| `origin` | `{left,top}` | `{0,0}` | Offset everything onto an artboard that lives elsewhere |\n| `emitArtboard` | `boolean \\| {name,id,fill}` | `false` | Prepend a non-selectable page rect named `clip` |\n| `clipToDocument` | `boolean` | `true` | Canvas-level clip at the document edges |\n| `setObjectName` | `boolean` | `true` | Copy the layer name onto `object.name` |\n| `background` | `string` | — | Canvas background; omitted unless set |\n| `fabricVersion` | `string` | `'7.0.0'` | Written to the JSON `version` field |\n| `fallbackSize` | `{width,height}` | `300×150` | Used only when there is no size *and* no viewBox |\n| `crossOrigin` | `'anonymous' \\| 'use-credentials'` | — | Passed to Fabric when it loads an `<image>` |\n| `signal` | `AbortSignal` | — | Abort a conversion still loading images |\n| `reviver` | `(element, object) => void` | — | Called after each element is converted |\n| `onProgress` | `(progress) => void` | — | Monotonic `ratio`, never goes backwards |\n| `maxFileBytes` | `number` | 32 MB | Checked before decoding |\n| `maxDocumentPixels` | `number` | 100 M | Rejects absurd `scale` values |\n| `maxElements` | `number` | 50 000 | Rejects pathological files |\n\nA few of these deserve a sentence:\n\n**`scale`** is a parse-time option on purpose. Scaling objects afterwards\nmultiplies through every nested transform; rewriting the root's width against a\nfixed viewBox scales the artwork with the path data left exact. An icon authored\nat 24×24 is unusable as a 24px object on a 1080p artboard.\n\n**`origin`** exists because an SVG starts at (0, 0) and an editor's artboard\nusually does not — a workspace canvas with the page centred inside it puts the\nartboard at something like (402, −194). Pass that artboard's `left`/`top` and\nthe objects land on the page instead of beside it.\n\n**`emitArtboard`** matters for editors that model the page as a non-selectable\nrectangle at the bottom of the stack — conventionally named `clip` — and find it\nby name to drive zoom-to-fit, page resize and export. Without one, a loaded\ndocument has no page as far as the host is concerned.\n\n**`clipToDocument`** makes the JSON describe its own artboard, so it renders the\nsame whatever size canvas it lands on. Turn it off if your editor deliberately\nshows the area around the artboard, or adds its own objects to the same canvas —\na canvas clip applies to everything on it.\n\n### `ConversionResult`\n\n```ts\ninterface ConversionResult {\n  fabricJson: FabricJson;          // ready for loadFromJSON / enlivenObjects\n  document: SvgDesign;             // the normalized layer tree\n  assets: ConversionAsset[];       // images the file referenced\n  warnings: ConversionWarning[];   // what could not be represented\n}\n\ninterface ConversionAsset {\n  id: string;\n  url: string;      // the string the object's `src` points at\n  width: number;\n  height: number;\n  external: boolean; // leaves this origin, and so may taint the canvas\n}\n```\n\nNothing is re-encoded — an SVG's images are already `data:` URLs or URLs, so\n`assets` is a manifest rather than an extraction.\n\n### `SvgNode` / `SvgDesign`\n\n```ts\ninterface SvgNode {\n  id: string;            // deterministic: 'layer-0.1.2'\n  name: string;\n  type: 'group' | 'path' | 'shape' | 'text' | 'image' | 'unknown';\n  tagName: string;       // 'g', 'path', 'rect', 'text'…\n  sourceId?: string;     // the element's own id attribute\n  visible: boolean;\n  opacity: number;       // as authored — see below\n  bounds: { left: number; top: number; width: number; height: number };\n  children?: SvgNode[];\n  objectIndex?: number;\n  unsupported?: string[];\n}\n\ninterface SvgDesign {\n  id: string;\n  width: number;\n  height: number;\n  viewBox?: { x: number; y: number; width: number; height: number };\n  children: SvgNode[];\n  source?: { fileName?: string; fileSize?: number };\n}\n```\n\n`id` is derived from the node's position in the tree, never from a counter or a\nrandom source — re-importing the same file produces byte-identical JSON, so a\nhost that diffs two imports gets no spurious changes.\n\n`opacity` is the **authored** value, not the effective one. Fabric's parser\nmultiplies ancestor opacity into each object, so the Fabric objects carry the\nfolded-in number; the tree carries what the designer typed, which is what a\nlayers panel should show next to a group.\n\n`bounds` are in canvas coordinates, after the viewBox transform. A group's box\nis the union of its children's.\n\n### Metadata\n\nEverything this package adds lives under one namespaced key, exported as\n`ACETRUM_PROP` (`'acetrum'`).\n\n```ts\ninterface AcetrumObjectMeta {\n  sourceLayerId: string;\n  sourceLayerName: string;\n  sourceType: string;     // the SvgNode type\n  sourceTag: string;      // the SVG tag, lowercased\n  sourceId?: string;      // the element's own id attribute\n  sourcePath?: string[];  // ancestor group ids, outermost first\n  unsupported?: string[];\n}\n\ninterface AcetrumDocumentMeta {\n  schemaVersion: number;  // ACETRUM_SCHEMA_VERSION\n  source: 'svg';\n  generator: string;      // GENERATOR\n  homepage: string;       // HOMEPAGE\n  document: { width: number; height: number; fileName?: string };\n  flattenedGroups: boolean;\n}\n```\n\nObject metadata sits on each object; document metadata sits on\n`result.fabricJson.acetrum`.\n\n### Canvas helpers\n\n```ts\naddToFabric(canvas, result): Promise<unknown[]>\n```\nAdds the result's objects to a canvas that already has content, and returns the\nobjects it added. **This is the one an editor wants** — importing artwork should\nnot throw away the user's document. The canvas-level `clipPath` and background\nfrom the result are deliberately ignored, since they describe a whole page.\n\n```ts\nloadIntoFabric(canvas, result): Promise<void>\n```\nReplaces the canvas contents entirely, clip and background included. Right for a\nviewer or a demo, wrong for an importer.\n\n```ts\nregisterAcetrumProperties(FabricObject, extra?: string[]): void\n```\nTeaches the host's Fabric to keep the metadata through `toObject()`. Registers\n`'name'` too unless you pass a different `extra`.\n\n```ts\napplyOrigin(fabricJson, { left, top }): void\n```\nShifts a finished document. Only top-level objects, the document clip, and\n`absolutePositioned` clipPaths move — group children are stored relative to\ntheir group's centre, so shifting them too would double the offset.\n\n```ts\nbuildFabricJson(input): FabricJson\napplyObjectNames(objects): void\n```\nThe serializer, exported for hosts assembling their own documents.\n\n### Tree helpers\n\n```ts\nflattenTree(nodes: readonly SvgNode[]): SvgNode[]\nfindNode(nodes: readonly SvgNode[], id: string): SvgNode | undefined\n```\n\n### Persistence helpers\n\n```ts\nhasExternalAssets(fabricJson): boolean\ninlineImages(result, options?): Promise<{ result: ConversionResult; failed: string[] }>\nblobToDataUrl(blob): Promise<string>\n```\n\nAn SVG's `<image>` is usually a `data:` URL and needs nothing. When it is a URL,\nsaved JSON renders every path while the photo comes up empty the day that URL\nmoves — and, before that, taints the canvas so `toDataURL()` throws.\n\n`inlineImages` fetches each one and rewrites it as a `data:` URL. It makes\nnetwork requests, which is why conversion never does it for you. Options:\n`timeoutMs` (15 000), `maxBytes` (8 MB), `fetchImpl`. An image that cannot be\nfetched is left as it was and listed in `failed` — a document with one remote\nphoto still beats a rejected save.\n\n### Warnings\n\nConversion never throws for an element it cannot handle. It records a warning\nand carries on: a file with one unreadable `<filter>` should still import.\n\n```ts\ninterface ConversionWarning {\n  code: ConversionWarningCode;\n  message: string;\n  severity: 'info' | 'warning' | 'error';\n  layerId?: string;\n  layerName?: string;\n}\n```\n\n| Code | Means |\n|---|---|\n| `SCRIPT_REMOVED` | The file carried something that would have executed. **Surface this.** |\n| `EXTERNAL_REFERENCE` | A reference to another origin was dropped, or will taint the canvas |\n| `UNSUPPORTED_FEATURE` | Filters, masks, `mix-blend-mode` — reported, not applied |\n| `UNSUPPORTED_ELEMENT` | Fabric's parser could not turn an element into an object |\n| `ELEMENT_SKIPPED` | Deliberately left out, e.g. an image with no usable source |\n| `SIZE_ASSUMED` | No usable width/height or viewBox, so a size was assumed |\n| `PARSE_RECOVERED` | Malformed, but enough parsed to continue |\n\n---\n\n## Recipes\n\n### A layers panel\n\n```tsx\nfunction Layers({ nodes, canvas }: { nodes: readonly SvgNode[]; canvas: Canvas }) {\n  const select = (node: SvgNode) => {\n    const all = canvas.getObjects().flatMap(function walk(o): FabricObject[] {\n      const kids = (o as { _objects?: FabricObject[] })._objects;\n      return kids ? [o, ...kids.flatMap(walk)] : [o];\n    });\n\n    const exact = all.find((o) => o.acetrum?.sourceLayerId === node.id);\n    if (exact) {\n      canvas.setActiveObject(exact);\n    } else {\n      // A group row with no object of its own — which is what flattening means.\n      const kids = all.filter((o) => o.acetrum?.sourcePath?.includes(node.id));\n      if (kids.length) canvas.setActiveObject(new ActiveSelection(kids, { canvas }));\n    }\n    canvas.requestRenderAll();\n  };\n\n  // Reversed: the tree is in paint order, panels list the top layer first.\n  return (\n    <ul>\n      {[...nodes].reverse().map((node) => (\n        <li key={node.id}>\n          <button onClick={() => select(node)}>{node.name}</button>\n          {node.children && <Layers nodes={node.children} canvas={canvas} />}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n### Re-nest a flat list\n\n```ts\nfunction nest(objects: FabricObjectJson[]) {\n  const roots: Record<string, unknown[]> = { '': [] };\n\n  for (const object of objects) {\n    const path = object.acetrum?.sourcePath ?? [];\n    const key = path.join('/');\n    (roots[key] ??= []).push(object);\n  }\n  return roots;\n}\n```\n\n### Toggle a whole group's visibility\n\n```ts\nconst ids = new Set([node.id, ...flattenTree(node.children ?? []).map((n) => n.id)]);\n\nfor (const object of canvas.getObjects()) {\n  const meta = object.acetrum;\n  if (ids.has(meta?.sourceLayerId) || meta?.sourcePath?.includes(node.id)) {\n    object.set('visible', false);\n  }\n}\ncanvas.requestRenderAll();\n```\n\n### Import onto an artboard that is not at (0, 0)\n\n```ts\nconst artboard = canvas.getObjects().find((o) => o.name === 'clip');\n\nawait convertSvgToFabric(file, {\n  origin: { left: artboard.left, top: artboard.top },\n  clipToDocument: false, // the host already owns its artboard clip\n});\n```\n\n### Progress for a large file\n\n```ts\nawait convertSvgToFabric(file, {\n  onProgress: ({ phase, ratio, layerName }) => {\n    setLabel(`${phase}${layerName ? ` · ${layerName}` : ''}`);\n    setBar(ratio); // monotonic; safe to drive a progress bar directly\n  },\n});\n```\n\nPhases, in order: `parsing`, `sanitizing`, `building`, `converting`, `done`.\n\n### Save it\n\n```ts\nconst { result: safe, failed } = hasExternalAssets(result.fabricJson)\n  ? await inlineImages(result)\n  : { result, failed: [] };\n\nif (failed.length) console.warn('Could not inline:', failed);\nawait fetch('/api/documents', { method: 'POST', body: JSON.stringify(safe.fabricJson) });\n```\n\n---\n\n## Behaviour worth knowing\n\n**Objects come back on a centre origin.** Fabric's SVG parser sets\n`originX: 'center'`, `originY: 'center'`, so `left`/`top` are the object's\n*centre*, not its top-left corner. A host that assumes top-left will place every\nimported object half its own size off. This package leaves the origin alone\nrather than rewriting it, because rewriting it changes what `left` means for\nobjects your editor may already be positioning by hand.\n\nNested positions are therefore just the sum of the `left` values:\n\n```ts\nconst absoluteLeft = group.left + child.left; // no width/2 anywhere\n```\n\n**Hidden layers** are handled the way you would want: `display: none`,\n`visibility: hidden` and a hidden `<g>` all come through as `visible: false`,\nand `includeHidden: false` drops them — a hidden group taking its children with\nit.\n\n**`<defs>`, `<clipPath>`, `<mask>`, `<symbol>`, `<pattern>` and gradients are\nnot mistaken for layers.** They define things for later reference; walking into\nthem would invent layers the designer never made.\n\n**An empty `<g>` is not a layer.** A group that turned out to hold nothing\ndrawable is scaffolding from an exporter, and is dropped from the tree.\n\n**An `<image>` with no usable source is dropped from the canvas but kept in the\ntree**, marked `unsupported: ['image source unavailable']`. It would render\nnothing and never could, but the file did have an image there and the panel\nshould say so.\n\n---\n\n## Security\n\nAn SVG is markup, and markup that reaches a DOM runs. This is the one concern an\nSVG importer has that a PSD or `.ai` importer does not — those are binary\nformats whose bytes never become markup. A user uploading a logo to your editor\nis exactly the untrusted-input case.\n\nSanitization is on by default and removes:\n\n- `<script>`, `<foreignObject>`, `<iframe>`, `<embed>`, `<object>`, `<handler>`,\n  `<listener>`\n- every `on*` event handler attribute — the prefix test is complete, because SVG\n  defines no drawing attribute starting with `on`\n- `javascript:`, `vbscript:`, `livescript:` and `mocha:` URLs\n- `data:` URLs that are not images, since a non-image `data:` URL is a document\n  and a document can script\n- SMIL animation elements — `<animate attributeName=\"href\" values=\"javascript:…\">`\n  is a known way past a filter that only checked static attributes\n- `@import`, `expression()` and `url(javascript:…)` inside `<style>`\n- references to other origins, unless `allowExternalResources: true`\n\nParsing goes through `image/svg+xml`, never the lenient HTML parser: the HTML\nparser reinterprets tags, lowercases `viewBox` into `viewbox`, and can resurrect\nmarkup a sanitizer expected to be inert.\n\nAnything removed becomes a warning. A `SCRIPT_REMOVED` warning means the file\ncontained something that would have executed — surface it:\n\n```ts\nconst active = result.warnings.filter((w) => w.code === 'SCRIPT_REMOVED');\nif (active.length) showBanner('This file contained active content, which was removed.');\n```\n\nByte, pixel and element limits are enforced before the expensive work, so a\nzip-bomb-shaped SVG is rejected rather than parsed.\n\nThis is defence in depth, not a licence to skip a CSP.\n\n---\n\n## Limitations\n\n- **SVG filters and masks are reported, not applied.** Canvas has no equivalent.\n  The artwork under them still imports; you get an `UNSUPPORTED_FEATURE` warning\n  naming how many uses were found.\n- **`mix-blend-mode` is reported, not applied.**\n- **Group opacity is an approximation.** Fabric multiplies group alpha into each\n  child rather than compositing the group first, so overlapping children inside a\n  faded group look different from a browser.\n- **Fonts are matched by name.** An SVG references fonts, it does not embed them\n  — text renders with whatever the browser resolves.\n- **No worker mode.** Fabric's SVG parser needs a DOM. SVGs are small enough that\n  this has not been a problem.\n- **SMIL animation is removed, not played.** Fabric cannot animate SVG timelines,\n  and the elements are an injection vector.\n\n---\n\n## Compatibility\n\n| | |\n|---|---|\n| Fabric | ≥ 7.0.0 < 8.0.0 (peer dependency — your copy is used, never a second one) |\n| Node | ≥ 20, for `parseSvg` and tooling |\n| Browsers | Anything with `DOMParser`, `XMLSerializer` and ES2022 |\n| Module format | ESM only |\n| Types | Bundled, no `@types` package needed |\n\n---\n\n## Development\n\n```bash\nnpm install\nnpm run demo          # Vite playground at :5173\nnpm test              # vitest, jsdom\nnpm run typecheck\nnpm run build         # dist/ — ESM + .d.ts\n```\n\nSource layout:\n\n```\nsrc/\n  svg/\n    sanitize.ts   strip everything active, before anything parses\n    readSvg.ts    input normalization, parsing, size resolution\n    layerTree.ts  rebuild the hierarchy from Fabric's flat output\n    convert.ts    tree → Fabric JSON, flat and nested\n  fabric/         serialize, origin, load, persist, customProperties\n  types/          document, fabric, options\n  utils/          warnings, progress, ids, units\n```\n\n---\n\n## Support\n\n- Website — [acetrum.com](https://acetrum.com)\n- Email — [info.acetrum@gmail.com](mailto:info.acetrum@gmail.com)\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-c7c03b9d7540718c4b74686c81bf36d5"}