# ag-psd
ag-psd is a JavaScript/TypeScript library for reading and writing Adobe Photoshop PSD and PSB files, implemented according to the official Adobe PSD format specification. It supports both Node.js (via `node-canvas`) and browser environments, as well as Web Workers using `OffscreenCanvas`. The library parses PSD documents into a rich, structured JavaScript object tree—exposing layers, groups, text, adjustment layers, smart objects, vector masks, layer effects, image resources, and linked files—and can serialize that same object structure back into a valid PSD binary.
The library's core functionality revolves around four main functions: `readPsd`, `writePsd`, `writePsdUint8Array`, and `writePsdBuffer`. It also provides utilities for reading Adobe Brush (`ABR`), Adobe Swatch Exchange (`ASE`), and Custom Shapes (`CSH`) files. Image data can be accessed via HTML Canvas elements or raw `ImageData`/`PixelData` objects, with configurable options to skip sections not needed (thumbnails, composite image, linked files), use raw/uncompressed data, or generate thumbnails automatically on write.
---
## `readPsd(buffer, options?)` — Read a PSD file into a structured Psd object
Parses an `ArrayBuffer`, `Buffer`, or typed array view containing PSD binary data and returns a `Psd` object representing the full document tree, including all layers, image data, effects, and metadata. Options allow skipping expensive sections for performance.
```typescript
import * as fs from 'fs';
import 'ag-psd/initialize-canvas'; // required for canvas/image decoding in Node.js
import { readPsd, ColorMode } from 'ag-psd';
const buffer = fs.readFileSync('design.psd');
// Full read: document structure + all image data
const psd = readPsd(buffer);
console.log(`Size: ${psd.width}x${psd.height}`);
console.log(`Color mode: ${psd.colorMode === ColorMode.RGB ? 'RGB' : 'other'}`);
console.log(`Bits per channel: ${psd.bitsPerChannel}`);
console.log(`Layers: ${psd.children?.length}`);
// Save composite canvas image
const canvas = psd.canvas as any;
fs.writeFileSync('composite.png', canvas.toBuffer());
// Save first layer image
if (psd.children?.[0]?.canvas) {
fs.writeFileSync('layer-0.png', (psd.children[0].canvas as any).toBuffer());
}
// Lightweight read: skip all image data for fast metadata extraction
const meta = readPsd(buffer, {
skipLayerImageData: true,
skipCompositeImageData: true,
skipThumbnail: true,
skipLinkedFilesData: true,
});
console.log('Layer names:', meta.children?.map(l => l.name));
// Read using raw ImageData instead of canvas (avoids alpha premultiplication)
const raw = readPsd(buffer, { useImageData: true });
const layerPixels = raw.children?.[0]?.imageData;
// layerPixels.data is Uint8ClampedArray (8-bit), Uint16Array (16-bit), or Float32Array (32-bit)
```
---
## `writePsd(psd, options?)` — Write a Psd object to an ArrayBuffer
Serializes a `Psd` object into a PSD binary and returns it as an `ArrayBuffer`. Accepts write options to control thumbnail generation, image trimming, text layer invalidation, and output format (PSD or PSB).
```typescript
import * as fs from 'fs';
import 'ag-psd/initialize-canvas';
import { writePsd, writePsdBuffer, writePsdUint8Array } from 'ag-psd';
import { createCanvas } from 'canvas';
// Create a canvas for layer image data
const layerCanvas = createCanvas(300, 200);
const ctx = layerCanvas.getContext('2d');
ctx.fillStyle = '#3498db';
ctx.fillRect(0, 0, 300, 200);
ctx.fillStyle = '#e74c3c';
ctx.fillRect(50, 50, 100, 80);
const compositeCanvas = createCanvas(300, 200);
const cctx = compositeCanvas.getContext('2d');
cctx.drawImage(layerCanvas, 0, 0);
const psd = {
width: 300,
height: 200,
canvas: compositeCanvas, // composite/merged image (optional but recommended)
children: [
{
name: 'Background',
top: 0,
left: 0,
canvas: layerCanvas,
blendMode: 'normal' as const,
opacity: 1,
hidden: false,
},
],
};
// Write to ArrayBuffer (browser-compatible)
const arrayBuffer: ArrayBuffer = writePsd(psd, { generateThumbnail: true });
// Write to Node.js Buffer (server-side)
const nodeBuffer: Buffer = writePsdBuffer(psd, {
generateThumbnail: true,
trimImageData: true, // trim transparent pixels to save space
noBackground: false,
});
fs.writeFileSync('output.psd', nodeBuffer);
// Write to Uint8Array (avoids extra memory allocation)
const uint8: Uint8Array = writePsdUint8Array(psd);
// Write as PSB (Large Document Format, supports >30000px)
const psb = writePsdBuffer(psd, { psb: true });
fs.writeFileSync('output.psb', psb);
// Write with zip compression (smaller file, may be slower)
const compressed = writePsdBuffer(psd, { compress: true });
fs.writeFileSync('output-compressed.psd', compressed);
```
---
## `decodeLayerPixels(layer, useImageData?)` — Lazy-decode raw layer pixel data
When a PSD is read with `useRawData: true`, pixel data is stored as compressed `rawData` on each layer and not decoded. Call `decodeLayerPixels` on individual layers to decode only the layers you actually need, reducing memory and CPU usage.
```typescript
import * as fs from 'fs';
import 'ag-psd/initialize-canvas';
import { readPsd, decodeLayerPixels } from 'ag-psd';
const buffer = fs.readFileSync('large-file.psd');
// Read all layers as raw (compressed) data — very fast
const psd = readPsd(buffer, { useRawData: true });
// Selectively decode only the layers you need
for (const layer of psd.children ?? []) {
if (layer.name === 'Export Target') {
// Decode into canvas (default)
decodeLayerPixels(layer, false);
console.log('Canvas decoded:', layer.canvas);
// OR decode into ImageData (no premultiplication)
decodeLayerPixels(layer, true);
console.log('ImageData decoded:', layer.imageData?.width, layer.imageData?.height);
}
}
```
---
## Text Layers — Writing and updating `LayerTextData`
Text layers are created by setting the `text` property on a layer. Supports styled runs, paragraph styles, transforms, anti-aliasing, and orientation. When updating an existing text layer loaded from a PSD, pass `invalidateTextLayers: true` to force Photoshop to re-render text bitmaps on open.
```typescript
import { writePsdBuffer, readPsd } from 'ag-psd';
import * as fs from 'fs';
// --- Creating a new text layer ---
const psd = {
width: 400,
height: 300,
children: [
{
name: 'Styled Text',
text: {
text: 'Hello World\nSecond Line',
transform: [1, 0, 0, 1, 30, 80], // translate 30px right, 80px down
antiAlias: 'smooth' as const,
orientation: 'horizontal' as const,
style: {
font: { name: 'ArialMT' },
fontSize: 36,
fillColor: { r: 0, g: 0, b: 0 },
},
styleRuns: [
{
length: 5, // 'Hello'
style: { fillColor: { r: 220, g: 50, b: 50 }, fauxBold: true },
},
{
length: 6, // ' World'
style: { fillColor: { r: 50, g: 100, b: 220 }, underline: true },
},
{
length: 12, // '\nSecond Line'
style: { fillColor: { r: 30, g: 150, b: 30 }, fontSize: 24 },
},
],
paragraphStyle: {
justification: 'center' as const,
spaceBefore: 4,
spaceAfter: 4,
},
},
},
],
};
fs.writeFileSync('text.psd', writePsdBuffer(psd));
// --- Updating an existing text layer ---
const existing = readPsd(fs.readFileSync('text.psd'));
if (existing.children?.[0]?.text) {
existing.children[0].text.text = 'Updated Text';
existing.children[0].canvas = undefined; // remove stale bitmap
}
// invalidateTextLayers forces Photoshop to redraw text on open
fs.writeFileSync('text-updated.psd', writePsdBuffer(existing, { invalidateTextLayers: true }));
```
---
## Layer Effects — `LayerEffectsInfo`
Layer blending effects (drop shadow, inner glow, stroke, bevel, etc.) are stored in the `effects` property of any layer. Multiple instances of some effects (shadows, strokes, fills, gradient overlays) are supported for modern Photoshop compatibility.
```typescript
import { writePsdBuffer } from 'ag-psd';
import * as fs from 'fs';
const psd = {
width: 400,
height: 400,
children: [
{
name: 'Styled Layer',
top: 50, left: 50,
effects: {
dropShadow: [{
enabled: true,
color: { r: 0, g: 0, b: 0 },
opacity: 0.6,
angle: 135,
distance: { units: 'Pixels' as const, value: 8 },
size: { units: 'Pixels' as const, value: 6 },
choke: { units: 'Pixels' as const, value: 0 },
useGlobalLight: false,
}],
stroke: [{
enabled: true,
size: { units: 'Pixels' as const, value: 3 },
position: 'outside' as const,
fillType: 'color' as const,
blendMode: 'normal' as const,
opacity: 1,
color: { r: 255, g: 200, b: 0 },
}],
outerGlow: {
enabled: true,
color: { r: 255, g: 255, b: 150 },
opacity: 0.75,
size: { units: 'Pixels' as const, value: 12 },
blendMode: 'screen' as const,
},
bevel: {
enabled: true,
style: 'inner bevel' as const,
technique: 'smooth' as const,
size: { units: 'Pixels' as const, value: 5 },
angle: 120,
strength: 60,
highlightOpacity: 0.75,
shadowOpacity: 0.75,
},
},
},
],
};
fs.writeFileSync('effects.psd', writePsdBuffer(psd));
```
---
## Adjustment Layers — `AdjustmentLayer`
Adjustment layers are identified by the presence of an `adjustment` property. No `canvas`/`imageData` is needed. Use `adjustment.type` to distinguish between the many supported adjustment types.
```typescript
import { writePsdBuffer, readPsd } from 'ag-psd';
import * as fs from 'fs';
const psd = {
width: 400,
height: 300,
children: [
{
name: 'Hue/Saturation',
adjustment: {
type: 'hue/saturation' as const,
master: { a: 0, b: 0, c: 0, d: 0, hue: 20, saturation: -30, lightness: 10 },
},
},
{
name: 'Brightness/Contrast',
adjustment: {
type: 'brightness/contrast' as const,
brightness: 15,
contrast: 25,
useLegacy: false,
},
},
{
name: 'Curves',
adjustment: {
type: 'curves' as const,
rgb: [
{ input: 0, output: 0 },
{ input: 128, output: 148 },
{ input: 255, output: 255 },
],
},
},
{
name: 'Levels',
adjustment: {
type: 'levels' as const,
rgb: { shadowInput: 10, highlightInput: 240, midtoneInput: 1.2, shadowOutput: 0, highlightOutput: 255 },
},
},
],
};
fs.writeFileSync('adjustments.psd', writePsdBuffer(psd));
// Reading adjustment layers
const loaded = readPsd(fs.readFileSync('adjustments.psd'), { skipLayerImageData: true });
for (const layer of loaded.children ?? []) {
if (layer.adjustment) {
console.log(`Adjustment type: ${layer.adjustment.type}`);
}
}
```
---
## Smart Object Layers — `PlacedLayer` and `linkedFiles`
Smart objects are identified by the `placedLayer` property. The linked file data is stored in `psd.linkedFiles`. The `placedLayer.id` links to a `LinkedFile` entry by its `id` field.
```typescript
import { readPsd } from 'ag-psd';
import * as fs from 'fs';
const psd = readPsd(fs.readFileSync('with-smart-objects.psd'));
for (const layer of psd.children ?? []) {
if (layer.placedLayer) {
const placed = layer.placedLayer;
console.log(`Smart object: type=${placed.type}, id=${placed.id}`);
console.log(`Transform corners:`, placed.transform); // 8 numbers: x,y of 4 corners
// Find the linked file data
const linked = psd.linkedFiles?.find(f => f.id === placed.id);
if (linked) {
console.log(`Linked file name: ${linked.name}`);
if (linked.data) {
// embedded file data as Uint8Array
fs.writeFileSync(`smart-object-${linked.name}`, Buffer.from(linked.data));
}
}
}
}
```
---
## Vector Masks and Shapes — `LayerVectorMask` and `BezierPath`
Layers can carry a `vectorMask` defining Bezier paths for non-destructive clipping. Each path has an `open` flag, a boolean operation, and an array of `BezierKnot` control points. The `vectorFill` and `vectorStroke` properties control the fill and stroke appearance.
```typescript
import { readPsd } from 'ag-psd';
import * as fs from 'fs';
const psd = readPsd(fs.readFileSync('vectors.psd'), { skipLayerImageData: true });
for (const layer of psd.children ?? []) {
if (layer.vectorMask) {
console.log(`Layer "${layer.name}" has ${layer.vectorMask.paths.length} vector path(s)`);
for (const path of layer.vectorMask.paths) {
console.log(` Path: open=${path.open}, operation=${path.operation}, knots=${path.knots.length}`);
// Each knot: { linked, points: [x0,y0, x1,y1, x2,y2] } (control points)
}
}
if (layer.vectorFill) {
if (layer.vectorFill.type === 'color') {
console.log(` Fill color:`, layer.vectorFill.color);
}
}
if (layer.vectorStroke) {
console.log(` Stroke width:`, layer.vectorStroke.lineWidth);
}
}
```
---
## Layer Masks — `LayerMaskData`
Layer masks are accessed via `layer.mask` and `layer.realMask`. The mask canvas or imageData is structured the same way as layer image data, but represents the mask bitmap.
```typescript
import 'ag-psd/initialize-canvas';
import { readPsd } from 'ag-psd';
import * as fs from 'fs';
const psd = readPsd(fs.readFileSync('masked.psd'));
for (const layer of psd.children ?? []) {
if (layer.mask) {
const mask = layer.mask;
console.log(`Mask bounds: top=${mask.top}, left=${mask.left}, bottom=${mask.bottom}, right=${mask.right}`);
console.log(`Mask disabled: ${mask.disabled}`);
console.log(`From vector data: ${mask.fromVectorData}`);
if (mask.canvas) {
// mask canvas is a grayscale-painted HTMLCanvasElement
fs.writeFileSync(`mask-${layer.name}.png`, (mask.canvas as any).toBuffer());
}
}
}
```
---
## Image Resources — `ImageResources`
Global document settings such as guides, resolution, slices, layer comps, print info, and timeline data are stored on `psd.imageResources`. All fields are optional and may be omitted when writing.
```typescript
import { writePsdBuffer } from 'ag-psd';
import * as fs from 'fs';
const psd = {
width: 800,
height: 600,
children: [],
imageResources: {
resolutionInfo: {
horizontalResolution: 300,
horizontalResolutionUnit: 'PPI' as const,
widthUnit: 'Inches' as const,
verticalResolution: 300,
verticalResolutionUnit: 'PPI' as const,
heightUnit: 'Inches' as const,
},
gridAndGuidesInformation: {
grid: { horizontal: 64, vertical: 64 },
guides: [
{ location: 100, direction: 'horizontal' as const },
{ location: 200, direction: 'vertical' as const },
],
},
globalAngle: 120,
globalAltitude: 30,
backgroundColor: { r: 255, g: 255, b: 255 },
printInformation: {
printerManagesColors: false,
printerName: 'My Printer',
renderingIntent: 'relative colorimetric' as const,
blackPointCompensation: true,
},
},
};
fs.writeFileSync('with-resources.psd', writePsdBuffer(psd));
```
---
## Browser Usage — Reading and writing PSD in the browser
In the browser, no canvas initialization import is needed. Use `XMLHttpRequest` or `fetch` to load PSD binary data, pass it to `readPsd`, and manipulate or display canvases directly in the DOM.
```html
```
---
## Web Worker Usage — Reading PSD off the main thread
For large files, use a Web Worker with `OffscreenCanvas` to avoid blocking the main thread. Composite canvas is transferred as an `ImageBitmap`.
```javascript
// worker.js
importScripts('ag-psd/dist/bundle.js');
agPsd.initializeCanvas(
(width, height) => new OffscreenCanvas(width, height)
);
onmessage = ({ data: buffer }) => {
const psd = agPsd.readPsd(buffer, {
skipLayerImageData: true,
skipThumbnail: true,
});
const bmp = psd.canvas.transferToImageBitmap();
delete psd.canvas;
postMessage({ psd, image: bmp }, [bmp]);
};
// main.js
const worker = new Worker('worker.js');
worker.onmessage = ({ data: { psd, image } }) => {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext('bitmaprenderer').transferFromImageBitmap(image);
document.body.appendChild(canvas);
console.log('Layers:', psd.children?.length);
};
fetch('large.psd')
.then(r => r.arrayBuffer())
.then(buf => worker.postMessage(buf, [buf]));
```
---
## `readAbr(buffer)` — Read Adobe Brush (ABR) files
Parses an Adobe Brush preset file (`.abr`) and returns a structured `Abr` object containing brush definitions, sample bitmaps, and embedded patterns.
```typescript
import * as fs from 'fs';
import { readAbr } from 'ag-psd';
const buffer = fs.readFileSync('brushes.abr');
const abr = readAbr(buffer);
console.log(`Brushes: ${abr.brushes.length}`);
for (const brush of abr.brushes) {
console.log(` Shape type: ${brush.shape?.type}`);
if (brush.shape?.type === 'computed') {
console.log(` Size: ${brush.shape.size}, Hardness: ${brush.shape.hardness}`);
}
}
console.log(`Samples: ${abr.samples.length}`);
for (const sample of abr.samples) {
console.log(` Sample id=${sample.id}, bounds: ${sample.bounds.w}x${sample.bounds.h}`);
// sample.alpha is Uint8Array of alpha channel pixel data
}
console.log(`Patterns: ${abr.patterns.length}`);
```
---
## `readCsh(buffer)` — Read Custom Shapes (CSH) files
Parses a Photoshop Custom Shapes file (`.csh`) and returns a `Csh` object with an array of named vector shapes, each containing Bezier path data and bounding dimensions.
```typescript
import * as fs from 'fs';
import { readCsh } from 'ag-psd';
const buffer = fs.readFileSync('shapes.csh');
const csh = readCsh(buffer);
for (const shape of csh.shapes) {
console.log(`Shape: "${shape.name}" (id=${shape.id}), ${shape.width}x${shape.height}`);
console.log(` Paths: ${shape.paths.length}`);
for (const path of shape.paths) {
console.log(` Bezier path: open=${path.open}, knots=${path.knots.length}`);
}
}
```
---
## Modifying PSD files — Read, update, write round-trip
The recommended workflow for modifying a PSD is to read it with `useImageData: true` (to prevent alpha premultiplication corruption), apply changes, then write it back.
```typescript
import * as fs from 'fs';
import 'ag-psd/initialize-canvas';
import { readPsd, writePsdBuffer } from 'ag-psd';
const input = fs.readFileSync('original.psd');
// Use useImageData to preserve pixel accuracy during round-trip
const psd = readPsd(input, { useImageData: true, useRawThumbnail: true });
// Update layer name
if (psd.children?.[0]) {
psd.children[0].name = 'Renamed Layer';
psd.children[0].opacity = 0.8;
psd.children[0].hidden = false;
}
// Update image resources
if (psd.imageResources) {
psd.imageResources.globalAngle = 90;
}
// Add a new empty group
psd.children?.push({
name: 'New Group',
children: [], // empty array = empty group
blendMode: 'normal',
opacity: 1,
});
const output = writePsdBuffer(psd, {
generateThumbnail: true,
trimImageData: true,
});
fs.writeFileSync('modified.psd', output);
```
---
## Summary
ag-psd is best suited for server-side tooling and browser-based creative applications that need to programmatically generate, inspect, or transform Photoshop documents without launching Photoshop. Typical use cases include design automation pipelines that stamp text or swap assets in PSD templates, export tools that extract layer canvases to PNG/JPEG, CI/CD visual testing workflows that parse layer data for assertions, and headless renderers that read composite image data for preview generation. The library's ability to round-trip a PSD file (read → modify → write) without corrupting untouched layers makes it especially valuable for template-based personalization systems.
Integration patterns commonly combine ag-psd with Node.js `canvas` for server-side rasterization, with browser Canvas API for interactive PSD viewers, or with Web Workers for non-blocking processing of large files. For ESM environments the `dist-es/` build is available; for legacy bundlers and CDN usage the pre-built `dist/bundle.js` UMD bundle exposes everything under the `agPsd` global. The library has no native dependencies beyond `pako` (zlib) and `base64-js`, making it straightforward to deploy in serverless functions, Docker containers, or embedded in frontend bundles.