{"_id":"@acetrumtech/design-to-fabric","_rev":"4-a692c3b85830b958f007374413ff7f66","name":"@acetrumtech/design-to-fabric","dist-tags":{"latest":"0.1.3"},"versions":{"0.1.0":{"name":"@acetrumtech/design-to-fabric","version":"0.1.0","keywords":["psd","photoshop","ai","illustrator","fabric","fabricjs","canvas","converter","design","editor","pdf"],"author":{"url":"https://acetrum.com","name":"Acetrum"},"license":"MIT","_id":"@acetrumtech/design-to-fabric@0.1.0","maintainers":[{"name":"acetrumtech","email":"info.acetrum@gmail.com"}],"homepage":"https://acetrum.com","dist":{"shasum":"a6d623cc543101ba3ae633627e47a3c7de36066b","tarball":"https://registry.npmjs.org/@acetrumtech/design-to-fabric/-/design-to-fabric-0.1.0.tgz","fileCount":58,"integrity":"sha512-gK9s5/VubacG8NtNo/cF7PcMpBnU/P1ioKj+I+bjjPa4PZI5HE1gFU5cpOMr+3i10Qo3Gaj5h/V3BO8EFbrgzg==","signatures":[{"sig":"MEUCIEmhzOHQaDyX7Dd80ftbdgIrK9L5kRN9n84wAPNzmfBUAiEA02zHXTymMrHMnKRHyW/nThR8qBrarBb2hhioGZm2fyc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":203213},"type":"module","types":"./dist/core/index.d.ts","module":"./dist/core/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./ai":{"types":"./dist/ai/index.d.ts","import":"./dist/ai/index.js"},"./psd":{"types":"./dist/psd/index.d.ts","import":"./dist/psd/index.js"},"./worker":{"types":"./dist/psd/worker.d.ts","import":"./dist/psd/worker.js"},"./package.json":"./package.json"},"scripts":{"demo":"vite --config demo/vite.config.ts","test":"vitest run","build":"vite build && npm run build:types","prepack":"npm run build","typecheck":"tsc --noEmit","demo:build":"vite build --config demo/vite.config.ts","test:watch":"vitest","build:types":"tsc -p tsconfig.build.json","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"acetrumtech","email":"info.acetrum@gmail.com"},"_npmVersion":"11.6.2","description":"Turn design files into editable layers — convert Adobe Photoshop (PSD) and Illustrator (AI) files into Fabric.js JSON.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","dependencies":{"ag-psd":"^31.0.2","gl-matrix":"^3.4.4","pdfjs-dist":"^6.2.108","opentype.js":"^2.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^7.0.0","react":"^19.2.8","fabric":"^7.4.0","vitest":"^3.0.0","react-dom":"^19.2.8","typescript":"^5.7.0","@types/node":"^24.0.0","@types/react":"^19.2.18","@napi-rs/canvas":"^0.1.60","@types/react-dom":"^19.2.4","@csstools/css-tokenizer":"^3.0.4","@csstools/css-parser-algorithms":"^3.0.5"},"peerDependencies":{"fabric":">=7 <8"},"optionalDependencies":{"fontkit":"^2.0.4"},"peerDependenciesMeta":{"fabric":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/design-to-fabric_0.1.0_1786470796171_0.7056061823700928","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@acetrumtech/design-to-fabric","version":"0.1.1","keywords":["psd","photoshop","ai","illustrator","fabric","fabricjs","canvas","converter","design","editor","pdf"],"author":{"url":"https://acetrum.com","name":"Acetrum"},"license":"MIT","_id":"@acetrumtech/design-to-fabric@0.1.1","maintainers":[{"name":"acetrumtech","email":"info.acetrum@gmail.com"}],"homepage":"https://acetrum.com","dist":{"shasum":"383f86815e839570df5794845666e63a3b39ee23","tarball":"https://registry.npmjs.org/@acetrumtech/design-to-fabric/-/design-to-fabric-0.1.1.tgz","fileCount":58,"integrity":"sha512-AGybU8uZrKBYE343XcUNS1qQW+K1oIlYimFygMbpUxJm2sdUsy3BXa5O6O/8mzGbJCrBQeNc4b8sd853JE69+g==","signatures":[{"sig":"MEUCIQCzXxmI+J9Qm6Q+Y/KyyYgy9HoXNm1o4W8i+CdHnPl53QIgH2FZ2YMVcCFgCbZ5i7XFjBlSujxIVo+p0MFvHnVD+1s=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":206044},"type":"module","types":"./dist/core/index.d.ts","module":"./dist/core/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./ai":{"types":"./dist/ai/index.d.ts","import":"./dist/ai/index.js"},"./psd":{"types":"./dist/psd/index.d.ts","import":"./dist/psd/index.js"},"./worker":{"types":"./dist/psd/worker.d.ts","import":"./dist/psd/worker.js"},"./package.json":"./package.json"},"gitHead":"b96a670b6fb4b66f677d27463406cce607224922","scripts":{"demo":"vite --config demo/vite.config.ts","test":"vitest run","build":"vite build && npm run build:types","prepack":"npm run build","typecheck":"tsc --noEmit","demo:build":"vite build --config demo/vite.config.ts","test:watch":"vitest","build:types":"tsc -p tsconfig.build.json","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"acetrumtech","email":"info.acetrum@gmail.com"},"_npmVersion":"10.8.2","description":"Turn design files into editable layers — convert Adobe Photoshop (PSD) and Illustrator (AI) files into Fabric.js JSON.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.1","dependencies":{"ag-psd":"^31.0.2","gl-matrix":"^3.4.4","pdfjs-dist":"^6.2.108","opentype.js":"^2.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^7.0.0","react":"^19.2.8","fabric":"^7.4.0","vitest":"^3.0.0","react-dom":"^19.2.8","typescript":"^5.7.0","@types/node":"^24.0.0","@types/react":"^19.2.18","@napi-rs/canvas":"^0.1.60","@types/react-dom":"^19.2.4","@csstools/css-tokenizer":"^3.0.4","@csstools/css-parser-algorithms":"^3.0.5"},"peerDependencies":{"fabric":">=7 <8"},"optionalDependencies":{"fontkit":"^2.0.4"},"peerDependenciesMeta":{"fabric":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/design-to-fabric_0.1.1_1786690014497_0.5878473815421592","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@acetrumtech/design-to-fabric","version":"0.1.2","keywords":["psd","photoshop","ai","illustrator","fabric","fabricjs","canvas","converter","design","editor","pdf"],"author":{"url":"https://acetrum.com","name":"Acetrum"},"license":"MIT","_id":"@acetrumtech/design-to-fabric@0.1.2","maintainers":[{"name":"acetrumtech","email":"info.acetrum@gmail.com"}],"homepage":"https://acetrum.com","dist":{"shasum":"480d9b4bdb0ca4c143eb92af6c69018114329c4e","tarball":"https://registry.npmjs.org/@acetrumtech/design-to-fabric/-/design-to-fabric-0.1.2.tgz","fileCount":58,"integrity":"sha512-L1dGLmQqlul7NByeXkF9KxkTwxkAQluLVaq0Cj57+St0OPbSvau1/GwH7Y0gZPRlx16soN4kv1ZqNCg4s2ieqA==","signatures":[{"sig":"MEYCIQDKk3prXimLOfdPRiJ2Bomf72xrKjIyRwblTSv1o1PNnAIhAL95/TAQQgxeGgebQOEUHDaUjJ4Z8XSGGWwngaUlU0kK","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":207490},"type":"module","types":"./dist/core/index.d.ts","module":"./dist/core/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./ai":{"types":"./dist/ai/index.d.ts","import":"./dist/ai/index.js"},"./psd":{"types":"./dist/psd/index.d.ts","import":"./dist/psd/index.js"},"./worker":{"types":"./dist/psd/worker.d.ts","import":"./dist/psd/worker.js"},"./package.json":"./package.json"},"gitHead":"b96a670b6fb4b66f677d27463406cce607224922","scripts":{"demo":"vite --config demo/vite.config.ts","test":"vitest run","build":"vite build && npm run build:types","prepack":"npm run build","typecheck":"tsc --noEmit","demo:build":"vite build --config demo/vite.config.ts","test:watch":"vitest","build:types":"tsc -p tsconfig.build.json","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"acetrumtech","email":"info.acetrum@gmail.com"},"_npmVersion":"10.8.2","description":"Turn design files into editable layers — convert Adobe Photoshop (PSD) and Illustrator (AI) files into Fabric.js JSON.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.1","dependencies":{"ag-psd":"^31.0.2","gl-matrix":"^3.4.4","pdfjs-dist":"^6.2.108","opentype.js":"^2.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^7.0.0","react":"^19.2.8","fabric":"^7.4.0","vitest":"^3.0.0","react-dom":"^19.2.8","typescript":"^5.7.0","@types/node":"^24.0.0","@types/react":"^19.2.18","@napi-rs/canvas":"^0.1.60","@types/react-dom":"^19.2.4","@csstools/css-tokenizer":"^3.0.4","@csstools/css-parser-algorithms":"^3.0.5"},"peerDependencies":{"fabric":">=7 <8"},"optionalDependencies":{"fontkit":"^2.0.4"},"peerDependenciesMeta":{"fabric":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/design-to-fabric_0.1.2_1786690627604_0.5343292248198044","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@acetrumtech/design-to-fabric","version":"0.1.3","description":"Turn design files into editable layers — convert Adobe Photoshop (PSD) and Illustrator (AI) files into Fabric.js JSON.","homepage":"https://acetrum.com","repository":{"type":"git","url":"git+https://github.com/acetrumtech/design-to-fabric.git"},"bugs":{"url":"https://github.com/acetrumtech/design-to-fabric/issues"},"author":{"name":"Acetrum","url":"https://acetrum.com"},"type":"module","license":"MIT","sideEffects":false,"exports":{".":{"types":"./dist/core/index.d.ts","import":"./dist/core/index.js"},"./psd":{"types":"./dist/psd/index.d.ts","import":"./dist/psd/index.js"},"./ai":{"types":"./dist/ai/index.d.ts","import":"./dist/ai/index.js"},"./worker":{"types":"./dist/psd/worker.d.ts","import":"./dist/psd/worker.js"},"./package.json":"./package.json"},"types":"./dist/core/index.d.ts","module":"./dist/core/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"},"peerDependenciesMeta":{"fabric":{"optional":true}},"dependencies":{"ag-psd":"^31.0.2","gl-matrix":"^3.4.4","opentype.js":"^2.0.0","pdfjs-dist":"^6.2.108"},"devDependencies":{"@csstools/css-parser-algorithms":"^3.0.5","@csstools/css-tokenizer":"^3.0.4","@napi-rs/canvas":"^0.1.60","@types/node":"^24.0.0","@types/react":"^19.2.18","@types/react-dom":"^19.2.4","fabric":"^7.4.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"},"optionalDependencies":{"fontkit":"^2.0.4"},"keywords":["psd","photoshop","ai","illustrator","fabric","fabricjs","canvas","converter","design","editor","pdf"],"_id":"@acetrumtech/design-to-fabric@0.1.3","gitHead":"fa3e48339b0e94dad22f941d04dfd565a2c79921","_nodeVersion":"20.20.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-0nkxMvZVthDZxmtoBxGRv4EX60tYtBUByeAWQK+Iq1QWVJ6pTsBqWnzGdn81AAimQi0RBzw+CkRjm8zatdGZdg==","shasum":"72489e46c4cb9a296bcc649c648636993bd505e7","tarball":"https://registry.npmjs.org/@acetrumtech/design-to-fabric/-/design-to-fabric-0.1.3.tgz","fileCount":58,"unpackedSize":207972,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGTrK/4F4xhWazy5zsWnLBztWbsx23S8MwcbDf4icWm2AiEA65CJZGN/TWDBuXsG9PV908v7AJJI0vtHTjBu8Guk9as="}]},"_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/design-to-fabric_0.1.3_1786692199276_0.26966128828779334"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-11T17:53:15.983Z","modified":"2026-08-14T07:23:19.589Z","0.1.0":"2026-08-11T17:53:16.318Z","0.1.1":"2026-08-14T06:46:54.642Z","0.1.2":"2026-08-14T06:57:07.750Z","0.1.3":"2026-08-14T07:23:19.437Z"},"author":{"name":"Acetrum","url":"https://acetrum.com"},"license":"MIT","homepage":"https://acetrum.com","keywords":["psd","photoshop","ai","illustrator","fabric","fabricjs","canvas","converter","design","editor","pdf"],"description":"Turn design files into editable layers — convert Adobe Photoshop (PSD) and Illustrator (AI) files into Fabric.js JSON.","maintainers":[{"name":"acetrumtech","email":"info.acetrum@gmail.com"}],"readme":"# @acetrumtech/design-to-fabric\n\n[![npm](https://img.shields.io/npm/v/@acetrumtech/design-to-fabric.svg)](https://www.npmjs.com/package/@acetrumtech/design-to-fabric)\n[![license](https://img.shields.io/npm/l/@acetrumtech/design-to-fabric.svg)](LICENSE)\n[![node](https://img.shields.io/node/v/@acetrumtech/design-to-fabric.svg)](https://nodejs.org)\n\nTurn design files into editable layers — convert design files into Fabric.js JSON.\n\n```bash\nnpm install @acetrumtech/design-to-fabric fabric\n```\n\nOne package, one entry per format — see [Entry points](#entry-points). Everything after parsing\n— placement, masks, text, assets, artboards, warnings, workers — is shared, so a consumer\nimporting only `/psd` never pulls another format's parser.\n\nBy [Acetrum](https://acetrum.com).\n\nBrowser-first TypeScript library. Parses a PSD with [`ag-psd`](https://github.com/Agamnentzar/ag-psd)\nand an `.ai` file with [`pdf.js`](https://github.com/mozilla/pdf.js), normalises the result into\na stable document model, and emits official Fabric.js JSON plus a namespaced `acetrum` metadata\nobject.\n\nOn the two commercial templates it is tested against, the rendered result sits within\n**0.39%** and **0.78%** of the composite Photoshop saved inside the file.\n\n- [Install](#install)\n- [Quick start](#quick-start)\n- [Illustrator files](#illustrator-files)\n- [Integrating with an editor](#integrating-with-an-editor)\n- [API](#api)\n- [Options](#options)\n- [What it converts](#what-it-converts)\n- [Fonts](#fonts)\n- [Running in a Worker](#running-in-a-worker)\n- [Troubleshooting](#troubleshooting)\n- [Limitations](#limitations)\n- [Development](#development)\n\nWhy it does what it does — the Fabric and Photoshop behaviours it works around — is in\n[`docs/INTERNALS.md`](docs/INTERNALS.md).\n\n---\n\n## Install\n\nThe package is published on npm as\n[`@acetrumtech/design-to-fabric`](https://www.npmjs.com/package/@acetrumtech/design-to-fabric).\n\n```bash\nnpm install @acetrumtech/design-to-fabric fabric\n```\n\n```bash\npnpm add @acetrumtech/design-to-fabric fabric\n```\n\n```bash\nyarn add @acetrumtech/design-to-fabric fabric\n```\n\n`fabric` is listed separately on purpose — it is a **peer** dependency, so npm 7+ installs it\nautomatically but pnpm and yarn do not. Installing it explicitly works correctly on all three.\n\nNothing extra is needed for the scope: `@acetrumtech` is a **public** scope, so no login, token\nor registry configuration is involved. If an install fails with `404 Not Found` or `E401`, an\n`.npmrc` in the project or home directory is pointing `@acetrumtech` at a private registry —\ncheck with `npm config get @acetrumtech:registry`, which should print `undefined` or\n`https://registry.npmjs.org/`.\n\n### Verify the install\n\n```bash\nnpm ls @acetrumtech/design-to-fabric\n```\n\nThe package is **ESM-only** (`\"type\": \"module\"`) and ships its own TypeScript types — no\n`@types/*` package to install. Import it and TypeScript resolves the declarations through the\n`exports` map:\n\n```ts\nimport { convertPsdToFabric } from '@acetrumtech/design-to-fabric/psd';\n```\n\nFor that subpath import to typecheck, TypeScript needs `moduleResolution` set to `\"bundler\"`,\n`\"node16\"` or `\"nodenext\"`. The legacy `\"node\"` setting ignores `exports` entirely and reports\nthe subpath as missing types.\n\n### Entry points\n\n| Import | Gives you |\n|---|---|\n| `@acetrumtech/design-to-fabric` | shared model, host integration, helpers |\n| `@acetrumtech/design-to-fabric/psd` | `convertPsdToFabric`, `parsePsd` |\n| `@acetrumtech/design-to-fabric/ai` | `convertAiToFabric`, `parseAi` |\n| `@acetrumtech/design-to-fabric/worker` | the PSD worker entry |\n\nImporting only `/psd` never pulls in another format's parser.\n\n### From source, while iterating\n\nOnly needed if you are changing this library itself. To test exactly what publishing would\ninstall:\n\n```bash\nnpm pack\n```\n\nThat produces `acetrum-design-to-fabric-0.1.0.tgz`, which the consuming project installs by\npath:\n\n```bash\nnpm install /path/to/acetrum-design-to-fabric-0.1.0.tgz\n```\n\nOr link the source, so there is no re-pack and re-install on every change:\n\n```bash\nnpm install file:/path/to/design-to-fabric\n```\n\nThen `npm run build` in this repo and restart the consuming dev server.\n\n### Dependencies\n\n`fabric` is a **peer dependency** (`>=7 <8`), not a dependency. Your editor's Fabric copy is\nthe one that gets used — a second copy in the bundle would break `instanceof` against your\nclasses and double the bundle size.\n\n`ag-psd`, `gl-matrix` and `opentype.js` install automatically. `fontkit` is optional and only\nloads if you ask for it.\n\n`pdfjs-dist` is used only by `/ai`, and only through a dynamic `import()`. A consumer that never\nconverts an `.ai` file never loads it — the ~1 MB parser stays out of the bundle's initial\nchunk.\n\nRequires Node 20+ to build. At runtime it targets browsers and workers.\n\n---\n\n## Quick start\n\n```ts\nimport { convertPsdToFabric } from '@acetrumtech/design-to-fabric/psd';\nimport { Canvas } from 'fabric';\n\nconst result = await convertPsdToFabric(file);\n\nconst canvas = new Canvas('c');\ncanvas.setDimensions({ width: result.document.width, height: result.document.height });\nawait canvas.loadFromJSON(result.fabricJson);\ncanvas.requestRenderAll();\n```\n\n`loadFromJSON` does **not** size the canvas — objects are in the PSD's own coordinates, so a\ncanvas left at its default shows a cropped corner of the design.\n\nThe core converter never imports `fabric`. `result.fabricJson` is a plain object; your app\nloads it with its own Fabric instance.\n\n### Keeping the PSD metadata\n\nFabric restores unknown properties onto object instances during `loadFromJSON`, but\n`toObject()` drops them — so `acetrum` would vanish the first time your editor saves the\ncanvas. Register it once at start-up:\n\n```ts\nimport { FabricObject } from 'fabric';\nimport { registerAcetrumProperties } from '@acetrumtech/design-to-fabric/psd';\n\nregisterAcetrumProperties(FabricObject);\n```\n\n### Saving the JSON ⚠️\n\nAssets default to **blob URLs**, and a `blob:` URL belongs to the document that created it.\nSaving JSON that still contains one produces a file that looks complete, loads without an\nerror, renders every text layer — and shows nothing where the images should be.\n\nIf the JSON goes to a database, an API or a file, make it self-contained:\n\n```ts\nimport { inlineAssets } from '@acetrumtech/design-to-fabric/psd';\n\nconst portable = await inlineAssets(result);\nawait save(portable.fabricJson);\n```\n\nOr convert that way from the start, which avoids the second pass:\n\n```ts\nconst result = await convertPsdToFabric(file, { assetFormat: 'data-url' });\n```\n\nOr pass an `assetStore` that uploads to your own storage and returns permanent URLs — the best\noption for large documents, since base64 costs about a third more bytes than binary.\n\n### Releasing memory\n\nRevoke the blob URLs when the canvas that used them is disposed:\n\n```ts\nimport { revokeAssetUrls } from '@acetrumtech/design-to-fabric/psd';\n\nrevokeAssetUrls(result);\n```\n\n---\n\n## Illustrator files\n\n`.ai` has been a PDF since Illustrator 9 (2000), so it is read with a PDF parser rather than a\nPostScript interpreter — which is what makes it possible in a browser at all.\n\n```ts\nimport { convertAiToFabric } from '@acetrumtech/design-to-fabric/ai';\n\nconst result = await convertAiToFabric(file);\nawait canvas.loadFromJSON(result.fabricJson);\n```\n\nThe result shape, warnings, asset handling and host-integration options are the same as for\nPSD, so everything under [Integrating with an editor](#integrating-with-an-editor) and\n[Options](#options) applies unchanged.\n\n### What it converts\n\n| Illustrator | Result |\n|---|---|\n| Vector artwork | Fabric `Path`, in canvas coordinates |\n| Point text | `IText`, one object per line, editable |\n| Embedded image | Fabric `Image`, deduplicated across placements |\n| Luminosity soft mask | Baked into the image's alpha |\n| Group opacity | `opacity` on each object the group contains |\n| Blend mode | `globalCompositeOperation` where canvas has an equivalent |\n| Page size | `result.document.width` / `.height`, in points |\n\n### No layer tree\n\nThe output is a **flat list of objects**, not a named hierarchy. Illustrator does record its\nlayer names in the PDF's optional-content groups, but on a real file the drawable content turns\nout not to be partitioned by them: toggling every group changed nothing in the render, and the\npage carried three marked-content sections against fifteen named groups. The real structure\nlives in the file's private Illustrator stream, which is proprietary.\n\n`parseAi` returns the page size and, when the file has them, the optional-content group names —\nuseful for showing the user what the file claims to contain, not for splitting it up.\n\n### Where pdf.js runs\n\nBy default the PDF is parsed on the calling thread. That needs no configuration and works in\nevery bundler, which is why it is the default — pdf.js otherwise refuses to start in a browser\nwithout a worker URL, and only your bundler knows that URL.\n\nFor a large file, hand it the worker instead:\n\n```ts\nawait convertAiToFabric(file, {\n  workerSrc: new URL('pdfjs-dist/legacy/build/pdf.worker.mjs', import.meta.url).toString(),\n});\n```\n\nSetting `GlobalWorkerOptions.workerSrc` yourself works too — an already-configured pdf.js is\nleft alone.\n\n### Outside a browser\n\nA render pass is what makes pdf.js hand over its embedded images. In a browser and in a worker\nthat happens through `OffscreenCanvas` with no configuration. Under Node there is no canvas, so\npass one:\n\n```ts\nimport { createCanvas } from '@napi-rs/canvas';\n\nclass NodeCanvasFactory {\n  create(width, height) {\n    const canvas = createCanvas(Math.max(1, width), Math.max(1, height));\n    return { canvas, context: canvas.getContext('2d') };\n  }\n  reset(target, width, height) { target.canvas.width = width; target.canvas.height = height; }\n  destroy(target) { target.canvas.width = 0; target.canvas.height = 0; }\n}\n\nawait convertAiToFabric(buffer, { canvasFactory: NodeCanvasFactory, rasterizer: nodeRasterizer });\n```\n\npdf.js wants a **constructor**, not an instance. It also paints through `Path2D` and `DOMMatrix`,\nwhich Node does not provide — assign them onto `globalThis` from your canvas library before\nconverting. Without a factory the conversion still succeeds; the images are skipped and a\nwarning says so.\n\nIn a browser and in a worker none of this applies: `OffscreenCanvas` is used automatically.\n\n### Limits\n\n`convertAiToFabric` throws with an explanation, rather than a parse error, for a file saved\nbefore Illustrator 9 or with **Create PDF Compatible File** turned off. Multi-page files convert\npage 1 and warn; pick another with `{ page: 2 }`.\n\nReported but **not applied**: clipping paths (artwork comes through uncropped), gradient and\nshading fills. Fonts are matched by name against what the host has, exactly as for PSD — the\nPDF's embedded font programs are not installed.\n\n---\n\n## Integrating with an editor\n\nEditors model their page in one of two ways. Pick the one that matches yours.\n\n### A. The page comes from the JSON\n\nThe import is a plain `canvas.loadFromJSON(json)`, and the document is expected to bring its\nown page — conventionally a non-selectable rectangle named `clip` at the bottom of the object\nstack, which the editor finds by name to drive zoom-to-fit, page resize and export.\n\nOne option covers it, and everything stays at (0, 0):\n\n```ts\nconst result = await convertPsdToFabric(file, {\n  emitArtboard: true,      // the `clip` rect, sized to the PSD, under every layer\n  assetFormat: 'data-url',\n});\n\nawait canvas.loadFromJSON(result.fabricJson);\ncanvas.renderAll();\nautoZoom();\n```\n\n`emitArtboard` prepends the page rectangle; `clipToDocument` (on by default) puts a matching\nclip in the JSON, which Fabric restores onto the canvas during `loadFromJSON`. So the host's\n`getWorkspace()` finds a page sized to the PSD, and nothing paints outside it.\n\nCustomise the names if yours differ:\n\n```ts\nemitArtboard: { name: 'clip', id: 'workspace', fill: 'rgba(255,255,255,1)' }\n```\n\n**Do not set `clipToDocument: false` here.** Without the clip in the JSON there is nothing to\nrestore, `canvas.clipPath` ends up undefined, and every layer that runs past the artboard\nshows.\n\nThis works even if the importer deletes the JSON's `clipPath` before loading and re-derives it\nfrom the page object — a common pattern, and the reason the page object matters more than the\nclip does.\n\n### B. The editor already has an artboard\n\nThe page exists before the import, often positioned by `canvas.centerObject(...)` so it sits at\nan arbitrary offset. Resize it to the PSD, place the document on it, and **add** the objects\nrather than loading them — `loadFromJSON` replaces the whole canvas, taking the workspace and\nits clip with it:\n\n```ts\nimport { util } from 'fabric';\nimport { convertPsdToFabric, applyOrigin } from '@acetrumtech/design-to-fabric/psd';\n\nconst result = await convertPsdToFabric(file, {\n  clipToDocument: false,   // the workspace already clips the canvas\n  assetFormat: 'data-url',\n});\n\n// 1. The artboard takes the PSD's dimensions.\nconst workspace = canvas.getObjects().find((o) => o.name === 'clip');\nworkspace.set({ width: result.document.width, height: result.document.height });\nworkspace.setCoords();\ncanvas.clipPath = workspace;\n\n// 2. Place the document on it — read the position *after* the resize.\nconst artboard = workspace.getBoundingRect();\napplyOrigin(result.fabricJson, { left: artboard.left, top: artboard.top });\n\n// 3. Add, don't load.\nconst objects = await util.enlivenObjects(result.fabricJson.objects);\nobjects.forEach((object) => canvas.add(object));\ncanvas.requestRenderAll();\n```\n\nUse `getBoundingRect()` rather than `workspace.left`: it gives the top-left whatever origin the\nhost has configured, so this works both with Fabric 7's `center` default and with an editor\nthat sets `FabricObject.ownDefaults.originX = 'left'`.\n\n`applyOrigin` is the same offset the `origin` option performs, applied after the fact — useful\nhere because the artboard's final position is only known once it has been resized.\n\n### Layers panels\n\nEvery object carries `name`, set to its PSD layer name, because that is what editors key their\nlayers panel on. If your serialiser takes an explicit property list, include it:\n\n```ts\ncanvas.toObject(['name', 'acetrum']);\n```\n\n---\n\n## API\n\n```ts\nconvertPsdToFabric(input, options?): Promise<ConversionResult>   // /psd\nparsePsd(input): Promise<PsdDesign>                              // /psd\nconvertAiToFabric(input, options?): Promise<ConversionResult>    // /ai\nparseAi(input, options?): Promise<PsdDesign>                     // /ai\n```\n\nBoth converters return the same `ConversionResult`, so a host handles either format with one\ncode path once it has picked an entry point by file extension.\n\n| | |\n|---|---|\n| `input` | `ArrayBuffer \\| Blob \\| File` |\n| `ConversionResult` | `{ fabricJson, document, assets, warnings }` |\n| `PsdDesign` | the normalised layer tree, without extracting any pixels |\n\nNothing throws for a layer it cannot handle — it becomes a hidden placeholder plus a warning,\nso the rest of the document keeps its z-order.\n\n### Helpers\n\n```ts\nregisterAcetrumProperties(fabricObjectClass): void  // keep acetrum across saves\nloadIntoFabric(canvas, result): Promise<void>       // load into a canvas you own\nrevokeAssetUrls(result): void                       // release blob URLs\ninlineAssets(result): Promise<ConversionResult>     // make the JSON self-contained\nhasSessionScopedAssets(fabricJson): boolean         // true ⇒ its images won't survive a save\napplyOrigin(fabricJson, { left, top }): void        // move a converted document\ncreatePsdWorkerClient(workerOrFactory): PsdWorkerClient\n```\n\n### Result shape\n\n```ts\ninterface ConversionResult {\n  fabricJson: FabricJson;      // load this into Fabric\n  document: PsdDesign;         // full layer hierarchy, always nested\n  assets: ConversionAsset[];   // { id, type, blob, url, width, height }\n  warnings: ConversionWarning[];\n}\n\ninterface ConversionWarning {\n  code: string;                // e.g. 'SMART_OBJECT_RASTERIZED'\n  message: string;\n  severity: 'info' | 'warning' | 'error';\n  layerId?: string;\n  layerName?: string;\n}\n```\n\n`document` keeps the full hierarchy whatever the Fabric output flattens, so a layers panel can\nshow groups even when the objects are flat.\n\n### Per-object metadata\n\n```ts\nobject.acetrum = {\n  sourceLayerId, sourceLayerName, sourceType,\n  blendMode, sourceOpacity, unsupported, rasterized,\n  sourceFont, fontEngine, fontSource, textApproximations, sourceText,\n  referenceAssetId,\n}\n```\n\nThe document itself carries `{ schemaVersion, source, generator, homepage, psd, flattenedGroups }`,\nso a stray JSON can always be traced back to the tool that made it.\n\n---\n\n## Options\n\nEvery field is optional.\n\n### Output shape\n\n| Option | Default | |\n|---|---|---|\n| `emitArtboard` | `false` | Add a page rectangle (`name: 'clip'`) under every layer |\n| `clipToDocument` | `true` | Canvas-level clip at the document's edges |\n| `cropToDocument` | `false` | Trim each layer to the artboard — needs no host clip |\n| `origin` | `{ left: 0, top: 0 }` | Place the document at a host artboard's position |\n| `setObjectName` | `true` | Set `name` to the PSD layer name |\n| `preserveGroups` | `false` | Emit Fabric `Group` objects instead of a flat list |\n| `background` | *(omitted)* | A PSD has no canvas colour; set it if your editor wants one |\n| `fabricVersion` | `'7.0.0'` | Written to the JSON's `version` field |\n\n### Text\n\n| Option | Default | |\n|---|---|---|\n| `preserveText` | `true` | Editable text objects; `false` rasterises them |\n| `textRasterReference` | `false` | Also keep a hidden raster of each text layer |\n| `fontResolver` | *(none)* | See [Fonts](#fonts) |\n| `fontEngine` | `'opentype'` | `'fontkit'` or `'auto'` for shaped measurement |\n\n### Assets\n\n| Option | Default | |\n|---|---|---|\n| `extractAssets` | `true` | Off ⇒ structure only, layers become hidden placeholders |\n| `assetFormat` | `'blob-url'` | Or `'data-url'` |\n| `assetMimeType` | `'image/png'` | Or `'image/jpeg'`, `'image/webp'` |\n| `assetStore` | object URLs | Push assets to your own storage instead |\n| `extractSmartObjectSources` | `false` | Also return each Smart Object's embedded original |\n| `maxRasterFallbackPixels` | `16_000_000` | Larger layers are downscaled, not dropped |\n\n### Everything else\n\n| Option | Default | |\n|---|---|---|\n| `includeHidden` | `true` | Hidden PSD layers arrive as `visible: false` |\n| `onProgress` | *(none)* | `{ phase, completed, total, ratio, layerName? }` |\n| `rasterizer` | OffscreenCanvas / `<canvas>` | Override to run outside a browser |\n| `maxFileBytes` | `512 MB` | Rejects oversized input |\n| `maxDocumentPixels` | `100_000_000` | Rejects oversized documents |\n| `worker` | `false` | See [Running in a Worker](#running-in-a-worker) — this flag only warns |\n\n---\n\n## What it converts\n\n| PSD | Result |\n|---|---|\n| Raster layer | Fabric `Image` at exact PSD bounds |\n| Text layer | `Textbox` (paragraph) or `IText` (point), editable |\n| Vector shape | Rasterised `Image` |\n| Group | Folded into the object list, or a Fabric `Group` with `preserveGroups` |\n| Layer mask | Baked into the layer's alpha |\n| Group mask | Baked into every descendant's alpha |\n| Clipping mask | Fabric `clipPath` — the clipped layer stays editable |\n| Opacity, visibility | `opacity`, `visible` |\n| Blend mode | `globalCompositeOperation` where canvas has an equivalent |\n| Colour Overlay | Baked into RGB |\n| Gradient Overlay | Baked into RGB (linear) |\n| Drop Shadow | Fabric's native `shadow` — stays editable |\n| Outer Glow | Fabric `shadow` with no offset, plus a warning |\n| Smart Object | Rasterised; placement and source recorded, source extractable |\n\nText carries per-character styles, so a headline with one coloured word survives as a single\neditable object:\n\n```json\n\"styles\": [{ \"start\": 0, \"end\": 3, \"style\": { \"fill\": \"#a4ca00\" } }]\n```\n\nMapped per run: `fill`, `fontSize`, `fontFamily`, `fontWeight`, `fontStyle`, `underline`,\n`linethrough`, and `baselineShift` → `deltaY`.\n\nPhotoshop's all-caps (`fontCaps`) is a *style*, not the text — the stored string stays mixed\ncase. It is folded into the string so the design looks right, with the original kept on\n`acetrum.sourceText`.\n\nGroups are flattened by default because a flat list is what most editors expect from an\nimport. `preserveGroups: true` emits nested Fabric `Group` objects instead; the rendered pixels\nare identical either way, and `result.document` carries the full hierarchy regardless.\n\n---\n\n## Fonts\n\nPSD text layers store only a PostScript name (`Arial-BoldMT`) — no font file, no family, no\nweight. With nothing else to go on, the name is parsed heuristically. That is a convention, not\na spec, so a family whose real name contains \"Black\" will read as weight 900.\n\nGive the converter a `FontResolver` and it stops guessing:\n\n```ts\nconst result = await convertPsdToFabric(file, {\n  fontResolver: {\n    async resolve(postScriptName) {\n      const buffer = await myFontService.load(postScriptName);\n      if (!buffer) return null;\n      return {\n        metrics: { family: postScriptName, unitsPerEm: 1000, ascent: 800, descent: -200 },\n        buffer,\n      };\n    },\n  },\n});\n```\n\nReturn a `buffer` and the font itself supplies the real family, weight, italic flag and\nmetrics. Return metrics only and those are used with a heuristic family. Return `null` and you\nget the heuristic, plus a `FONT_NOT_FOUND` warning.\n\nEvery text object records which path was taken:\n\n```ts\nobject.acetrum.fontSource; // 'font-file' | 'metrics-only' | 'name-heuristic'\nobject.acetrum.fontEngine; // 'opentype' | 'fontkit'\n```\n\nFonts are **never fetched automatically** — only what your resolver hands over is used, so no\ncopyrighted font is loaded or embedded on your users' behalf. Results are cached per PostScript\nname for the life of one conversion.\n\n**The host has to load the font too.** The converter records which family a layer needs; the\nbrowser still has to have it, or Fabric renders a fallback and the text looks wrong for reasons\nthat have nothing to do with the conversion.\n\n### Choosing a font engine\n\n| | `'opentype'` (default) | `'fontkit'` | `'auto'` |\n|---|---|---|---|\n| Must be installed | yes (a dependency) | yes (optional) | falls back if missing |\n| Family / weight / italic | yes | yes | — |\n| Metrics | yes | plus `capHeight`/`xHeight` | — |\n| Variable-font axes | no | yes | — |\n| Shaped measurement | no | yes | — |\n\nShaped measurement is what fontkit buys you: a box-text layer wider than the box Photoshop\nrecorded is reported as `text-overflows-box` — usually the sign that a substituted font is\nwider than the original.\n\nfontkit **cannot** change how glyphs are drawn. Fabric renders text through the canvas\n`fillText` API, which does its own shaping with whatever font the browser has loaded. fontkit\ninforms and measures here; it does not drive rendering.\n\nBoth parsers are imported dynamically and kept external, so neither loads until a resolver\nsupplies a font buffer.\n\n---\n\n## Running in a Worker\n\nA 14 MB, 84-layer PSD takes roughly 750 ms to convert — long enough to drop frames. The Worker\nis constructed by **you**, not by this package: a library cannot build a worker URL that\nreliably resolves inside someone else's bundler.\n\n```ts\nimport { createPsdWorkerClient } from '@acetrumtech/design-to-fabric/psd';\n\nconst client = createPsdWorkerClient(\n  () => new Worker(new URL('@acetrumtech/design-to-fabric/worker', import.meta.url), { type: 'module' }),\n);\n\nconst result = await client.convert(file, {\n  onProgress: ({ ratio, phase }) => setBar(ratio, phase),\n});\n\nclient.dispose();\n```\n\n`convert()` returns the same `ConversionResult` as the main-thread call — the test suite\nasserts the two produce byte-identical `fabricJson`.\n\n### What crosses the boundary\n\n`postMessage` only carries structured-cloneable values, so every function-shaped option needs a\ndecision rather than a silent drop:\n\n| Option | Handling |\n|---|---|\n| `onProgress` | Stays on the main thread, driven by the worker's progress messages |\n| `fontResolver` | **Proxied** — each lookup round-trips back to your resolver |\n| `rasterizer` | Ignored; the worker uses OffscreenCanvas. Reported as a warning |\n| `assetStore` | Ignored; the client mints the URLs. Reported as a warning |\n\nAn `ArrayBuffer` input is **transferred**, not copied, so your copy is detached afterwards.\nPass a `File` or `Blob` to avoid that — those clone by reference.\n\n### Bundler note\n\nThe worker dynamically imports its font parsers, so it needs a worker format that can\ncode-split. In Vite:\n\n```ts\nexport default defineConfig({ worker: { format: 'es' } });\n```\n\nVite defaults to IIFE workers, which cannot split — and the dev server works either way, so the\nfirst sign of trouble is a production build failing inside Rollup.\n\nNext.js and webpack 5 handle `new Worker(new URL(...))` without configuration. Keep the PSD and\nFabric work inside a Client Component.\n\n---\n\n## Troubleshooting\n\nEvery entry here is a failure that actually happened during integration.\n\n### Images are missing, text renders fine\n\nBlob URLs. `assetFormat` defaults to `'blob-url'`, and those die with the page that created\nthem. If the JSON is saved and loaded later, every image fails silently.\n\n```ts\nhasSessionScopedAssets(json); // true ⇒ this is it\n```\n\nFix with `inlineAssets(result)` before saving, or convert with `assetFormat: 'data-url'`, or\npass an `assetStore`. See [Saving the JSON](#saving-the-json-️).\n\n### Everything is offset, or only a corner shows\n\n`loadFromJSON` does not size the canvas:\n\n```ts\ncanvas.setDimensions({ width: result.document.width, height: result.document.height });\n```\n\nTo fit a smaller viewport, scale with `setZoom` **and** size the canvas to match. Scaling the\nelement with CSS alone leaves Fabric's pointer maths at the original scale, so every click lands\nin the wrong place.\n\n### Layers spill past the artboard\n\nPSD layers routinely extend past the canvas; Photoshop never draws the excess, Fabric has no\nsuch boundary. Three ways out, in order of preference:\n\n1. `emitArtboard: true` and let `clipToDocument` (default) supply the clip.\n2. Set `canvas.clipPath` to your own artboard object after import.\n3. `cropToDocument: true` — removes those pixels outright, so no canvas arrangement can reveal\n   them. Also shrinks the assets. The cost: a cropped layer no longer carries the part that was\n   outside, so dragging it will not bring more back.\n\nCheck which case you are in:\n\n```ts\nconsole.log(canvas.clipPath?.width, canvas.getObjects().find((o) => o.name === 'clip')?.width);\n```\n\n### The editor's zoom-to-fit / resize / export does nothing\n\nThose features find the page by name. Without `emitArtboard: true` there is no object named\n`clip`, so `getWorkspace()` returns undefined and each of them quietly no-ops.\n\n### `acetrum` disappears after the first save\n\n`registerAcetrumProperties(FabricObject)` was not called. See\n[Keeping the PSD metadata](#keeping-the-psd-metadata).\n\n### Text is the right size and place but the wrong typeface\n\nThe font is not loaded in the browser. Check `object.acetrum.fontSource` — `name-heuristic`\nmeans no resolver supplied it, and `fontFamily` is a best guess from the PostScript name.\n\n### \"Canvas not initialized\" in Node\n\nag-psd builds its results with the global `ImageData`, which Node lacks, and falls back to\ncreating a canvas:\n\n```ts\nimport { createCanvas } from '@napi-rs/canvas';\nimport { initializeCanvas } from 'ag-psd';\ninitializeCanvas(createCanvas);\n```\n\nBrowsers and workers have both, so this is a non-issue there.\n\n### Nothing changed at all\n\nCheck which converter is actually running. If the host has its own PSD pipeline, installing\nthis one changes nothing until the call site is swapped.\n\n---\n\n## Limitations\n\n**Not applied**, reported per layer instead: Inner Shadow, Inner Glow, Bevel, Satin, Stroke,\nPattern Overlay. These need per-pixel compositing inside the layer, or room outside its pixel\nrectangle, and a layer raster offers neither. Photoshop stores only the un-effected pixels plus\nthe effect parameters, so rasterising the result would mean *computing* it.\n\n**Group opacity is an approximation.** Photoshop composites a group and applies its opacity\nonce; Fabric multiplies the group's alpha into each child. Overlapping children inside a\nsemi-transparent group show their seams. This is a Fabric limitation — real Fabric groups do not\nfix it.\n\n**Paragraph-level runs are not mapped.** A layer whose paragraphs differ in alignment or\nindentation takes the first paragraph's settings.\n\n**Untested formats.** Both test PSDs are 8-bit RGB. PSB, 16/32-bit, CMYK, Grayscale and Indexed\nhave never been run, and neither have corrupt or truncated files.\n\n**`.ai` is flat, uncropped, and unshaded.** No layer tree, clipping paths reported rather than\napplied, gradients skipped. See [Illustrator files](#illustrator-files) for why.\n\n---\n\n## Development\n\n```bash\nnpm test\n```\n\n```bash\nnpm run typecheck && npm run build\n```\n\n```bash\nnpm run demo          # Vite + React demo at localhost:5173\n```\n\nThe demo is the only place the library runs in a real browser, against real `OffscreenCanvas`, a\nreal `Worker` and real font loading. Two bugs came straight out of standing it up.\n\n### Testing against real files\n\nDrop any `.psd` into `psd/` — `tests/realPsd.test.ts` runs **every** file it finds, converts it,\nrenders through Fabric, and compares against the composite Photoshop saved inside the PSD. `.ai`\nfiles in `ai/` work the same way through `tests/ai.test.ts`. With either folder empty the suite\nskips itself, so no binary needs committing.\n\nTests otherwise build PSDs in memory with `ag-psd`'s writer and run the whole conversion under\nNode via the `rasterizer` seam — no browser needed.\n\n---\n\n## Roadmap\n\n| Phase | Deliverable | |\n|---|---|---|\n| 1 | Parser, layer tree, image layers, Fabric JSON | ✅ |\n| 2 | Text layers, font resolver, opentype.js metrics | ✅ |\n| 3 | fontkit metrics, per-character styles | ✅ |\n| 4 | Groups, transforms, masks, clipping | ✅ |\n| 5 | Smart Objects, layer effects | ✅ |\n| 6 | Web Worker, progress events, memory | ✅ |\n| 7 | Demo, real-PSD test suite | ✅ |\n| 8 | npm publish | ✅ |\n| 9 | Semantic versioning, CI | |\n\nBeyond the PSD roadmap: `.ai` support ships in 0.1.0 (paths, text, images, soft masks). Clipping\npaths and gradients are the next two gaps.\n","readmeFilename":"README.md","repository":{"type":"git","url":"git+https://github.com/acetrumtech/design-to-fabric.git"},"bugs":{"url":"https://github.com/acetrumtech/design-to-fabric/issues"}}