支持 mac

This commit is contained in:
onvia
2023-07-24 11:13:08 +08:00
parent 413e79966a
commit 516a7f20c1
1940 changed files with 693119 additions and 1178 deletions

View File

@@ -0,0 +1,540 @@
import { BlnM, DescriptorUnitsValue, parseAngle, parsePercent, parseUnitsToNumber, readVersionAndDescriptor } from './descriptor';
import { BlendMode, PatternInfo } from './psd';
import {
checkSignature, createReader, readBytes, readDataRLE, readInt16, readInt32, readPascalString, readPattern,
readSignature, readUint16, readUint32, readUint8, skipBytes
} from './psdReader';
export interface Abr {
brushes: Brush[];
samples: SampleInfo[];
patterns: PatternInfo[];
}
export interface SampleInfo {
id: string;
bounds: { x: number; y: number; w: number; h: number; };
alpha: Uint8Array;
}
export interface BrushDynamics {
control: 'off' | 'fade' | 'pen pressure' | 'pen tilt' | 'stylus wheel' | 'initial direction' | 'direction' | 'initial rotation' | 'rotation';
steps: number; // for fade
jitter: number;
minimum: number;
}
const dynamicsControl = ['off', 'fade', 'pen pressure', 'pen tilt', 'stylus wheel', 'initial direction', 'direction', 'initial rotation', 'rotation'];
export interface BrushShape {
name?: string;
size: number;
angle: number;
roundness: number;
hardness?: number;
spacingOn: boolean;
spacing: number;
flipX: boolean;
flipY: boolean;
sampledData?: string;
}
export interface Brush {
name: string;
shape: BrushShape;
shapeDynamics?: {
sizeDynamics: BrushDynamics;
minimumDiameter: number;
tiltScale: number;
angleDynamics: BrushDynamics; // jitter 0-1 -> 0-360 deg ?
roundnessDynamics: BrushDynamics;
minimumRoundness: number;
flipX: boolean;
flipY: boolean;
brushProjection: boolean;
};
scatter?: {
bothAxes: boolean;
scatterDynamics: BrushDynamics;
countDynamics: BrushDynamics;
count: number;
};
texture?: {
id: string;
name: string;
invert: boolean;
scale: number;
brightness: number;
contrast: number;
blendMode: BlendMode;
depth: number;
depthMinimum: number;
depthDynamics: BrushDynamics;
};
dualBrush?: {
flip: boolean;
shape: BrushShape;
blendMode: BlendMode;
useScatter: boolean;
spacing: number;
count: number;
bothAxes: boolean;
countDynamics: BrushDynamics;
scatterDynamics: BrushDynamics;
};
colorDynamics?: {
foregroundBackground: BrushDynamics;
hue: number;
saturation: number;
brightness: number;
purity: number;
perTip: boolean;
};
transfer?: {
flowDynamics: BrushDynamics;
opacityDynamics: BrushDynamics;
wetnessDynamics: BrushDynamics;
mixDynamics: BrushDynamics;
};
brushPose?: {
overrideAngle: boolean;
overrideTiltX: boolean;
overrideTiltY: boolean;
overridePressure: boolean;
pressure: number;
tiltX: number;
tiltY: number;
angle: number;
};
noise: boolean;
wetEdges: boolean;
// TODO: build-up
// TODO: smoothing
protectTexture?: boolean;
spacing: number;
brushGroup?: undefined; // ?
interpretation?: boolean; // ?
useBrushSize: boolean; // ?
toolOptions?: {
brushPreset: boolean;
flow: number; // 0-100
smooth: number; // ?
mode: BlendMode;
opacity: number; // 0-100
smoothing: boolean;
smoothingValue: number;
smoothingRadiusMode: boolean;
smoothingCatchup: boolean;
smoothingCatchupAtEnd: boolean;
smoothingZoomCompensation: boolean;
pressureSmoothing: boolean;
usePressureOverridesSize: boolean;
usePressureOverridesOpacity: boolean;
useLegacy: boolean;
flowDynamics?: BrushDynamics;
opacityDynamics?: BrushDynamics;
sizeDynamics?: BrushDynamics;
};
}
// internal
interface PhryDescriptor {
hierarchy: ({} | {
'Nm ': string;
zuid: string;
})[];
}
interface DynamicsDescriptor {
bVTy: number;
fStp: number;
jitter: DescriptorUnitsValue;
'Mnm ': DescriptorUnitsValue;
}
interface BrushShapeDescriptor {
Dmtr: DescriptorUnitsValue;
Angl: DescriptorUnitsValue;
Rndn: DescriptorUnitsValue;
'Nm '?: string;
Spcn: DescriptorUnitsValue;
Intr: boolean;
Hrdn?: DescriptorUnitsValue;
flipX: boolean;
flipY: boolean;
sampledData?: string;
}
interface DescDescriptor {
Brsh: {
'Nm ': string;
Brsh: BrushShapeDescriptor;
useTipDynamics: boolean;
flipX: boolean;
flipY: boolean;
brushProjection: boolean;
minimumDiameter: DescriptorUnitsValue;
minimumRoundness: DescriptorUnitsValue;
tiltScale: DescriptorUnitsValue;
szVr: DynamicsDescriptor;
angleDynamics: DynamicsDescriptor;
roundnessDynamics: DynamicsDescriptor;
useScatter: boolean;
Spcn: DescriptorUnitsValue;
'Cnt ': number;
bothAxes: boolean;
countDynamics: DynamicsDescriptor;
scatterDynamics: DynamicsDescriptor;
dualBrush: { useDualBrush: false; } | {
useDualBrush: true;
Flip: boolean;
Brsh: BrushShapeDescriptor;
BlnM: string;
useScatter: boolean;
Spcn: DescriptorUnitsValue;
'Cnt ': number;
bothAxes: boolean;
countDynamics: DynamicsDescriptor;
scatterDynamics: DynamicsDescriptor;
};
brushGroup: { useBrushGroup: false; };
useTexture: boolean;
TxtC: boolean;
interpretation: boolean;
textureBlendMode: string;
textureDepth: DescriptorUnitsValue;
minimumDepth: DescriptorUnitsValue;
textureDepthDynamics: DynamicsDescriptor;
Txtr?: {
'Nm ': string;
Idnt: string;
};
textureScale: DescriptorUnitsValue;
InvT: boolean;
protectTexture: boolean;
textureBrightness: number;
textureContrast: number;
usePaintDynamics: boolean;
prVr?: DynamicsDescriptor;
opVr?: DynamicsDescriptor;
wtVr?: DynamicsDescriptor;
mxVr?: DynamicsDescriptor;
useColorDynamics: boolean;
clVr?: DynamicsDescriptor;
'H '?: DescriptorUnitsValue;
Strt?: DescriptorUnitsValue;
Brgh?: DescriptorUnitsValue;
purity?: DescriptorUnitsValue;
colorDynamicsPerTip?: true;
Wtdg: boolean;
Nose: boolean;
'Rpt ': boolean;
useBrushSize: boolean;
useBrushPose: boolean;
overridePoseAngle?: boolean;
overridePoseTiltX?: boolean;
overridePoseTiltY?: boolean;
overridePosePressure?: boolean;
brushPosePressure?: DescriptorUnitsValue;
brushPoseTiltX?: number;
brushPoseTiltY?: number;
brushPoseAngle?: number;
toolOptions?: {
brushPreset: boolean;
flow?: number;
Smoo?: number;
'Md ': string;
Opct?: number;
smoothing?: boolean;
smoothingValue?: number;
smoothingRadiusMode?: boolean;
smoothingCatchup?: boolean;
smoothingCatchupAtEnd?: boolean;
smoothingZoomCompensation?: boolean;
pressureSmoothing?: boolean;
usePressureOverridesSize?: boolean;
usePressureOverridesOpacity?: boolean;
useLegacy: boolean;
'Prs '?: number; // TODO: ???
MgcE?: boolean; // TODO: ???
ErsB?: number; // TODO: ???
prVr?: DynamicsDescriptor;
opVr?: DynamicsDescriptor;
szVr?: DynamicsDescriptor;
};
}[];
}
function parseDynamics(desc: DynamicsDescriptor): BrushDynamics {
return {
control: dynamicsControl[desc.bVTy] as any,
steps: desc.fStp,
jitter: parsePercent(desc.jitter),
minimum: parsePercent(desc['Mnm ']),
};
}
function parseBrushShape(desc: BrushShapeDescriptor): BrushShape {
const shape: BrushShape = {
size: parseUnitsToNumber(desc.Dmtr, 'Pixels'),
angle: parseAngle(desc.Angl),
roundness: parsePercent(desc.Rndn),
spacingOn: desc.Intr,
spacing: parsePercent(desc.Spcn),
flipX: desc.flipX,
flipY: desc.flipY,
};
if (desc['Nm ']) shape.name = desc['Nm '];
if (desc.Hrdn) shape.hardness = parsePercent(desc.Hrdn);
if (desc.sampledData) shape.sampledData = desc.sampledData;
return shape;
}
export function readAbr(buffer: ArrayBufferView, options: { logMissingFeatures?: boolean; } = {}): Abr {
const reader = createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength);
const version = readInt16(reader);
const samples: SampleInfo[] = [];
const brushes: Brush[] = [];
const patterns: PatternInfo[] = [];
if (version === 1 || version === 2) {
throw new Error(`Unsupported ABR version (${version})`); // TODO: ...
} else if (version === 6 || version === 7 || version === 9 || version === 10) {
const minorVersion = readInt16(reader);
if (minorVersion !== 1 && minorVersion !== 2) throw new Error('Unsupported ABR minor version');
while (reader.offset < reader.view.byteLength) {
checkSignature(reader, '8BIM');
const type = readSignature(reader) as 'samp' | 'desc' | 'patt' | 'phry';
let size = readUint32(reader);
const end = reader.offset + size;
switch (type) {
case 'samp': {
while (reader.offset < end) {
let brushLength = readUint32(reader);
while (brushLength & 0b11) brushLength++; // pad to 4 byte alignment
const brushEnd = reader.offset + brushLength;
const id = readPascalString(reader, 1);
// v1 - Skip the Int16 bounds rectangle and the unknown Int16.
// v2 - Skip the unknown bytes.
skipBytes(reader, minorVersion === 1 ? 10 : 264);
const y = readInt32(reader);
const x = readInt32(reader);
const h = readInt32(reader) - y;
const w = readInt32(reader) - x;
if (w <= 0 || h <= 0) throw new Error('Invalid bounds');
const depth = readInt16(reader);
const compression = readUint8(reader); // 0 - raw, 1 - RLE
const alpha = new Uint8Array(w * h);
if (depth === 8) {
if (compression === 0) {
alpha.set(readBytes(reader, alpha.byteLength));
} else if (compression === 1) {
readDataRLE(reader, { width: w, height: h, data: alpha }, w, h, 1, [0], false);
} else {
throw new Error('Invalid compression');
}
} else if (depth === 16) {
if (compression === 0) {
for (let i = 0; i < alpha.byteLength; i++) {
alpha[i] = readUint16(reader) >> 8; // convert to 8bit values
}
} else if (compression === 1) {
throw new Error('not implemented (16bit RLE)'); // TODO: ...
} else {
throw new Error('Invalid compression');
}
} else {
throw new Error('Invalid depth');
}
samples.push({ id, bounds: { x, y, w, h }, alpha });
reader.offset = brushEnd;
}
break;
}
case 'desc': {
const desc: DescDescriptor = readVersionAndDescriptor(reader);
// console.log(require('util').inspect(desc, false, 99, true));
for (const brush of desc.Brsh) {
const b: Brush = {
name: brush['Nm '],
shape: parseBrushShape(brush.Brsh),
spacing: parsePercent(brush.Spcn),
// TODO: brushGroup ???
wetEdges: brush.Wtdg,
noise: brush.Nose,
// TODO: TxtC ??? smoothing / build-up ?
// TODO: 'Rpt ' ???
useBrushSize: brush.useBrushSize, // ???
};
if (brush.interpretation != null) b.interpretation = brush.interpretation;
if (brush.protectTexture != null) b.protectTexture = brush.protectTexture;
if (brush.useTipDynamics) {
b.shapeDynamics = {
tiltScale: parsePercent(brush.tiltScale),
sizeDynamics: parseDynamics(brush.szVr),
angleDynamics: parseDynamics(brush.angleDynamics),
roundnessDynamics: parseDynamics(brush.roundnessDynamics),
flipX: brush.flipX,
flipY: brush.flipY,
brushProjection: brush.brushProjection,
minimumDiameter: parsePercent(brush.minimumDiameter),
minimumRoundness: parsePercent(brush.minimumRoundness),
};
}
if (brush.useScatter) {
b.scatter = {
count: brush['Cnt '],
bothAxes: brush.bothAxes,
countDynamics: parseDynamics(brush.countDynamics),
scatterDynamics: parseDynamics(brush.scatterDynamics),
};
}
if (brush.useTexture && brush.Txtr) {
b.texture = {
id: brush.Txtr.Idnt,
name: brush.Txtr['Nm '],
blendMode: BlnM.decode(brush.textureBlendMode),
depth: parsePercent(brush.textureDepth),
depthMinimum: parsePercent(brush.minimumDepth),
depthDynamics: parseDynamics(brush.textureDepthDynamics),
scale: parsePercent(brush.textureScale),
invert: brush.InvT,
brightness: brush.textureBrightness,
contrast: brush.textureContrast,
};
}
const db = brush.dualBrush;
if (db && db.useDualBrush) {
b.dualBrush = {
flip: db.Flip,
shape: parseBrushShape(db.Brsh),
blendMode: BlnM.decode(db.BlnM),
useScatter: db.useScatter,
spacing: parsePercent(db.Spcn),
count: db['Cnt '],
bothAxes: db.bothAxes,
countDynamics: parseDynamics(db.countDynamics),
scatterDynamics: parseDynamics(db.scatterDynamics),
};
}
if (brush.useColorDynamics) {
b.colorDynamics = {
foregroundBackground: parseDynamics(brush.clVr!),
hue: parsePercent(brush['H ']!),
saturation: parsePercent(brush.Strt!),
brightness: parsePercent(brush.Brgh!),
purity: parsePercent(brush.purity!),
perTip: brush.colorDynamicsPerTip!,
};
}
if (brush.usePaintDynamics) {
b.transfer = {
flowDynamics: parseDynamics(brush.prVr!),
opacityDynamics: parseDynamics(brush.opVr!),
wetnessDynamics: parseDynamics(brush.wtVr!),
mixDynamics: parseDynamics(brush.mxVr!),
};
}
if (brush.useBrushPose) {
b.brushPose = {
overrideAngle: brush.overridePoseAngle!,
overrideTiltX: brush.overridePoseTiltX!,
overrideTiltY: brush.overridePoseTiltY!,
overridePressure: brush.overridePosePressure!,
pressure: parsePercent(brush.brushPosePressure!),
tiltX: brush.brushPoseTiltX!,
tiltY: brush.brushPoseTiltY!,
angle: brush.brushPoseAngle!,
};
}
const to = brush.toolOptions;
if (to) {
b.toolOptions = {
brushPreset: to.brushPreset,
flow: to.flow ?? 100,
smooth: to.Smoo ?? 0,
mode: BlnM.decode(to['Md '] || 'BlnM.Nrml'), // sometimes mode is missing
opacity: to.Opct ?? 100,
smoothing: !!to.smoothing,
smoothingValue: to.smoothingValue || 0,
smoothingRadiusMode: !!to.smoothingRadiusMode,
smoothingCatchup: !!to.smoothingCatchup,
smoothingCatchupAtEnd: !!to.smoothingCatchupAtEnd,
smoothingZoomCompensation: !!to.smoothingZoomCompensation,
pressureSmoothing: !!to.pressureSmoothing,
usePressureOverridesSize: !!to.usePressureOverridesSize,
usePressureOverridesOpacity: !!to.usePressureOverridesOpacity,
useLegacy: !!to.useLegacy,
};
if (to.prVr) {
b.toolOptions.flowDynamics = parseDynamics(to.prVr);
}
if (to.opVr) {
b.toolOptions.opacityDynamics = parseDynamics(to.opVr);
}
if (to.szVr) {
b.toolOptions.sizeDynamics = parseDynamics(to.szVr);
}
}
brushes.push(b);
}
break;
}
case 'patt': {
if (reader.offset < end) { // TODO: check multiple patterns
patterns.push(readPattern(reader));
reader.offset = end;
}
break;
}
case 'phry': {
// TODO: what is this ?
const desc: PhryDescriptor = readVersionAndDescriptor(reader);
if (options.logMissingFeatures) {
if (desc.hierarchy?.length) {
console.log('unhandled phry section', desc);
}
}
break;
}
default:
throw new Error(`Invalid brush type: ${type}`);
}
// align to 4 bytes
while (size % 4) {
reader.offset++;
size++;
}
}
} else {
throw new Error(`Unsupported ABR version (${version})`);
}
return { samples, patterns, brushes };
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
import { readVectorMask } from './additionalInfo';
import { LayerVectorMask } from './psd';
import { readUint32, checkSignature, createReader, readPascalString, readUnicodeString } from './psdReader';
export interface Csh {
shapes: (LayerVectorMask & {
name: string;
id: string;
width: number;
height: number;
})[];
}
export function readCsh(buffer: ArrayBufferView): Csh {
const reader = createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength);
const csh: Csh = { shapes: [] };
checkSignature(reader, 'cush');
if (readUint32(reader) !== 2) throw new Error('Invalid version');
const count = readUint32(reader);
for (let i = 0; i < count; i++) {
const name = readUnicodeString(reader);
while (reader.offset % 4) reader.offset++; // pad to 4byte bounds
if (readUint32(reader) !== 1) throw new Error('Invalid shape version');
const size = readUint32(reader);
const end = reader.offset + size;
const id = readPascalString(reader, 1);
// this might not be correct ???
const y1 = readUint32(reader);
const x1 = readUint32(reader);
const y2 = readUint32(reader);
const x2 = readUint32(reader);
const width = x2 - x1;
const height = y2 - y1;
const mask: LayerVectorMask = { paths: [] };
readVectorMask(reader, mask, width, height, end - reader.offset);
csh.shapes.push({ name, id, width, height, ...mask });
reader.offset = end;
}
return csh;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,305 @@
import { LayerEffectsInfo, BevelStyle, LayerEffectShadow } from './psd';
import { toBlendMode, fromBlendMode } from './helpers';
import {
PsdReader, checkSignature, readSignature, skipBytes, readUint16, readUint8,
readUint32, readFixedPoint32, readColor
} from './psdReader';
import {
PsdWriter, writeSignature, writeUint16, writeZeros, writeFixedPoint32,
writeUint8, writeUint32, writeColor
} from './psdWriter';
const bevelStyles: BevelStyle[] = [
undefined as any, 'outer bevel', 'inner bevel', 'emboss', 'pillow emboss', 'stroke emboss'
];
function readBlendMode(reader: PsdReader) {
checkSignature(reader, '8BIM');
return toBlendMode[readSignature(reader)] || 'normal';
}
function writeBlendMode(writer: PsdWriter, mode: string | undefined) {
writeSignature(writer, '8BIM');
writeSignature(writer, fromBlendMode[mode!] || 'norm');
}
function readFixedPoint8(reader: PsdReader) {
return readUint8(reader) / 0xff;
}
function writeFixedPoint8(writer: PsdWriter, value: number) {
writeUint8(writer, Math.round(value * 0xff) | 0);
}
export function readEffects(reader: PsdReader) {
const version = readUint16(reader);
if (version !== 0) throw new Error(`Invalid effects layer version: ${version}`);
const effectsCount = readUint16(reader);
const effects: LayerEffectsInfo = <any>{};
for (let i = 0; i < effectsCount; i++) {
checkSignature(reader, '8BIM');
const type = readSignature(reader);
switch (type) {
case 'cmnS': { // common state (see See Effects layer, common state info)
const size = readUint32(reader);
const version = readUint32(reader);
const visible = !!readUint8(reader);
skipBytes(reader, 2);
if (size !== 7 || version !== 0 || !visible) throw new Error(`Invalid effects common state`);
break;
}
case 'dsdw': // drop shadow (see See Effects layer, drop shadow and inner shadow info)
case 'isdw': { // inner shadow (see See Effects layer, drop shadow and inner shadow info)
const blockSize = readUint32(reader);
const version = readUint32(reader);
if (blockSize !== 41 && blockSize !== 51) throw new Error(`Invalid shadow size: ${blockSize}`);
if (version !== 0 && version !== 2) throw new Error(`Invalid shadow version: ${version}`);
const size = readFixedPoint32(reader);
readFixedPoint32(reader); // intensity
const angle = readFixedPoint32(reader);
const distance = readFixedPoint32(reader);
const color = readColor(reader);
const blendMode = readBlendMode(reader);
const enabled = !!readUint8(reader);
const useGlobalLight = !!readUint8(reader);
const opacity = readFixedPoint8(reader);
if (blockSize >= 51) readColor(reader); // native color
const shadowInfo: LayerEffectShadow = {
size: { units: 'Pixels', value: size },
distance: { units: 'Pixels', value: distance },
angle, color, blendMode, enabled, useGlobalLight, opacity
};
if (type === 'dsdw') {
effects.dropShadow = [shadowInfo];
} else {
effects.innerShadow = [shadowInfo];
}
break;
}
case 'oglw': { // outer glow (see See Effects layer, outer glow info)
const blockSize = readUint32(reader);
const version = readUint32(reader);
if (blockSize !== 32 && blockSize !== 42) throw new Error(`Invalid outer glow size: ${blockSize}`);
if (version !== 0 && version !== 2) throw new Error(`Invalid outer glow version: ${version}`);
const size = readFixedPoint32(reader);
readFixedPoint32(reader); // intensity
const color = readColor(reader);
const blendMode = readBlendMode(reader);
const enabled = !!readUint8(reader);
const opacity = readFixedPoint8(reader);
if (blockSize >= 42) readColor(reader); // native color
effects.outerGlow = {
size: { units: 'Pixels', value: size },
color, blendMode, enabled, opacity
};
break;
}
case 'iglw': { // inner glow (see See Effects layer, inner glow info)
const blockSize = readUint32(reader);
const version = readUint32(reader);
if (blockSize !== 32 && blockSize !== 43) throw new Error(`Invalid inner glow size: ${blockSize}`);
if (version !== 0 && version !== 2) throw new Error(`Invalid inner glow version: ${version}`);
const size = readFixedPoint32(reader);
readFixedPoint32(reader); // intensity
const color = readColor(reader);
const blendMode = readBlendMode(reader);
const enabled = !!readUint8(reader);
const opacity = readFixedPoint8(reader);
if (blockSize >= 43) {
readUint8(reader); // inverted
readColor(reader); // native color
}
effects.innerGlow = {
size: { units: 'Pixels', value: size },
color, blendMode, enabled, opacity
};
break;
}
case 'bevl': { // bevel (see See Effects layer, bevel info)
const blockSize = readUint32(reader);
const version = readUint32(reader);
if (blockSize !== 58 && blockSize !== 78) throw new Error(`Invalid bevel size: ${blockSize}`);
if (version !== 0 && version !== 2) throw new Error(`Invalid bevel version: ${version}`);
const angle = readFixedPoint32(reader);
const strength = readFixedPoint32(reader);
const size = readFixedPoint32(reader);
const highlightBlendMode = readBlendMode(reader);
const shadowBlendMode = readBlendMode(reader);
const highlightColor = readColor(reader);
const shadowColor = readColor(reader);
const style = bevelStyles[readUint8(reader)] || 'inner bevel';
const highlightOpacity = readFixedPoint8(reader);
const shadowOpacity = readFixedPoint8(reader);
const enabled = !!readUint8(reader);
const useGlobalLight = !!readUint8(reader);
const direction = readUint8(reader) ? 'down' : 'up';
if (blockSize >= 78) {
readColor(reader); // real highlight color
readColor(reader); // real shadow color
}
effects.bevel = {
size: { units: 'Pixels', value: size },
angle, strength, highlightBlendMode, shadowBlendMode, highlightColor, shadowColor,
style, highlightOpacity, shadowOpacity, enabled, useGlobalLight, direction,
};
break;
}
case 'sofi': { // solid fill (Photoshop 7.0) (see See Effects layer, solid fill (added in Photoshop 7.0))
const size = readUint32(reader);
const version = readUint32(reader);
if (size !== 34) throw new Error(`Invalid effects solid fill info size: ${size}`);
if (version !== 2) throw new Error(`Invalid effects solid fill info version: ${version}`);
const blendMode = readBlendMode(reader);
const color = readColor(reader);
const opacity = readFixedPoint8(reader);
const enabled = !!readUint8(reader);
readColor(reader); // native color
effects.solidFill = [{ blendMode, color, opacity, enabled }];
break;
}
default:
throw new Error(`Invalid effect type: '${type}'`);
}
}
return effects;
}
function writeShadowInfo(writer: PsdWriter, shadow: LayerEffectShadow) {
writeUint32(writer, 51);
writeUint32(writer, 2);
writeFixedPoint32(writer, shadow.size && shadow.size.value || 0);
writeFixedPoint32(writer, 0); // intensity
writeFixedPoint32(writer, shadow.angle || 0);
writeFixedPoint32(writer, shadow.distance && shadow.distance.value || 0);
writeColor(writer, shadow.color);
writeBlendMode(writer, shadow.blendMode);
writeUint8(writer, shadow.enabled ? 1 : 0);
writeUint8(writer, shadow.useGlobalLight ? 1 : 0);
writeFixedPoint8(writer, shadow.opacity ?? 1);
writeColor(writer, shadow.color); // native color
}
export function writeEffects(writer: PsdWriter, effects: LayerEffectsInfo) {
const dropShadow = effects.dropShadow?.[0];
const innerShadow = effects.innerShadow?.[0];
const outerGlow = effects.outerGlow;
const innerGlow = effects.innerGlow;
const bevel = effects.bevel;
const solidFill = effects.solidFill?.[0];
let count = 1;
if (dropShadow) count++;
if (innerShadow) count++;
if (outerGlow) count++;
if (innerGlow) count++;
if (bevel) count++;
if (solidFill) count++;
writeUint16(writer, 0);
writeUint16(writer, count);
writeSignature(writer, '8BIM');
writeSignature(writer, 'cmnS');
writeUint32(writer, 7); // size
writeUint32(writer, 0); // version
writeUint8(writer, 1); // visible
writeZeros(writer, 2);
if (dropShadow) {
writeSignature(writer, '8BIM');
writeSignature(writer, 'dsdw');
writeShadowInfo(writer, dropShadow);
}
if (innerShadow) {
writeSignature(writer, '8BIM');
writeSignature(writer, 'isdw');
writeShadowInfo(writer, innerShadow);
}
if (outerGlow) {
writeSignature(writer, '8BIM');
writeSignature(writer, 'oglw');
writeUint32(writer, 42);
writeUint32(writer, 2);
writeFixedPoint32(writer, outerGlow.size?.value || 0);
writeFixedPoint32(writer, 0); // intensity
writeColor(writer, outerGlow.color);
writeBlendMode(writer, outerGlow.blendMode);
writeUint8(writer, outerGlow.enabled ? 1 : 0);
writeFixedPoint8(writer, outerGlow.opacity || 0);
writeColor(writer, outerGlow.color);
}
if (innerGlow) {
writeSignature(writer, '8BIM');
writeSignature(writer, 'iglw');
writeUint32(writer, 43);
writeUint32(writer, 2);
writeFixedPoint32(writer, innerGlow.size?.value || 0);
writeFixedPoint32(writer, 0); // intensity
writeColor(writer, innerGlow.color);
writeBlendMode(writer, innerGlow.blendMode);
writeUint8(writer, innerGlow.enabled ? 1 : 0);
writeFixedPoint8(writer, innerGlow.opacity || 0);
writeUint8(writer, 0); // inverted
writeColor(writer, innerGlow.color);
}
if (bevel) {
writeSignature(writer, '8BIM');
writeSignature(writer, 'bevl');
writeUint32(writer, 78);
writeUint32(writer, 2);
writeFixedPoint32(writer, bevel.angle || 0);
writeFixedPoint32(writer, bevel.strength || 0);
writeFixedPoint32(writer, bevel.size?.value || 0);
writeBlendMode(writer, bevel.highlightBlendMode);
writeBlendMode(writer, bevel.shadowBlendMode);
writeColor(writer, bevel.highlightColor);
writeColor(writer, bevel.shadowColor);
const style = bevelStyles.indexOf(bevel.style!);
writeUint8(writer, style <= 0 ? 1 : style);
writeFixedPoint8(writer, bevel.highlightOpacity || 0);
writeFixedPoint8(writer, bevel.shadowOpacity || 0);
writeUint8(writer, bevel.enabled ? 1 : 0);
writeUint8(writer, bevel.useGlobalLight ? 1 : 0);
writeUint8(writer, bevel.direction === 'down' ? 1 : 0);
writeColor(writer, bevel.highlightColor);
writeColor(writer, bevel.shadowColor);
}
if (solidFill) {
writeSignature(writer, '8BIM');
writeSignature(writer, 'sofi');
writeUint32(writer, 34);
writeUint32(writer, 2);
writeBlendMode(writer, solidFill.blendMode);
writeColor(writer, solidFill.color);
writeFixedPoint8(writer, solidFill.opacity || 0);
writeUint8(writer, solidFill.enabled ? 1 : 0);
writeColor(writer, solidFill.color);
}
}

View File

@@ -0,0 +1,359 @@
function isWhitespace(char: number) {
// ' ', '\n', '\r', '\t'
return char === 32 || char === 10 || char === 13 || char === 9;
}
function isNumber(char: number) {
// 0123456789.-
return (char >= 48 && char <= 57) || char === 46 || char === 45;
}
export function parseEngineData(data: number[] | Uint8Array) {
let index = 0;
function skipWhitespace() {
while (index < data.length && isWhitespace(data[index])) {
index++;
}
}
function getTextByte() {
let byte = data[index];
index++;
if (byte === 92) { // \
byte = data[index];
index++;
}
return byte;
}
function getText() {
let result = '';
if (data[index] === 41) { // )
index++;
return result;
}
// Strings start with utf-16 BOM
if (data[index] !== 0xFE || data[index + 1] !== 0xFF) {
throw new Error('Invalid utf-16 BOM');
}
index += 2;
// ), ( and \ characters are escaped in ascii manner, remove the escapes before interpreting
// the bytes as utf-16
while (index < data.length && data[index] !== 41) { // )
const high = getTextByte();
const low = getTextByte();
const char = (high << 8) | low;
result += String.fromCharCode(char);
}
index++;
return result;
}
let root: any = null;
const stack: any[] = [];
function pushContainer(value: any) {
if (!stack.length) {
stack.push(value);
root = value;
} else {
pushValue(value);
stack.push(value);
}
}
function pushValue(value: any) {
if (!stack.length) throw new Error('Invalid data');
const top = stack[stack.length - 1];
if (typeof top === 'string') {
stack[stack.length - 2][top] = value;
pop();
} else if (Array.isArray(top)) {
top.push(value);
} else {
throw new Error('Invalid data');
}
}
function pushProperty(name: string) {
if (!stack.length) pushContainer({});
const top = stack[stack.length - 1];
if (top && typeof top === 'string') {
if (name === 'nil') {
pushValue(null);
} else {
pushValue(`/${name}`);
}
} else if (top && typeof top === 'object') {
stack.push(name);
} else {
throw new Error('Invalid data');
}
}
function pop() {
if (!stack.length) throw new Error('Invalid data');
stack.pop();
}
skipWhitespace();
while (index < data.length) {
const i = index;
const char = data[i];
if (char === 60 && data[i + 1] === 60) { // <<
index += 2;
pushContainer({});
} else if (char === 62 && data[i + 1] === 62) { // >>
index += 2;
pop();
} else if (char === 47) { // /
index += 1;
const start = index;
while (index < data.length && !isWhitespace(data[index])) {
index++;
}
let name = '';
for (let i = start; i < index; i++) {
name += String.fromCharCode(data[i]);
}
pushProperty(name);
} else if (char === 40) { // (
index += 1;
pushValue(getText());
} else if (char === 91) { // [
index += 1;
pushContainer([]);
} else if (char === 93) { // ]
index += 1;
pop();
} else if (char === 110 && data[i + 1] === 117 && data[i + 2] === 108 && data[i + 3] === 108) { // null
index += 4;
pushValue(null);
} else if (char === 116 && data[i + 1] === 114 && data[i + 2] === 117 && data[i + 3] === 101) { // true
index += 4;
pushValue(true);
} else if (char === 102 && data[i + 1] === 97 && data[i + 2] === 108 && data[i + 3] === 115 && data[i + 4] === 101) { // false
index += 5;
pushValue(false);
} else if (isNumber(char)) {
let value = '';
while (index < data.length && isNumber(data[index])) {
value += String.fromCharCode(data[index]);
index++;
}
pushValue(parseFloat(value));
} else {
index += 1;
console.log(`Invalid token ${String.fromCharCode(char)} at ${index}`);
// ` near ${String.fromCharCode.apply(null, data.slice(index - 10, index + 20) as any)}` +
// `data [${Array.from(data.slice(index - 10, index + 20)).join(', ')}]`
}
skipWhitespace();
}
return root;
}
const floatKeys = [
'Axis', 'XY', 'Zone', 'WordSpacing', 'FirstLineIndent', 'GlyphSpacing', 'StartIndent', 'EndIndent', 'SpaceBefore',
'SpaceAfter', 'LetterSpacing', 'Values', 'GridSize', 'GridLeading', 'PointBase', 'BoxBounds', 'TransformPoint0', 'TransformPoint1',
'TransformPoint2', 'FontSize', 'Leading', 'HorizontalScale', 'VerticalScale', 'BaselineShift', 'Tsume',
'OutlineWidth', 'AutoLeading',
];
const intArrays = ['RunLengthArray'];
// TODO: handle /nil
export function serializeEngineData(data: any, condensed = false) {
let buffer = new Uint8Array(1024);
let offset = 0;
let indent = 0;
function write(value: number) {
if (offset >= buffer.length) {
const newBuffer = new Uint8Array(buffer.length * 2);
newBuffer.set(buffer);
buffer = newBuffer;
}
buffer[offset] = value;
offset++;
}
function writeString(value: string) {
for (let i = 0; i < value.length; i++) {
write(value.charCodeAt(i));
}
}
function writeIndent() {
if (condensed) {
writeString(' ');
} else {
for (let i = 0; i < indent; i++) {
writeString('\t');
}
}
}
function writeProperty(key: string, value: any) {
writeIndent();
writeString(`/${key}`);
writeValue(value, key, true);
if (!condensed) writeString('\n');
}
function serializeInt(value: number) {
return value.toString();
}
function serializeFloat(value: number) {
return value.toFixed(5)
.replace(/(\d)0+$/g, '$1')
.replace(/^0+\.([1-9])/g, '.$1')
.replace(/^-0+\.0(\d)/g, '-.0$1');
}
function serializeNumber(value: number, key?: string) {
const isFloat = (key && floatKeys.indexOf(key) !== -1) || (value | 0) !== value;
return isFloat ? serializeFloat(value) : serializeInt(value);
}
function getKeys(value: any) {
const keys = Object.keys(value);
if (keys.indexOf('98') !== -1)
keys.unshift(...keys.splice(keys.indexOf('99'), 1));
if (keys.indexOf('99') !== -1)
keys.unshift(...keys.splice(keys.indexOf('99'), 1));
return keys;
}
function writeStringByte(value: number) {
if (value === 40 || value === 41 || value === 92) { // ( ) \
write(92); // \
}
write(value);
}
function writeValue(value: any, key?: string, inProperty = false) {
function writePrefix() {
if (inProperty) {
writeString(' ');
} else {
writeIndent();
}
}
if (value === null) {
writePrefix();
writeString(condensed ? '/nil' : 'null');
} else if (typeof value === 'number') {
writePrefix();
writeString(serializeNumber(value, key));
} else if (typeof value === 'boolean') {
writePrefix();
writeString(value ? 'true' : 'false');
} else if (typeof value === 'string') {
writePrefix();
if ((key === '99' || key === '98') && value.charAt(0) === '/') {
writeString(value);
} else {
writeString('(');
write(0xfe);
write(0xff);
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
writeStringByte((code >> 8) & 0xff);
writeStringByte(code & 0xff);
}
writeString(')');
}
} else if (Array.isArray(value)) {
writePrefix();
if (value.every(x => typeof x === 'number')) {
writeString('[');
const intArray = intArrays.indexOf(key!) !== -1;
for (const x of value) {
writeString(' ');
writeString(intArray ? serializeNumber(x) : serializeFloat(x));
}
writeString(' ]');
} else {
writeString('[');
if (!condensed) writeString('\n');
for (const x of value) {
writeValue(x, key);
if (!condensed) writeString('\n');
}
writeIndent();
writeString(']');
}
} else if (typeof value === 'object') {
if (inProperty && !condensed) writeString('\n');
writeIndent();
writeString('<<');
if (!condensed) writeString('\n');
indent++;
for (const key of getKeys(value)) {
writeProperty(key, value[key]);
}
indent--;
writeIndent();
writeString('>>');
}
return undefined;
}
if (condensed) {
if (typeof data === 'object') {
for (const key of getKeys(data)) {
writeProperty(key, data[key]);
}
}
} else {
writeString('\n\n');
writeValue(data);
}
return buffer.slice(0, offset);
}

View File

@@ -0,0 +1,387 @@
import { fromByteArray } from 'base64-js';
import { deflate } from 'pako';
import { Layer, BlendMode, LayerColor } from './psd';
export const MOCK_HANDLERS = false;
export const RAW_IMAGE_DATA = false;
export const fromBlendMode: { [key: string]: string } = {};
export const toBlendMode: { [key: string]: BlendMode } = {
'pass': 'pass through',
'norm': 'normal',
'diss': 'dissolve',
'dark': 'darken',
'mul ': 'multiply',
'idiv': 'color burn',
'lbrn': 'linear burn',
'dkCl': 'darker color',
'lite': 'lighten',
'scrn': 'screen',
'div ': 'color dodge',
'lddg': 'linear dodge',
'lgCl': 'lighter color',
'over': 'overlay',
'sLit': 'soft light',
'hLit': 'hard light',
'vLit': 'vivid light',
'lLit': 'linear light',
'pLit': 'pin light',
'hMix': 'hard mix',
'diff': 'difference',
'smud': 'exclusion',
'fsub': 'subtract',
'fdiv': 'divide',
'hue ': 'hue',
'sat ': 'saturation',
'colr': 'color',
'lum ': 'luminosity',
};
Object.keys(toBlendMode).forEach(key => fromBlendMode[toBlendMode[key]] = key);
export const layerColors: LayerColor[] = [
'none', 'red', 'orange', 'yellow', 'green', 'blue', 'violet', 'gray'
];
export const largeAdditionalInfoKeys = [
// from documentation
'LMsk', 'Lr16', 'Lr32', 'Layr', 'Mt16', 'Mt32', 'Mtrn', 'Alph', 'FMsk', 'lnk2', 'FEid', 'FXid', 'PxSD',
// from guessing
'cinf',
];
export interface Dict {
[key: string]: string;
}
export function revMap(map: Dict) {
const result: Dict = {};
Object.keys(map).forEach(key => result[map[key]] = key);
return result;
}
export function createEnum<T>(prefix: string, def: string, map: Dict) {
const rev = revMap(map);
const decode = (val: string): T => {
const value = val.split('.')[1];
if (value && !rev[value]) throw new Error(`Unrecognized value for enum: '${val}'`);
return (rev[value] as any) || def;
};
const encode = (val: T | undefined): string => {
if (val && !map[val as any]) throw new Error(`Invalid value for enum: '${val}'`);
return `${prefix}.${map[val as any] || map[def]}`;
};
return { decode, encode };
}
export const enum ColorSpace {
RGB = 0,
HSB = 1,
CMYK = 2,
Lab = 7,
Grayscale = 8,
}
export const enum LayerMaskFlags {
PositionRelativeToLayer = 1,
LayerMaskDisabled = 2,
InvertLayerMaskWhenBlending = 4, // obsolete
LayerMaskFromRenderingOtherData = 8,
MaskHasParametersAppliedToIt = 16,
}
export const enum MaskParams {
UserMaskDensity = 1,
UserMaskFeather = 2,
VectorMaskDensity = 4,
VectorMaskFeather = 8,
}
export const enum ChannelID {
Color0 = 0, // red (rgb) / cyan (cmyk)
Color1 = 1, // green (rgb) / magenta (cmyk)
Color2 = 2, // blue (rgb) / yellow (cmyk)
Color3 = 3, // - (rgb) / black (cmyk)
Transparency = -1,
UserMask = -2,
RealUserMask = -3,
}
export const enum Compression {
RawData = 0,
RleCompressed = 1,
ZipWithoutPrediction = 2,
ZipWithPrediction = 3,
}
export interface ChannelData {
channelId: ChannelID;
compression: Compression;
buffer: Uint8Array | undefined;
length: number;
}
export interface Bounds {
top: number;
left: number;
right: number;
bottom: number;
}
export interface LayerChannelData {
layer: Layer;
channels: ChannelData[];
top: number;
left: number;
right: number;
bottom: number;
mask?: Bounds;
}
export type PixelArray = Uint8ClampedArray | Uint8Array;
export interface PixelData {
data: PixelArray;
width: number;
height: number;
}
export function offsetForChannel(channelId: ChannelID, cmyk: boolean) {
switch (channelId) {
case ChannelID.Color0: return 0;
case ChannelID.Color1: return 1;
case ChannelID.Color2: return 2;
case ChannelID.Color3: return cmyk ? 3 : channelId + 1;
case ChannelID.Transparency: return cmyk ? 4 : 3;
default: return channelId + 1;
}
}
export function clamp(value: number, min: number, max: number) {
return value < min ? min : (value > max ? max : value);
}
export function hasAlpha(data: PixelData) {
const size = data.width * data.height * 4;
for (let i = 3; i < size; i += 4) {
if (data.data[i] !== 255) {
return true;
}
}
return false;
}
export function resetImageData({ data }: PixelData) {
const buffer = new Uint32Array(data.buffer);
const size = buffer.length | 0;
for (let p = 0; p < size; p = (p + 1) | 0) {
buffer[p] = 0xff000000;
}
}
export function decodeBitmap(input: PixelArray, output: PixelArray, width: number, height: number) {
for (let y = 0, p = 0, o = 0; y < height; y++) {
for (let x = 0; x < width;) {
let b = input[o++];
for (let i = 0; i < 8 && x < width; i++, x++) {
const v = b & 0x80 ? 0 : 255;
b = b << 1;
output[p++] = v;
output[p++] = v;
output[p++] = v;
output[p++] = 255;
}
}
}
}
export function writeDataRaw(data: PixelData, offset: number, width: number, height: number) {
if (!width || !height)
return undefined;
const array = new Uint8Array(width * height);
for (let i = 0; i < array.length; i++) {
array[i] = data.data[i * 4 + offset];
}
return array;
}
export function writeDataRLE(buffer: Uint8Array, { data, width, height }: PixelData, offsets: number[], large: boolean) {
if (!width || !height) return undefined;
const stride = (4 * width) | 0;
let ol = 0;
let o = (offsets.length * (large ? 4 : 2) * height) | 0;
for (const offset of offsets) {
for (let y = 0, p = offset | 0; y < height; y++) {
const strideStart = (y * stride) | 0;
const strideEnd = (strideStart + stride) | 0;
const lastIndex = (strideEnd + offset - 4) | 0;
const lastIndex2 = (lastIndex - 4) | 0;
const startOffset = o;
for (p = (strideStart + offset) | 0; p < strideEnd; p = (p + 4) | 0) {
if (p < lastIndex2) {
let value1 = data[p];
p = (p + 4) | 0;
let value2 = data[p];
p = (p + 4) | 0;
let value3 = data[p];
if (value1 === value2 && value1 === value3) {
let count = 3;
while (count < 128 && p < lastIndex && data[(p + 4) | 0] === value1) {
count = (count + 1) | 0;
p = (p + 4) | 0;
}
buffer[o++] = 1 - count;
buffer[o++] = value1;
} else {
const countIndex = o;
let writeLast = true;
let count = 1;
buffer[o++] = 0;
buffer[o++] = value1;
while (p < lastIndex && count < 128) {
p = (p + 4) | 0;
value1 = value2;
value2 = value3;
value3 = data[p];
if (value1 === value2 && value1 === value3) {
p = (p - 12) | 0;
writeLast = false;
break;
} else {
count++;
buffer[o++] = value1;
}
}
if (writeLast) {
if (count < 127) {
buffer[o++] = value2;
buffer[o++] = value3;
count += 2;
} else if (count < 128) {
buffer[o++] = value2;
count++;
p = (p - 4) | 0;
} else {
p = (p - 8) | 0;
}
}
buffer[countIndex] = count - 1;
}
} else if (p === lastIndex) {
buffer[o++] = 0;
buffer[o++] = data[p];
} else { // p === lastIndex2
buffer[o++] = 1;
buffer[o++] = data[p];
p = (p + 4) | 0;
buffer[o++] = data[p];
}
}
const length = o - startOffset;
if (large) {
buffer[ol++] = (length >> 24) & 0xff;
buffer[ol++] = (length >> 16) & 0xff;
}
buffer[ol++] = (length >> 8) & 0xff;
buffer[ol++] = length & 0xff;
}
}
return buffer.slice(0, o);
}
export function writeDataZipWithoutPrediction({ data, width, height }: PixelData, offsets: number[]) {
const size = width * height;
const channel = new Uint8Array(size);
const buffers: Uint8Array[] = [];
let totalLength = 0;
for (const offset of offsets) {
for (let i = 0, o = offset; i < size; i++, o += 4) {
channel[i] = data[o];
}
const buffer = deflate(channel);
buffers.push(buffer);
totalLength += buffer.byteLength;
}
if (buffers.length > 0) {
const buffer = new Uint8Array(totalLength);
let offset = 0;
for (const b of buffers) {
buffer.set(b, offset);
offset += b.byteLength;
}
return buffer;
} else {
return buffers[0];
}
}
export let createCanvas: (width: number, height: number) => HTMLCanvasElement = () => {
throw new Error('Canvas not initialized, use initializeCanvas method to set up createCanvas method');
};
export let createCanvasFromData: (data: Uint8Array) => HTMLCanvasElement = () => {
throw new Error('Canvas not initialized, use initializeCanvas method to set up createCanvasFromData method');
};
let tempCanvas: HTMLCanvasElement | undefined = undefined;
export let createImageData: (width: number, height: number) => ImageData = (width, height) => {
if (!tempCanvas) tempCanvas = createCanvas(1, 1);
return tempCanvas.getContext('2d')!.createImageData(width, height);
};
if (typeof document !== 'undefined') {
createCanvas = (width, height) => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
createCanvasFromData = (data) => {
const image = new Image();
image.src = 'data:image/jpeg;base64,' + fromByteArray(data);
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext('2d')!.drawImage(image, 0, 0);
return canvas;
};
}
export function initializeCanvas(
createCanvasMethod: (width: number, height: number) => HTMLCanvasElement,
createCanvasFromDataMethod?: (data: Uint8Array) => HTMLCanvasElement,
createImageDataMethod?: (width: number, height: number) => ImageData
) {
createCanvas = createCanvasMethod;
createCanvasFromData = createCanvasFromDataMethod || createCanvasFromData;
createImageData = createImageDataMethod || createImageData;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
import { Psd, ReadOptions, WriteOptions } from './psd';
import { PsdWriter, writePsd as writePsdInternal, getWriterBuffer, createWriter, getWriterBufferNoCopy } from './psdWriter';
import { PsdReader, readPsd as readPsdInternal, createReader } from './psdReader';
export * from './abr';
export * from './csh';
export { initializeCanvas } from './helpers';
export * from './psd';
import { fromByteArray } from 'base64-js';
export { PsdReader, PsdWriter };
interface BufferLike {
buffer: ArrayBuffer;
byteOffset: number;
byteLength: number;
}
export const byteArrayToBase64 = fromByteArray;
export function readPsd(buffer: ArrayBuffer | BufferLike, options?: ReadOptions): Psd {
const reader = 'buffer' in buffer ?
createReader(buffer.buffer, buffer.byteOffset, buffer.byteLength) :
createReader(buffer);
return readPsdInternal(reader, options);
}
export function writePsd(psd: Psd, options?: WriteOptions): ArrayBuffer {
const writer = createWriter();
writePsdInternal(writer, psd, options);
return getWriterBuffer(writer);
}
export function writePsdUint8Array(psd: Psd, options?: WriteOptions): Uint8Array {
const writer = createWriter();
writePsdInternal(writer, psd, options);
return getWriterBufferNoCopy(writer);
}
export function writePsdBuffer(psd: Psd, options?: WriteOptions): Buffer {
if (typeof Buffer === 'undefined') {
throw new Error('Buffer not supported on this platform');
}
return Buffer.from(writePsdUint8Array(psd, options));
}

View File

@@ -0,0 +1,25 @@
import { createCanvas } from 'canvas';
import { initializeCanvas } from './index';
import { decodeJpeg } from './jpeg';
function createCanvasFromData(data: Uint8Array) {
const canvas = createCanvas(100, 100);
try {
const context = canvas.getContext('2d')!;
const imageData = decodeJpeg(data, (w, h) => context.createImageData(w, h));
canvas.width = imageData.width;
canvas.height = imageData.height;
context.putImageData(imageData, 0, 0);
} catch (e: any) {
console.error('JPEG decompression error', e.message);
}
return canvas;
}
initializeCanvas(createCanvas, createCanvasFromData);
export function initialize() {
initializeCanvas(createCanvas, createCanvasFromData);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,760 @@
import { Psd, Layer, LayerAdditionalInfo, ColorMode, SectionDividerType, WriteOptions, Color, GlobalLayerMaskInfo } from './psd';
import {
hasAlpha, createCanvas, writeDataRLE, PixelData, LayerChannelData, ChannelData,
offsetForChannel, createImageData, fromBlendMode, ChannelID, Compression, clamp,
LayerMaskFlags, MaskParams, ColorSpace, Bounds, largeAdditionalInfoKeys, RAW_IMAGE_DATA, writeDataZipWithoutPrediction
} from './helpers';
import { ExtendedWriteOptions, hasMultiEffects, infoHandlers } from './additionalInfo';
import { resourceHandlers } from './imageResources';
export interface PsdWriter {
offset: number;
buffer: ArrayBuffer;
view: DataView;
}
export function createWriter(size = 4096): PsdWriter {
const buffer = new ArrayBuffer(size);
const view = new DataView(buffer);
const offset = 0;
return { buffer, view, offset };
}
export function getWriterBuffer(writer: PsdWriter) {
return writer.buffer.slice(0, writer.offset);
}
export function getWriterBufferNoCopy(writer: PsdWriter) {
return new Uint8Array(writer.buffer, 0, writer.offset);
}
export function writeUint8(writer: PsdWriter, value: number) {
const offset = addSize(writer, 1);
writer.view.setUint8(offset, value);
}
export function writeInt16(writer: PsdWriter, value: number) {
const offset = addSize(writer, 2);
writer.view.setInt16(offset, value, false);
}
export function writeUint16(writer: PsdWriter, value: number) {
const offset = addSize(writer, 2);
writer.view.setUint16(offset, value, false);
}
export function writeInt32(writer: PsdWriter, value: number) {
const offset = addSize(writer, 4);
writer.view.setInt32(offset, value, false);
}
export function writeUint32(writer: PsdWriter, value: number) {
const offset = addSize(writer, 4);
writer.view.setUint32(offset, value, false);
}
export function writeFloat32(writer: PsdWriter, value: number) {
const offset = addSize(writer, 4);
writer.view.setFloat32(offset, value, false);
}
export function writeFloat64(writer: PsdWriter, value: number) {
const offset = addSize(writer, 8);
writer.view.setFloat64(offset, value, false);
}
// 32-bit fixed-point number 16.16
export function writeFixedPoint32(writer: PsdWriter, value: number) {
writeInt32(writer, value * (1 << 16));
}
// 32-bit fixed-point number 8.24
export function writeFixedPointPath32(writer: PsdWriter, value: number) {
writeInt32(writer, value * (1 << 24));
}
export function writeBytes(writer: PsdWriter, buffer: Uint8Array | undefined) {
if (buffer) {
ensureSize(writer, writer.offset + buffer.length);
const bytes = new Uint8Array(writer.buffer);
bytes.set(buffer, writer.offset);
writer.offset += buffer.length;
}
}
export function writeZeros(writer: PsdWriter, count: number) {
for (let i = 0; i < count; i++) {
writeUint8(writer, 0);
}
}
export function writeSignature(writer: PsdWriter, signature: string) {
if (signature.length !== 4) throw new Error(`Invalid signature: '${signature}'`);
for (let i = 0; i < 4; i++) {
writeUint8(writer, signature.charCodeAt(i));
}
}
export function writePascalString(writer: PsdWriter, text: string, padTo: number) {
let length = text.length;
writeUint8(writer, length);
for (let i = 0; i < length; i++) {
const code = text.charCodeAt(i);
writeUint8(writer, code < 128 ? code : '?'.charCodeAt(0));
}
while (++length % padTo) {
writeUint8(writer, 0);
}
}
export function writeUnicodeString(writer: PsdWriter, text: string) {
writeUint32(writer, text.length);
for (let i = 0; i < text.length; i++) {
writeUint16(writer, text.charCodeAt(i));
}
}
export function writeUnicodeStringWithPadding(writer: PsdWriter, text: string) {
writeUint32(writer, text.length + 1);
for (let i = 0; i < text.length; i++) {
writeUint16(writer, text.charCodeAt(i));
}
writeUint16(writer, 0);
}
function getLargestLayerSize(layers: Layer[] = []): number {
let max = 0;
for (const layer of layers) {
if (layer.canvas || layer.imageData) {
const { width, height } = getLayerDimentions(layer);
max = Math.max(max, 2 * height + 2 * width * height);
}
if (layer.children) {
max = Math.max(max, getLargestLayerSize(layer.children));
}
}
return max;
}
export function writeSection(writer: PsdWriter, round: number, func: () => void, writeTotalLength = false, large = false) {
if (large) writeUint32(writer, 0);
const offset = writer.offset;
writeUint32(writer, 0);
func();
let length = writer.offset - offset - 4;
let len = length;
while ((len % round) !== 0) {
writeUint8(writer, 0);
len++;
}
if (writeTotalLength) {
length = len;
}
writer.view.setUint32(offset, length, false);
}
export function writePsd(writer: PsdWriter, psd: Psd, options: WriteOptions = {}) {
if (!(+psd.width > 0 && +psd.height > 0))
throw new Error('Invalid document size');
if ((psd.width > 30000 || psd.height > 30000) && !options.psb)
throw new Error('Document size is too large (max is 30000x30000, use PSB format instead)');
let imageResources = psd.imageResources || {};
const opt: ExtendedWriteOptions = { ...options, layerIds: new Set(), layerToId: new Map() };
if (opt.generateThumbnail) {
imageResources = { ...imageResources, thumbnail: createThumbnail(psd) };
}
let imageData = psd.imageData;
if (!imageData && psd.canvas) {
imageData = psd.canvas.getContext('2d')!.getImageData(0, 0, psd.canvas.width, psd.canvas.height);
}
if (imageData && (psd.width !== imageData.width || psd.height !== imageData.height))
throw new Error('Document canvas must have the same size as document');
const globalAlpha = !!imageData && hasAlpha(imageData);
const maxBufferSize = Math.max(getLargestLayerSize(psd.children), 4 * 2 * psd.width * psd.height + 2 * psd.height);
const tempBuffer = new Uint8Array(maxBufferSize);
// header
writeSignature(writer, '8BPS');
writeUint16(writer, options.psb ? 2 : 1); // version
writeZeros(writer, 6);
writeUint16(writer, globalAlpha ? 4 : 3); // channels
writeUint32(writer, psd.height);
writeUint32(writer, psd.width);
writeUint16(writer, 8); // bits per channel
writeUint16(writer, ColorMode.RGB); // we only support saving RGB right now
// color mode data
writeSection(writer, 1, () => {
// TODO: implement
});
// image resources
writeSection(writer, 1, () => {
for (const handler of resourceHandlers) {
const has = handler.has(imageResources);
const count = has === false ? 0 : (has === true ? 1 : has);
for (let i = 0; i < count; i++) {
writeSignature(writer, '8BIM');
writeUint16(writer, handler.key);
writePascalString(writer, '', 2);
writeSection(writer, 2, () => handler.write(writer, imageResources, i));
}
}
});
// layer and mask info
writeSection(writer, 2, () => {
writeLayerInfo(tempBuffer, writer, psd, globalAlpha, opt);
writeGlobalLayerMaskInfo(writer, psd.globalLayerMaskInfo);
writeAdditionalLayerInfo(writer, psd, psd, opt);
}, undefined, !!opt.psb);
// image data
const channels = globalAlpha ? [0, 1, 2, 3] : [0, 1, 2];
const width = imageData ? imageData.width : psd.width;
const height = imageData ? imageData.height : psd.height;
const data: PixelData = { data: new Uint8Array(width * height * 4), width, height };
writeUint16(writer, Compression.RleCompressed); // Photoshop doesn't support zip compression of composite image data
if (RAW_IMAGE_DATA && (psd as any).imageDataRaw) {
console.log('writing raw image data');
writeBytes(writer, (psd as any).imageDataRaw);
} else {
if (imageData) data.data.set(new Uint8Array(imageData.data.buffer, imageData.data.byteOffset, imageData.data.byteLength));
// add weird white matte
if (globalAlpha) {
const size = data.width * data.height * 4;
const p = data.data;
for (let i = 0; i < size; i += 4) {
const pa = p[i + 3];
if (pa != 0 && pa != 255) {
const a = pa / 255;
const ra = 255 * (1 - a);
p[i + 0] = p[i + 0] * a + ra;
p[i + 1] = p[i + 1] * a + ra;
p[i + 2] = p[i + 2] * a + ra;
}
}
}
writeBytes(writer, writeDataRLE(tempBuffer, data, channels, !!options.psb));
}
}
function writeLayerInfo(tempBuffer: Uint8Array, writer: PsdWriter, psd: Psd, globalAlpha: boolean, options: ExtendedWriteOptions) {
writeSection(writer, 4, () => {
const layers: Layer[] = [];
addChildren(layers, psd.children);
if (!layers.length) layers.push({});
writeInt16(writer, globalAlpha ? -layers.length : layers.length);
const layersData = layers.map((l, i) => getChannels(tempBuffer, l, i === 0, options));
// layer records
for (const layerData of layersData) {
const { layer, top, left, bottom, right, channels } = layerData;
writeInt32(writer, top);
writeInt32(writer, left);
writeInt32(writer, bottom);
writeInt32(writer, right);
writeUint16(writer, channels.length);
for (const c of channels) {
writeInt16(writer, c.channelId);
if (options.psb) writeUint32(writer, 0);
writeUint32(writer, c.length);
}
writeSignature(writer, '8BIM');
writeSignature(writer, fromBlendMode[layer.blendMode!] || 'norm');
writeUint8(writer, Math.round(clamp(layer.opacity ?? 1, 0, 1) * 255));
writeUint8(writer, layer.clipping ? 1 : 0);
let flags = 0x08; // 1 for Photoshop 5.0 and later, tells if bit 4 has useful information
if (layer.transparencyProtected) flags |= 0x01;
if (layer.hidden) flags |= 0x02;
if (layer.vectorMask || (layer.sectionDivider && layer.sectionDivider.type !== SectionDividerType.Other)) {
flags |= 0x10; // pixel data irrelevant to appearance of document
}
if (layer.effects && hasMultiEffects(layer.effects)) { // TODO: this is not correct
flags |= 0x20; // just guessing this one, might be completely incorrect
}
// if ('_2' in layer) flags |= 0x20; // TEMP!!!
writeUint8(writer, flags);
writeUint8(writer, 0); // filler
writeSection(writer, 1, () => {
writeLayerMaskData(writer, layer, layerData);
writeLayerBlendingRanges(writer, psd);
writePascalString(writer, layer.name || '', 4);
writeAdditionalLayerInfo(writer, layer, psd, options);
});
}
// layer channel image data
for (const layerData of layersData) {
for (const channel of layerData.channels) {
writeUint16(writer, channel.compression);
if (channel.buffer) {
writeBytes(writer, channel.buffer);
}
}
}
}, true, options.psb);
}
function writeLayerMaskData(writer: PsdWriter, { mask }: Layer, layerData: LayerChannelData) {
writeSection(writer, 1, () => {
if (!mask) return;
const m = layerData.mask || {} as Partial<Bounds>;
writeInt32(writer, m.top!);
writeInt32(writer, m.left!);
writeInt32(writer, m.bottom!);
writeInt32(writer, m.right!);
writeUint8(writer, mask.defaultColor!);
let params = 0;
if (mask.userMaskDensity !== undefined) params |= MaskParams.UserMaskDensity;
if (mask.userMaskFeather !== undefined) params |= MaskParams.UserMaskFeather;
if (mask.vectorMaskDensity !== undefined) params |= MaskParams.VectorMaskDensity;
if (mask.vectorMaskFeather !== undefined) params |= MaskParams.VectorMaskFeather;
let flags = 0;
if (mask.disabled) flags |= LayerMaskFlags.LayerMaskDisabled;
if (mask.positionRelativeToLayer) flags |= LayerMaskFlags.PositionRelativeToLayer;
if (mask.fromVectorData) flags |= LayerMaskFlags.LayerMaskFromRenderingOtherData;
if (params) flags |= LayerMaskFlags.MaskHasParametersAppliedToIt;
writeUint8(writer, flags);
if (params) {
writeUint8(writer, params);
if (mask.userMaskDensity !== undefined) writeUint8(writer, Math.round(mask.userMaskDensity * 0xff));
if (mask.userMaskFeather !== undefined) writeFloat64(writer, mask.userMaskFeather);
if (mask.vectorMaskDensity !== undefined) writeUint8(writer, Math.round(mask.vectorMaskDensity * 0xff));
if (mask.vectorMaskFeather !== undefined) writeFloat64(writer, mask.vectorMaskFeather);
}
// TODO: handle rest of the fields
writeZeros(writer, 2);
});
}
function writeLayerBlendingRanges(writer: PsdWriter, psd: Psd) {
writeSection(writer, 1, () => {
writeUint32(writer, 65535);
writeUint32(writer, 65535);
let channels = psd.channels || 0; // TODO: use always 4 instead ?
// channels = 4; // TESTING
for (let i = 0; i < channels; i++) {
writeUint32(writer, 65535);
writeUint32(writer, 65535);
}
});
}
function writeGlobalLayerMaskInfo(writer: PsdWriter, info: GlobalLayerMaskInfo | undefined) {
writeSection(writer, 1, () => {
if (info) {
writeUint16(writer, info.overlayColorSpace);
writeUint16(writer, info.colorSpace1);
writeUint16(writer, info.colorSpace2);
writeUint16(writer, info.colorSpace3);
writeUint16(writer, info.colorSpace4);
writeUint16(writer, info.opacity * 0xff);
writeUint8(writer, info.kind);
writeZeros(writer, 3);
}
});
}
function writeAdditionalLayerInfo(writer: PsdWriter, target: LayerAdditionalInfo, psd: Psd, options: ExtendedWriteOptions) {
for (const handler of infoHandlers) {
let key = handler.key;
if (key === 'Txt2' && options.invalidateTextLayers) continue;
if (key === 'vmsk' && options.psb) key = 'vsms';
if (handler.has(target)) {
const large = options.psb && largeAdditionalInfoKeys.indexOf(key) !== -1;
writeSignature(writer, large ? '8B64' : '8BIM');
writeSignature(writer, key);
const fourBytes = key === 'Txt2' || key === 'luni' || key === 'vmsk' || key === 'artb' || key === 'artd' ||
key === 'vogk' || key === 'SoLd' || key === 'lnk2' || key === 'vscg' || key === 'vsms' || key === 'GdFl' ||
key === 'lmfx' || key === 'lrFX' || key === 'cinf' || key === 'PlLd' || key === 'Anno';
writeSection(writer, fourBytes ? 4 : 2, () => {
handler.write(writer, target, psd, options);
}, key !== 'Txt2' && key !== 'cinf' && key !== 'extn', large);
}
}
}
function addChildren(layers: Layer[], children: Layer[] | undefined) {
if (!children) return;
for (const c of children) {
if (c.children && c.canvas) throw new Error(`Invalid layer, cannot have both 'canvas' and 'children' properties`);
if (c.children && c.imageData) throw new Error(`Invalid layer, cannot have both 'imageData' and 'children' properties`);
if (c.children) {
layers.push({
name: '</Layer group>',
sectionDivider: {
type: SectionDividerType.BoundingSectionDivider,
},
// TESTING
// nameSource: 'lset',
// id: [4, 0, 0, 8, 11, 0, 0, 0, 0, 14][layers.length] || 0,
// layerColor: 'none',
// timestamp: [1611346817.349021, 0, 0, 1611346817.349175, 1611346817.3491833, 0, 0, 0, 0, 1611346817.349832][layers.length] || 0,
// protected: {},
// referencePoint: { x: 0, y: 0 },
});
addChildren(layers, c.children);
layers.push({
sectionDivider: {
type: c.opened === false ? SectionDividerType.ClosedFolder : SectionDividerType.OpenFolder,
key: fromBlendMode[c.blendMode!] || 'pass',
subType: 0,
},
...c,
});
} else {
layers.push({ ...c });
}
}
}
function resizeBuffer(writer: PsdWriter, size: number) {
let newLength = writer.buffer.byteLength;
do {
newLength *= 2;
} while (size > newLength);
const newBuffer = new ArrayBuffer(newLength);
const newBytes = new Uint8Array(newBuffer);
const oldBytes = new Uint8Array(writer.buffer);
newBytes.set(oldBytes);
writer.buffer = newBuffer;
writer.view = new DataView(writer.buffer);
}
function ensureSize(writer: PsdWriter, size: number) {
if (size > writer.buffer.byteLength) {
resizeBuffer(writer, size);
}
}
function addSize(writer: PsdWriter, size: number) {
const offset = writer.offset;
ensureSize(writer, writer.offset += size);
return offset;
}
function createThumbnail(psd: Psd) {
const canvas = createCanvas(10, 10);
let scale = 1;
if (psd.width > psd.height) {
canvas.width = 160;
canvas.height = Math.floor(psd.height * (canvas.width / psd.width));
scale = canvas.width / psd.width;
} else {
canvas.height = 160;
canvas.width = Math.floor(psd.width * (canvas.height / psd.height));
scale = canvas.height / psd.height;
}
const context = canvas.getContext('2d')!;
context.scale(scale, scale);
if (psd.imageData) {
const temp = createCanvas(psd.imageData.width, psd.imageData.height);
temp.getContext('2d')!.putImageData(psd.imageData, 0, 0);
context.drawImage(temp, 0, 0);
} else if (psd.canvas) {
context.drawImage(psd.canvas, 0, 0);
}
return canvas;
}
function getChannels(
tempBuffer: Uint8Array, layer: Layer, background: boolean, options: WriteOptions
): LayerChannelData {
const layerData = getLayerChannels(tempBuffer, layer, background, options);
const mask = layer.mask;
if (mask) {
let top = (mask.top as any) | 0;
let left = (mask.left as any) | 0;
let right = (mask.right as any) | 0;
let bottom = (mask.bottom as any) | 0;
let { width, height } = getLayerDimentions(mask);
let imageData = mask.imageData;
if (!imageData && mask.canvas && width && height) {
imageData = mask.canvas.getContext('2d')!.getImageData(0, 0, width, height);
}
if (width && height && imageData) {
right = left + width;
bottom = top + height;
if (imageData.width !== width || imageData.height !== height) {
throw new Error('Invalid imageData dimentions');
}
let buffer: Uint8Array;
let compression: Compression;
if (RAW_IMAGE_DATA && (layer as any).maskDataRaw) {
// console.log('written raw layer image data');
buffer = (layer as any).maskDataRaw;
compression = Compression.RleCompressed;
} else if (options.compress) {
buffer = writeDataZipWithoutPrediction(imageData, [0]);
compression = Compression.ZipWithoutPrediction;
} else {
buffer = writeDataRLE(tempBuffer, imageData, [0], !!options.psb)!;
compression = Compression.RleCompressed;
}
layerData.mask = { top, left, right, bottom };
layerData.channels.push({ channelId: ChannelID.UserMask, compression, buffer, length: 2 + buffer.length });
} else {
layerData.mask = { top: 0, left: 0, right: 0, bottom: 0 };
layerData.channels.push({ channelId: ChannelID.UserMask, compression: Compression.RawData, buffer: new Uint8Array(0), length: 0 });
}
}
return layerData;
}
function getLayerDimentions({ canvas, imageData }: Layer): { width: number; height: number; } {
return imageData || canvas || { width: 0, height: 0 };
}
function cropImageData(data: ImageData, left: number, top: number, width: number, height: number) {
const croppedData = createImageData(width, height);
const srcData = data.data;
const dstData = croppedData.data;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let src = ((x + left) + (y + top) * width) * 4;
let dst = (x + y * width) * 4;
dstData[dst] = srcData[src];
dstData[dst + 1] = srcData[src + 1];
dstData[dst + 2] = srcData[src + 2];
dstData[dst + 3] = srcData[src + 3];
}
}
return croppedData;
}
function getLayerChannels(
tempBuffer: Uint8Array, layer: Layer, background: boolean, options: WriteOptions
): LayerChannelData {
let top = (layer.top as any) | 0;
let left = (layer.left as any) | 0;
let right = (layer.right as any) | 0;
let bottom = (layer.bottom as any) | 0;
let channels: ChannelData[] = [
{ channelId: ChannelID.Transparency, compression: Compression.RawData, buffer: undefined, length: 2 },
{ channelId: ChannelID.Color0, compression: Compression.RawData, buffer: undefined, length: 2 },
{ channelId: ChannelID.Color1, compression: Compression.RawData, buffer: undefined, length: 2 },
{ channelId: ChannelID.Color2, compression: Compression.RawData, buffer: undefined, length: 2 },
];
let { width, height } = getLayerDimentions(layer);
if (!(layer.canvas || layer.imageData) || !width || !height) {
right = left;
bottom = top;
return { layer, top, left, right, bottom, channels };
}
right = left + width;
bottom = top + height;
let data = layer.imageData || layer.canvas!.getContext('2d')!.getImageData(0, 0, width, height);
if (options.trimImageData) {
const trimmed = trimData(data);
if (trimmed.left !== 0 || trimmed.top !== 0 || trimmed.right !== data.width || trimmed.bottom !== data.height) {
left += trimmed.left;
top += trimmed.top;
right -= (data.width - trimmed.right);
bottom -= (data.height - trimmed.bottom);
width = right - left;
height = bottom - top;
if (!width || !height) {
return { layer, top, left, right, bottom, channels };
}
if (layer.imageData) {
data = cropImageData(data, trimmed.left, trimmed.top, width, height);
} else {
data = layer.canvas!.getContext('2d')!.getImageData(trimmed.left, trimmed.top, width, height);
}
}
}
const channelIds = [
ChannelID.Color0,
ChannelID.Color1,
ChannelID.Color2,
];
if (!background || options.noBackground || layer.mask || hasAlpha(data) || (RAW_IMAGE_DATA && (layer as any).imageDataRaw?.['-1'])) {
channelIds.unshift(ChannelID.Transparency);
}
channels = channelIds.map(channelId => {
const offset = offsetForChannel(channelId, false); // TODO: psd.colorMode === ColorMode.CMYK);
let buffer: Uint8Array;
let compression: Compression;
if (RAW_IMAGE_DATA && (layer as any).imageDataRaw) {
// console.log('written raw layer image data');
buffer = (layer as any).imageDataRaw[channelId];
compression = Compression.RleCompressed;
} else if (options.compress) {
buffer = writeDataZipWithoutPrediction(data, [offset]);
compression = Compression.ZipWithoutPrediction;
} else {
buffer = writeDataRLE(tempBuffer, data, [offset], !!options.psb)!;
compression = Compression.RleCompressed;
}
return { channelId, compression, buffer, length: 2 + buffer.length };
});
return { layer, top, left, right, bottom, channels };
}
function isRowEmpty({ data, width }: PixelData, y: number, left: number, right: number) {
const start = ((y * width + left) * 4 + 3) | 0;
const end = (start + (right - left) * 4) | 0;
for (let i = start; i < end; i = (i + 4) | 0) {
if (data[i] !== 0) {
return false;
}
}
return true;
}
function isColEmpty({ data, width }: PixelData, x: number, top: number, bottom: number) {
const stride = (width * 4) | 0;
const start = (top * stride + x * 4 + 3) | 0;
for (let y = top, i = start; y < bottom; y++, i = (i + stride) | 0) {
if (data[i] !== 0) {
return false;
}
}
return true;
}
function trimData(data: PixelData) {
let top = 0;
let left = 0;
let right = data.width;
let bottom = data.height;
while (top < bottom && isRowEmpty(data, top, left, right))
top++;
while (bottom > top && isRowEmpty(data, bottom - 1, left, right))
bottom--;
while (left < right && isColEmpty(data, left, top, bottom))
left++;
while (right > left && isColEmpty(data, right - 1, top, bottom))
right--;
return { top, left, right, bottom };
}
export function writeColor(writer: PsdWriter, color: Color | undefined) {
if (!color) {
writeUint16(writer, ColorSpace.RGB);
writeZeros(writer, 8);
} else if ('r' in color) {
writeUint16(writer, ColorSpace.RGB);
writeUint16(writer, Math.round(color.r * 257));
writeUint16(writer, Math.round(color.g * 257));
writeUint16(writer, Math.round(color.b * 257));
writeUint16(writer, 0);
} else if ('fr' in color) {
writeUint16(writer, ColorSpace.RGB);
writeUint16(writer, Math.round(color.fr * 255 * 257));
writeUint16(writer, Math.round(color.fg * 255 * 257));
writeUint16(writer, Math.round(color.fb * 255 * 257));
writeUint16(writer, 0);
} else if ('l' in color) {
writeUint16(writer, ColorSpace.Lab);
writeInt16(writer, Math.round(color.l * 10000));
writeInt16(writer, Math.round(color.a < 0 ? (color.a * 12800) : (color.a * 12700)));
writeInt16(writer, Math.round(color.b < 0 ? (color.b * 12800) : (color.b * 12700)));
writeUint16(writer, 0);
} else if ('h' in color) {
writeUint16(writer, ColorSpace.HSB);
writeUint16(writer, Math.round(color.h * 0xffff));
writeUint16(writer, Math.round(color.s * 0xffff));
writeUint16(writer, Math.round(color.b * 0xffff));
writeUint16(writer, 0);
} else if ('c' in color) {
writeUint16(writer, ColorSpace.CMYK);
writeUint16(writer, Math.round(color.c * 257));
writeUint16(writer, Math.round(color.m * 257));
writeUint16(writer, Math.round(color.y * 257));
writeUint16(writer, Math.round(color.k * 257));
} else {
writeUint16(writer, ColorSpace.Grayscale);
writeUint16(writer, Math.round(color.k * 10000 / 255));
writeZeros(writer, 6);
}
}

View File

@@ -0,0 +1,752 @@
import { TextStyle, LayerTextData, ParagraphStyle, Font, AntiAlias, TextGridInfo, Justification, Color } from './psd';
interface Adjustments {
Axis: number[];
XY: number[];
}
interface TypeValues {
Type: number;
Values: number[];
}
interface ParagraphProperties {
Justification?: number;
FirstLineIndent?: number;
StartIndent?: number;
EndIndent?: number;
SpaceBefore?: number;
SpaceAfter?: number;
AutoHyphenate?: boolean;
HyphenatedWordSize?: number;
PreHyphen?: number;
PostHyphen?: number;
ConsecutiveHyphens?: number;
Zone?: number;
WordSpacing?: number[];
LetterSpacing?: number[];
GlyphSpacing?: number[];
AutoLeading?: number;
LeadingType?: number;
Hanging?: boolean;
Burasagari?: boolean;
KinsokuOrder?: number;
EveryLineComposer?: boolean;
}
interface ParagraphSheet {
Name?: string;
DefaultStyleSheet: number;
Properties: ParagraphProperties;
}
interface StyleSheetData {
Font?: number;
FontSize?: number;
FauxBold?: boolean;
FauxItalic?: boolean;
AutoLeading?: boolean;
Leading?: number;
HorizontalScale?: number;
VerticalScale?: number;
Tracking?: number;
AutoKerning?: boolean;
Kerning?: number;
BaselineShift?: number;
FontCaps?: number;
FontBaseline?: number;
Underline?: boolean;
Strikethrough?: boolean;
Ligatures?: boolean;
DLigatures?: boolean;
BaselineDirection?: number;
Tsume?: number;
StyleRunAlignment?: number;
Language?: number;
NoBreak?: boolean;
FillColor?: TypeValues;
StrokeColor?: TypeValues;
FillFlag?: boolean;
StrokeFlag?: boolean;
FillFirst?: boolean;
YUnderline?: number;
OutlineWidth?: number;
CharacterDirection?: number;
HindiNumbers?: boolean;
Kashida?: number;
DiacriticPos?: number;
}
interface FontSet {
Name: string;
Script: number;
FontType: number;
Synthetic: number;
}
interface ResourceDict {
KinsokuSet: any[];
MojiKumiSet: any[];
TheNormalStyleSheet: number;
TheNormalParagraphSheet: number;
ParagraphSheetSet: ParagraphSheet[];
StyleSheetSet: { Name: string; StyleSheetData: StyleSheetData; }[];
FontSet: FontSet[];
SuperscriptSize: number;
SuperscriptPosition: number;
SubscriptSize: number;
SubscriptPosition: number;
SmallCapSize: number;
}
interface ParagraphRun {
ParagraphSheet: ParagraphSheet;
Adjustments: Adjustments;
}
interface StyleRun {
StyleSheet: { StyleSheetData: StyleSheetData; };
}
interface PhotoshopNode {
ShapeType?: number;
PointBase?: number[];
BoxBounds?: number[];
Base?: {
ShapeType: number;
TransformPoint0: number[];
TransformPoint1: number[];
TransformPoint2: number[];
};
}
interface EngineData {
EngineDict: {
Editor: { Text: string; };
ParagraphRun: {
DefaultRunData: ParagraphRun;
RunArray: ParagraphRun[];
RunLengthArray: number[];
IsJoinable: number;
};
StyleRun: {
DefaultRunData: StyleRun;
RunArray: StyleRun[];
RunLengthArray: number[];
IsJoinable: number;
};
GridInfo: {
GridIsOn: boolean;
ShowGrid: boolean;
GridSize: number;
GridLeading: number;
GridColor: TypeValues;
GridLeadingFillColor: TypeValues;
AlignLineHeightToGridFlags: boolean;
};
AntiAlias: number;
UseFractionalGlyphWidths: boolean;
Rendered?: {
Version: number;
Shapes?: {
WritingDirection: number;
Children?: {
ShapeType?: number;
Procession: number;
Lines: { WritingDirection: number; Children: any[]; };
Cookie?: {
Photoshop?: PhotoshopNode;
};
}[];
};
};
};
ResourceDict: ResourceDict;
DocumentResources: ResourceDict;
}
const defaultFont: Font = {
name: 'MyriadPro-Regular',
script: 0,
type: 0,
synthetic: 0,
};
const defaultParagraphStyle: ParagraphStyle = {
justification: 'left',
firstLineIndent: 0,
startIndent: 0,
endIndent: 0,
spaceBefore: 0,
spaceAfter: 0,
autoHyphenate: true,
hyphenatedWordSize: 6,
preHyphen: 2,
postHyphen: 2,
consecutiveHyphens: 8,
zone: 36,
wordSpacing: [0.8, 1, 1.33],
letterSpacing: [0, 0, 0],
glyphSpacing: [1, 1, 1],
autoLeading: 1.2,
leadingType: 0,
hanging: false,
burasagari: false,
kinsokuOrder: 0,
everyLineComposer: false,
};
const defaultStyle: TextStyle = {
font: defaultFont,
fontSize: 12,
fauxBold: false,
fauxItalic: false,
autoLeading: true,
leading: 0,
horizontalScale: 1,
verticalScale: 1,
tracking: 0,
autoKerning: true,
kerning: 0,
baselineShift: 0,
fontCaps: 0,
fontBaseline: 0,
underline: false,
strikethrough: false,
ligatures: true,
dLigatures: false,
baselineDirection: 2,
tsume: 0,
styleRunAlignment: 2,
language: 0,
noBreak: false,
fillColor: { r: 0, g: 0, b: 0 },
strokeColor: { r: 0, g: 0, b: 0 },
fillFlag: true,
strokeFlag: false,
fillFirst: true,
yUnderline: 1,
outlineWidth: 1,
characterDirection: 0,
hindiNumbers: false,
kashida: 1,
diacriticPos: 2,
};
const defaultGridInfo: TextGridInfo = {
isOn: false,
show: false,
size: 18,
leading: 22,
color: { r: 0, g: 0, b: 255 },
leadingFillColor: { r: 0, g: 0, b: 255 },
alignLineHeightToGridFlags: false,
};
const paragraphStyleKeys: (keyof ParagraphStyle)[] = [
'justification', 'firstLineIndent', 'startIndent', 'endIndent', 'spaceBefore', 'spaceAfter',
'autoHyphenate', 'hyphenatedWordSize', 'preHyphen', 'postHyphen', 'consecutiveHyphens',
'zone', 'wordSpacing', 'letterSpacing', 'glyphSpacing', 'autoLeading', 'leadingType',
'hanging', 'burasagari', 'kinsokuOrder', 'everyLineComposer',
];
const styleKeys: (keyof TextStyle)[] = [
'font', 'fontSize', 'fauxBold', 'fauxItalic', 'autoLeading', 'leading', 'horizontalScale',
'verticalScale', 'tracking', 'autoKerning', 'kerning', 'baselineShift', 'fontCaps', 'fontBaseline',
'underline', 'strikethrough', 'ligatures', 'dLigatures', 'baselineDirection', 'tsume',
'styleRunAlignment', 'language', 'noBreak', 'fillColor', 'strokeColor', 'fillFlag',
'strokeFlag', 'fillFirst', 'yUnderline', 'outlineWidth', 'characterDirection', 'hindiNumbers',
'kashida', 'diacriticPos',
];
const antialias: AntiAlias[] = ['none', 'crisp', 'strong', 'smooth', 'sharp'];
const justification: Justification[] = ['left', 'right', 'center'];
function upperFirst(value: string) {
return value.substr(0, 1).toUpperCase() + value.substr(1);
}
function decodeColor(color: { Type: number; Values: number[]; }): Color {
const c = color.Values;
if (color.Type === 0) { // grayscale
return { r: c[1] * 255, g: c[1] * 255, b: c[1] * 255 }; // , c[0] * 255];
} else { // rgb
return { r: c[1] * 255, g: c[2] * 255, b: c[3] * 255, a: c[0] }; // , c[0] * 255];
}
}
function encodeColor(color: Color | undefined) {
if (color && 'r' in color) {
return ['a' in color ? color.a : 1, color.r / 255, color.g / 255, color.b / 255];
} else {
return [0, 0, 0, 0];
}
}
function arraysEqual(a: any[], b: any[]) {
if (!a || !b) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
}
function objectsEqual(a: any, b: any) {
if (!a || !b) return false;
for (const key of Object.keys(a)) if (a[key] !== b[key]) return false;
for (const key of Object.keys(b)) if (a[key] !== b[key]) return false;
return true;
}
function findOrAddFont(fonts: Font[], font: Font) {
for (let i = 0; i < fonts.length; i++) {
if (fonts[i].name === font.name) return i;
}
fonts.push(font);
return fonts.length - 1;
}
function decodeObject(obj: any, keys: string[], fonts: Font[]) {
const result: any = {};
for (const key of keys) {
const Key = upperFirst(key);
if (obj[Key] === undefined) continue;
if (key === 'justification') {
result[key] = justification[obj[Key]];
} else if (key === 'font') {
result[key] = fonts[obj[Key]];
} else if (key === 'fillColor' || key === 'strokeColor') {
result[key] = decodeColor(obj[Key]);
} else {
result[key] = obj[Key];
}
}
return result;
}
function encodeObject(obj: any, keys: string[], fonts: Font[]) {
const result: any = {};
for (const key of keys) {
const Key = upperFirst(key);
if (obj[key] === undefined) continue;
if (key === 'justification') {
result[Key] = justification.indexOf(obj[key] ?? 'left');
} else if (key === 'font') {
result[Key] = findOrAddFont(fonts, obj[key]);
} else if (key === 'fillColor' || key === 'strokeColor') {
result[Key] = { Type: 1, Values: encodeColor(obj[key]) } as TypeValues;
} else {
result[Key] = obj[key];
}
}
return result;
}
function decodeParagraphStyle(obj: ParagraphProperties, fonts: Font[]): ParagraphStyle {
return decodeObject(obj, paragraphStyleKeys, fonts);
}
function decodeStyle(obj: StyleSheetData, fonts: Font[]): TextStyle {
return decodeObject(obj, styleKeys, fonts);
}
function encodeParagraphStyle(obj: ParagraphStyle, fonts: Font[]): ParagraphProperties {
return encodeObject(obj, paragraphStyleKeys, fonts);
}
function encodeStyle(obj: TextStyle, fonts: Font[]): StyleSheetData {
return encodeObject(obj, styleKeys, fonts);
}
function deduplicateValues<T>(base: T, runs: { style: T; }[], keys: (keyof T)[]) {
if (!runs.length) return;
for (const key of keys) {
const value = runs[0].style[key];
if (value !== undefined) {
let identical = false;
if (Array.isArray(value)) {
identical = runs.every(r => arraysEqual(r.style[key] as any, value));
} else if (typeof value === 'object') {
identical = runs.every(r => objectsEqual(r.style[key] as any, value));
} else {
identical = runs.every(r => r.style[key] === value);
}
if (identical) {
base[key] = value as any;
}
}
const styleValue = base[key];
if (styleValue !== undefined) {
for (const r of runs) {
let same = false;
if (Array.isArray(value)) {
same = arraysEqual(r.style[key] as any, value);
} else if (typeof value === 'object') {
same = objectsEqual(r.style[key] as any, value);
} else {
same = r.style[key] === value;
}
if (same) delete r.style[key];
}
}
}
if (runs.every(x => Object.keys(x.style as any).length === 0)) {
runs.length = 0;
}
}
export function decodeEngineData(engineData: EngineData) {
// console.log('engineData', require('util').inspect(engineData, false, 99, true));
const engineDict = engineData.EngineDict;
const resourceDict = engineData.ResourceDict;
const fonts = resourceDict.FontSet.map<Font>(f => ({
name: f.Name,
script: f.Script,
type: f.FontType,
synthetic: f.Synthetic,
}));
let text = engineDict.Editor.Text.replace(/\r/g, '\n');
let removedCharacters = 0;
while (/\n$/.test(text)) {
text = text.substr(0, text.length - 1);
removedCharacters++;
}
const result: LayerTextData = {
text,
antiAlias: antialias[engineDict.AntiAlias] ?? 'smooth',
useFractionalGlyphWidths: !!engineDict.UseFractionalGlyphWidths,
superscriptSize: resourceDict.SuperscriptSize,
superscriptPosition: resourceDict.SuperscriptPosition,
subscriptSize: resourceDict.SubscriptSize,
subscriptPosition: resourceDict.SubscriptPosition,
smallCapSize: resourceDict.SmallCapSize,
};
// shape
const photoshop = engineDict.Rendered?.Shapes?.Children?.[0]?.Cookie?.Photoshop;
if (photoshop) {
result.shapeType = photoshop.ShapeType === 1 ? 'box' : 'point';
if (photoshop.PointBase) result.pointBase = photoshop.PointBase;
if (photoshop.BoxBounds) result.boxBounds = photoshop.BoxBounds;
}
// paragraph style
// const theNormalParagraphSheet = resourceDict.TheNormalParagraphSheet;
// const paragraphSheetSet = resourceDict.ParagraphSheetSet;
// const paragraphProperties = paragraphSheetSet[theNormalParagraphSheet].Properties;
const paragraphRun = engineData.EngineDict.ParagraphRun;
result.paragraphStyle = {}; // decodeParagraphStyle(paragraphProperties, fonts);
result.paragraphStyleRuns = [];
for (let i = 0; i < paragraphRun.RunArray.length; i++) {
const run = paragraphRun.RunArray[i];
const length = paragraphRun.RunLengthArray[i];
const style = decodeParagraphStyle(run.ParagraphSheet.Properties, fonts);
// const adjustments = {
// axis: run.Adjustments.Axis,
// xy: run.Adjustments.XY,
// };
result.paragraphStyleRuns.push({ length, style/*, adjustments*/ });
}
for (let counter = removedCharacters; result.paragraphStyleRuns.length && counter > 0; counter--) {
if (--result.paragraphStyleRuns[result.paragraphStyleRuns.length - 1].length === 0) {
result.paragraphStyleRuns.pop();
}
}
deduplicateValues(result.paragraphStyle, result.paragraphStyleRuns, paragraphStyleKeys);
if (!result.paragraphStyleRuns.length) delete result.paragraphStyleRuns;
// style
// const theNormalStyleSheet = resourceDict.TheNormalStyleSheet;
// const styleSheetSet = resourceDict.StyleSheetSet;
// const styleSheetData = styleSheetSet[theNormalStyleSheet].StyleSheetData;
const styleRun = engineData.EngineDict.StyleRun;
result.style = {}; // decodeStyle(styleSheetData, fonts);
result.styleRuns = [];
for (let i = 0; i < styleRun.RunArray.length; i++) {
const length = styleRun.RunLengthArray[i];
const style = decodeStyle(styleRun.RunArray[i].StyleSheet.StyleSheetData, fonts);
if (!style.font) style.font = fonts[0];
result.styleRuns.push({ length, style });
}
for (let counter = removedCharacters; result.styleRuns.length && counter > 0; counter--) {
if (--result.styleRuns[result.styleRuns.length - 1].length === 0) {
result.styleRuns.pop();
}
}
deduplicateValues(result.style, result.styleRuns, styleKeys);
if (!result.styleRuns.length) delete result.styleRuns;
return result;
}
export function encodeEngineData(data: LayerTextData) {
const text = `${(data.text || '').replace(/\r?\n/g, '\r')}\r`;
const fonts: Font[] = [
{ name: 'AdobeInvisFont', script: 0, type: 0, synthetic: 0 },
];
const defFont = data.style?.font || data.styleRuns?.find(s => s.style.font)?.style.font || defaultFont;
const paragraphRunArray: ParagraphRun[] = [];
const paragraphRunLengthArray: number[] = [];
const paragraphRuns = data.paragraphStyleRuns;
if (paragraphRuns && paragraphRuns.length) {
let leftLength = text.length;
for (const run of paragraphRuns) {
let runLength = Math.min(run.length, leftLength);
leftLength -= runLength;
if (!runLength) continue; // ignore 0 size runs
// extend last run if it's only for trailing \r
if (leftLength === 1 && run === paragraphRuns[paragraphRuns.length - 1]) {
runLength++;
leftLength--;
}
paragraphRunLengthArray.push(runLength);
paragraphRunArray.push({
ParagraphSheet: {
DefaultStyleSheet: 0,
Properties: encodeParagraphStyle({ ...defaultParagraphStyle, ...data.paragraphStyle, ...run.style }, fonts),
},
Adjustments: { Axis: [1, 0, 1], XY: [0, 0] },
});
}
if (leftLength) {
paragraphRunLengthArray.push(leftLength);
paragraphRunArray.push({
ParagraphSheet: {
DefaultStyleSheet: 0,
Properties: encodeParagraphStyle({ ...defaultParagraphStyle, ...data.paragraphStyle }, fonts),
},
Adjustments: { Axis: [1, 0, 1], XY: [0, 0] },
});
}
} else {
for (let i = 0, last = 0; i < text.length; i++) {
if (text.charCodeAt(i) === 13) { // \r
paragraphRunLengthArray.push(i - last + 1);
paragraphRunArray.push({
ParagraphSheet: {
DefaultStyleSheet: 0,
Properties: encodeParagraphStyle({ ...defaultParagraphStyle, ...data.paragraphStyle }, fonts),
},
Adjustments: { Axis: [1, 0, 1], XY: [0, 0] },
});
last = i + 1;
}
}
}
const styleSheetData = encodeStyle({ ...defaultStyle, font: defFont }, fonts);
const styleRuns = data.styleRuns || [{ length: text.length, style: data.style || {} }];
const styleRunArray: StyleRun[] = [];
const styleRunLengthArray: number[] = [];
let leftLength = text.length;
for (const run of styleRuns) {
let runLength = Math.min(run.length, leftLength);
leftLength -= runLength;
if (!runLength) continue; // ignore 0 size runs
// extend last run if it's only for trailing \r
if (leftLength === 1 && run === styleRuns[styleRuns.length - 1]) {
runLength++;
leftLength--;
}
styleRunLengthArray.push(runLength);
styleRunArray.push({
StyleSheet: {
StyleSheetData: encodeStyle({
kerning: 0,
autoKerning: true,
fillColor: { r: 0, g: 0, b: 0 },
...data.style,
...run.style,
}, fonts),
},
});
}
// add extra run to the end if existing ones didn't fill it up
if (leftLength && styleRuns.length) {
styleRunLengthArray.push(leftLength);
styleRunArray.push({
StyleSheet: {
StyleSheetData: encodeStyle({
kerning: 0,
autoKerning: true,
fillColor: { r: 0, g: 0, b: 0 },
...data.style,
}, fonts),
},
});
}
const gridInfo = { ...defaultGridInfo, ...data.gridInfo };
const WritingDirection = data.orientation === 'vertical' ? 2 : 0;
const Procession = data.orientation === 'vertical' ? 1 : 0;
const ShapeType = data.shapeType === 'box' ? 1 : 0;
const Photoshop: PhotoshopNode = {
ShapeType,
};
if (ShapeType === 0) {
Photoshop.PointBase = data.pointBase || [0, 0];
} else {
Photoshop.BoxBounds = data.boxBounds || [0, 0, 0, 0];
}
// needed for correct order of properties
Photoshop.Base = {
ShapeType,
TransformPoint0: [1, 0],
TransformPoint1: [0, 1],
TransformPoint2: [0, 0],
};
const defaultResources = {
KinsokuSet: [
{
Name: 'PhotoshopKinsokuHard',
NoStart: '、。,.・:;?!ー―’”)〕]}〉》」』】ヽヾゝゞ々ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ゛゜?!)]},.:;℃℉¢%‰',
NoEnd: '‘“(〔[{〈《「『【([{¥$£@§〒#',
Keep: '―‥',
Hanging: '、。.,',
},
{
Name: 'PhotoshopKinsokuSoft',
NoStart: '、。,.・:;?!’”)〕]}〉》」』】ヽヾゝゞ々',
NoEnd: '‘“(〔[{〈《「『【',
Keep: '―‥',
Hanging: '、。.,',
},
],
MojiKumiSet: [
{ InternalName: 'Photoshop6MojiKumiSet1' },
{ InternalName: 'Photoshop6MojiKumiSet2' },
{ InternalName: 'Photoshop6MojiKumiSet3' },
{ InternalName: 'Photoshop6MojiKumiSet4' },
],
TheNormalStyleSheet: 0,
TheNormalParagraphSheet: 0,
ParagraphSheetSet: [
{
Name: 'Normal RGB',
DefaultStyleSheet: 0,
Properties: encodeParagraphStyle({ ...defaultParagraphStyle, ...data.paragraphStyle }, fonts),
},
],
StyleSheetSet: [
{
Name: 'Normal RGB',
StyleSheetData: styleSheetData,
},
],
FontSet: fonts.map<FontSet>(f => ({
Name: f.name,
Script: f.script || 0,
FontType: f.type || 0,
Synthetic: f.synthetic || 0,
})),
SuperscriptSize: data.superscriptSize ?? 0.583,
SuperscriptPosition: data.superscriptPosition ?? 0.333,
SubscriptSize: data.subscriptSize ?? 0.583,
SubscriptPosition: data.subscriptPosition ?? 0.333,
SmallCapSize: data.smallCapSize ?? 0.7,
};
const engineData: EngineData = {
EngineDict: {
Editor: { Text: text },
ParagraphRun: {
DefaultRunData: {
ParagraphSheet: { DefaultStyleSheet: 0, Properties: {} },
Adjustments: { Axis: [1, 0, 1], XY: [0, 0] },
},
RunArray: paragraphRunArray,
RunLengthArray: paragraphRunLengthArray,
IsJoinable: 1,
},
StyleRun: {
DefaultRunData: { StyleSheet: { StyleSheetData: {} } },
RunArray: styleRunArray,
RunLengthArray: styleRunLengthArray,
IsJoinable: 2,
},
GridInfo: {
GridIsOn: !!gridInfo.isOn,
ShowGrid: !!gridInfo.show,
GridSize: gridInfo.size ?? 18,
GridLeading: gridInfo.leading ?? 22,
GridColor: { Type: 1, Values: encodeColor(gridInfo.color) },
GridLeadingFillColor: { Type: 1, Values: encodeColor(gridInfo.color) },
AlignLineHeightToGridFlags: !!gridInfo.alignLineHeightToGridFlags,
},
AntiAlias: antialias.indexOf(data.antiAlias ?? 'sharp'),
UseFractionalGlyphWidths: data.useFractionalGlyphWidths ?? true,
Rendered: {
Version: 1,
Shapes: {
WritingDirection,
Children: [
{
ShapeType,
Procession,
Lines: { WritingDirection, Children: [] },
Cookie: { Photoshop },
},
],
},
},
},
ResourceDict: { ...defaultResources },
DocumentResources: { ...defaultResources },
};
// console.log('encodeEngineData', require('util').inspect(engineData, false, 99, true));
return engineData;
}

View File

@@ -0,0 +1,160 @@
function charLengthInBytes(code: number): number {
if ((code & 0xffffff80) === 0) {
return 1;
} else if ((code & 0xfffff800) === 0) {
return 2;
} else if ((code & 0xffff0000) === 0) {
return 3;
} else {
return 4;
}
}
export function stringLengthInBytes(value: string): number {
let result = 0;
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
// high surrogate
if (code >= 0xd800 && code <= 0xdbff) {
if ((i + 1) < value.length) {
const extra = value.charCodeAt(i + 1);
// low surrogate
if ((extra & 0xfc00) === 0xdc00) {
i++;
result += charLengthInBytes(((code & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
}
}
} else {
result += charLengthInBytes(code);
}
}
return result;
}
function writeCharacter(buffer: Uint8Array | Buffer, offset: number, code: number): number {
const length = charLengthInBytes(code);
switch (length) {
case 1:
buffer[offset] = code;
break;
case 2:
buffer[offset] = ((code >> 6) & 0x1f) | 0xc0;
buffer[offset + 1] = (code & 0x3f) | 0x80;
break;
case 3:
buffer[offset] = ((code >> 12) & 0x0f) | 0xe0;
buffer[offset + 1] = ((code >> 6) & 0x3f) | 0x80;
buffer[offset + 2] = (code & 0x3f) | 0x80;
break;
default:
buffer[offset] = ((code >> 18) & 0x07) | 0xf0;
buffer[offset + 1] = ((code >> 12) & 0x3f) | 0x80;
buffer[offset + 2] = ((code >> 6) & 0x3f) | 0x80;
buffer[offset + 3] = (code & 0x3f) | 0x80;
break;
}
return length;
}
export function encodeStringTo(buffer: Uint8Array | Buffer, offset: number, value: string): number {
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
// high surrogate
if (code >= 0xd800 && code <= 0xdbff) {
if ((i + 1) < value.length) {
const extra = value.charCodeAt(i + 1);
// low surrogate
if ((extra & 0xfc00) === 0xdc00) {
i++;
const fullCode = ((code & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
offset += writeCharacter(buffer, offset, fullCode);
}
}
} else {
offset += writeCharacter(buffer, offset, code);
}
}
return offset;
}
export function encodeString(value: string): Uint8Array {
const buffer = new Uint8Array(stringLengthInBytes(value));
encodeStringTo(buffer, 0, value);
return buffer;
}
function continuationByte(buffer: Uint8Array, index: number): number {
if (index >= buffer.length) {
throw Error('Invalid byte index');
}
const continuationByte = buffer[index];
if ((continuationByte & 0xC0) === 0x80) {
return continuationByte & 0x3F;
} else {
throw Error('Invalid continuation byte');
}
}
export function decodeString(value: Uint8Array): string {
let result = '';
for (let i = 0; i < value.length;) {
const byte1 = value[i++];
let code: number;
if ((byte1 & 0x80) === 0) {
code = byte1;
} else if ((byte1 & 0xe0) === 0xc0) {
const byte2 = continuationByte(value, i++);
code = ((byte1 & 0x1f) << 6) | byte2;
if (code < 0x80) {
throw Error('Invalid continuation byte');
}
} else if ((byte1 & 0xf0) === 0xe0) {
const byte2 = continuationByte(value, i++);
const byte3 = continuationByte(value, i++);
code = ((byte1 & 0x0f) << 12) | (byte2 << 6) | byte3;
if (code < 0x0800) {
throw Error('Invalid continuation byte');
}
if (code >= 0xd800 && code <= 0xdfff) {
throw Error(`Lone surrogate U+${code.toString(16).toUpperCase()} is not a scalar value`);
}
} else if ((byte1 & 0xf8) === 0xf0) {
const byte2 = continuationByte(value, i++);
const byte3 = continuationByte(value, i++);
const byte4 = continuationByte(value, i++);
code = ((byte1 & 0x0f) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
if (code < 0x010000 || code > 0x10ffff) {
throw Error('Invalid continuation byte');
}
} else {
throw Error('Invalid UTF-8 detected');
}
if (code > 0xffff) {
code -= 0x10000;
result += String.fromCharCode(code >>> 10 & 0x3ff | 0xd800);
code = 0xdc00 | code & 0x3ff;
}
result += String.fromCharCode(code);
}
return result;
}