Files
ccc-tnt-psd2ui/npm-packages/mac-v2.4.x/ag-psd/src/abr.ts

541 lines
16 KiB
TypeScript
Raw Normal View History

2023-07-24 11:13:08 +08:00
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 };
}