You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

4538 lines
120 KiB

"use strict";
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
const Util = {
pad2: function(i) {
return i + 1 & ~1;
},
pad4: function(i) {
return (i + 4 & ~3) - 1;
},
getUnicodeCharacter: function(cp) {
if (cp >= 0 && cp <= 55295 || cp >= 57344 && cp <= 65535) {
return String.fromCharCode(cp);
} else if (cp >= 65536 && cp <= 1114111) {
cp -= 65536;
const first = ((1047552 & cp) >> 10) + 55296;
const second = (1023 & cp) + 56320;
return String.fromCharCode(first) + String.fromCharCode(second);
}
},
clamp: function(num, min, max) {
return Math.min(Math.max(num, min), max);
}
};
let _converters = {};
const Color = {
// Provide an API to receive customized color converters
setColorConverters: function(converters) {
Object.assign(_converters, converters);
},
rgbToRgb(r, g, b) {
if (_converters.rgbToRgb) {
const res = _converters.rgbToRgb([r, g, b]);
return res;
}
return [r, g, b];
},
// Converts from the CMYK color space to the RGB color space using
// a preset color profile.
cmykToRgb: function(c, m, y, k) {
if (_converters && _converters.cmykToRgb) {
return _converters.cmykToRgb([c, m, y, k]);
}
const r = Util.clamp(65535 - (c * (255 - k) + (k << 8)) >> 8, 0, 255);
const g = Util.clamp(65535 - (m * (255 - k) + (k << 8)) >> 8, 0, 255);
const b = Util.clamp(65535 - (y * (255 - k) + (k << 8)) >> 8, 0, 255);
return [r, g, b];
}
};
const utf16beDecoder = new TextDecoder("utf-16be");
class File {
// Creates a new File with the given Uint8Array.
constructor(data) {
this.data = data;
this.dataView = new DataView(data.buffer);
this.pos = 0;
}
// Returns the current cursor position.
tell() {
return this.pos;
}
// Reads raw file data with no processing.
read(length) {
const start = this.pos;
this.pos += length;
return this.data.subarray(start, this.pos);
}
// Moves the cursor without parsing data. If `rel = false`, then the cursor will be set to the
// given value, which effectively sets the position relative to the start of the file. If
// `rel = true`, then the cursor will be moved relative to the current position.
seek(amt, rel = false) {
if (rel) {
this.pos = this.pos + amt;
} else {
this.pos = amt;
}
}
// Reads a String of the given length.
readString(length) {
return String.fromCharCode.apply(null, this.read(length)).replace(/\u0000/g, "");
}
// Reads a Unicode UTF-16BE encoded string.
readUnicodeString(length = null) {
length || (length = this.readInt());
return utf16beDecoder.decode(this.read(length * 2)).replace(/\u0000/g, "");
}
// Helper that reads a single byte.
readByte() {
return this.read(1)[0];
}
// Helper that reads a single byte and interprets it as a boolean.
readBoolean() {
return this.readByte() !== 0;
}
// Reads a 32-bit color space value.
readSpaceColor() {
let colorComponent;
const colorSpace = this.readShort();
for (let i = 0; i < 4; i++) {
colorComponent = this.readShort() >> 8;
}
return {
colorSpace,
components: colorComponent
};
}
// Adobe's lovely signed 32-bit fixed-point number with 8bits.24bits
// http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/PhotoshopFileFormats.htm#50577409_17587
readPathNumber() {
const a = this.readByte();
const arr = this.read(3);
const b1 = arr[0] << 16;
const b2 = arr[1] << 8;
const b3 = arr[2];
const b = b1 | b2 | b3;
return parseFloat(a, 10) + parseFloat(b / Math.pow(2, 24), 10);
}
readInt() {
const int32 = this.dataView.getInt32(this.pos);
this.pos += 4;
return int32;
}
readUInt() {
const uint32 = this.dataView.getUint32(this.pos);
this.pos += 4;
return uint32;
}
readShort() {
const short = this.dataView.getInt16(this.pos);
this.pos += 2;
return short;
}
readUShort() {
const uShort = this.dataView.getUint16(this.pos);
this.pos += 2;
return uShort;
}
readFloat() {
const float32 = this.dataView.getFloat32(this.pos);
this.pos += 4;
return float32;
}
readDouble() {
const float64 = this.dataView.getFloat64(this.pos);
this.pos += 8;
return float64;
}
readLongLong() {
const data = this.read(8);
let result = 0;
for (let i = 0; i < 8; i++) {
const pow = 7 - i;
result += data[i] * Math.pow(256, pow);
}
return result;
}
}
const MODES = ["Bitmap", "GrayScale", "IndexedColor", "RGBColor", "CMYKColor", "HSLColor", "HSBColor", "Multichannel", "Duotone", "LabColor", "Gray16", "RGB48", "Lab48", "CMYK64", "DeepMultichannel", "Duotone16"];
class Header {
// Creates a new Header.
// @param [File] file The PSD file.
constructor(file) {
this.file = file;
this.sig = null;
this.version = null;
this.channels = null;
this.height = null;
this.width = null;
this.depth = null;
this.mode = null;
}
// Parses the header data.
parse() {
this.sig = this.file.readString(4);
if (this.sig !== "8BPS") {
throw new Error("Invalid file signature detected. Got: " + this.sig + ". Expected 8BPS.");
}
this.version = this.file.readUShort();
this.file.seek(6, true);
this.channels = this.file.readUShort();
this.height = this.file.readUInt();
this.width = this.file.readUInt();
this.depth = this.file.readUShort();
this.mode = this.file.readUShort();
const colorDataLen = this.file.readUInt();
this.file.seek(colorDataLen, true);
}
get rows() {
console.warn("rows is deprecated, use height instead.");
return this.height;
}
get cols() {
console.warn("cols is deprecated, use width instead.");
return this.height;
}
// Converts the color mode key to a readable version.
modeName() {
return MODES[this.mode];
}
// Exports all the header data in a basic object.
export() {
const data = {};
const keys = ["sig", "version", "channels", "height", "width", "depth", "mode"];
const len = keys.length;
for (let i = 0; i < len; i++) {
const key = keys[i];
data[key] = this[key];
}
return data;
}
}
const fs$1 = require("fs");
const { PNG } = require("pngjs");
const ExportPNG = {
toPng: function() {
const png = new PNG({
filterType: 4,
width: this.width(),
height: this.height()
});
png.data = this.pixelData;
return png;
},
saveAsPng: function(output) {
return new Promise((resolve) => {
return this.toPng().pack().pipe(fs$1.createWriteStream(output)).on("finish", resolve);
});
},
maskToPng: function() {
const png = new PNG({
filterType: 4,
width: this.layer.mask.width,
height: this.layer.mask.height
});
png.data = this.maskData;
return png;
},
saveMaskAsPng: function(output) {
return new Promise((resolve) => {
return this.maskToPng().pack().pipe(fs$1.createWriteStream(output)).on("finish", resolve);
});
}
};
class Image {
constructor(file, header, layer) {
this.file = file;
this.header = header;
if (layer) {
this.layer = layer;
this._width = layer.width;
this._height = layer.height;
this.width = function() {
return this._width;
};
this.height = function() {
return this._height;
};
}
this.numPixels = this.width() * this.height();
if (this.depth() === 16) {
this.numPixels *= 2;
}
this.calculateLength();
this.pixelData = new Uint8Array(this.channelLength * 4);
if (this.layer && this.layer.mask.size) {
const width = this.header.width;
const height = this.header.height;
const mask = this.layer.mask;
const w = Math.min(mask.right, width) - Math.max(mask.left, 0);
const h = Math.min(mask.bottom, height) - Math.max(mask.top, 0);
const areaLimit = 2e3 * 3e3;
const actualArea = w * h;
if (this.maskLength > areaLimit && actualArea / this.maskLength < 0.5) {
mask.width = width;
mask.height = height;
mask.isSingleChannel = true;
this.maskData = new Uint8Array(width * height);
} else {
this.maskData = new Uint8Array(this.maskLength * 4);
}
}
this.channelData = new Uint8Array(this.length + this.maskLength);
this.opacity = 1;
this.hasMask = false;
this.startPos = this.file.tell();
this.endPos = this.startPos + this.length;
this.setChannelsInfo();
}
// Sets the channel info based on the PSD color mode.
setChannelsInfo() {
switch (this.mode()) {
case 1:
return this.setGreyscaleChannels();
case 3:
return this.setRgbChannels();
case 4:
return this.setCmykChannels();
}
}
_calcAreaByDepth(depth) {
switch (depth) {
case 1:
return (this.width() + 7) / 8 * this.height();
case 16:
return this.width() * this.height() * 2;
default:
return this.width() * this.height();
}
}
// Calculates the length of the image data.
calculateLength() {
var _a, _b, _c, _d;
this.length = this._calcAreaByDepth(this.depth());
this.channelLength = this.length;
this.length *= this.channels();
this.maskLength = 0;
if ((_b = (_a = this.layer) == null ? void 0 : _a.mask) == null ? void 0 : _b.size) {
this.maskLength += this.layer.mask.width * this.layer.mask.height;
}
if ((_d = (_c = this.layer) == null ? void 0 : _c.mask) == null ? void 0 : _d.vmask) {
const { width, height } = this.layer.mask.vmask;
this.maskLength += width * height;
}
}
// Parses the image and formats the image data.
parse() {
this.compression = this.parseCompression();
if (this.compression === 2 || this.compression === 3) {
this.file.seek(this.endPos);
return;
}
this.parseImageData();
}
// Parses the compression mode.
parseCompression() {
return this.file.readShort();
}
// Parses the image data based on the compression mode.
parseImageData() {
switch (this.compression) {
case 0:
this.parseRaw();
break;
case 1:
this.parseRLE();
break;
case 2:
case 3:
this.parseZip();
break;
default:
this.file.seek(this.endPos);
}
this.processImageData();
}
// Processes the parsed image data based on the color mode.
processImageData() {
switch (this.mode()) {
case 1:
this.combineGreyscaleChannel();
break;
case 3:
this.combineRgbChannel();
break;
case 4:
this.combineCmykChannel();
}
this.channelData = null;
}
parseRaw() {
this.channelData.set(this.file.read(this.length));
}
parseRLE() {
this.byteCounts = this.parseByteCounts();
this.parseChannelData();
}
parseByteCounts() {
const len = this.channels() * this.height();
const byteCounts = [];
for (let i = 0; i < len; i++) {
if (this.header.version === 1) {
byteCounts.push(this.file.readShort());
} else {
byteCounts.push(this.file.readInt());
}
}
return byteCounts;
}
parseChannelData() {
this.chanPos = 0;
this.lineIndex = 0;
const channels = this.channels();
for (let i = 0; i < channels; i++) {
this.decodeRLEChannel();
this.lineIndex += this.height();
}
}
decodeRLEChannel() {
const height = this.height();
for (let i = 0; i < height; i++) {
const byteCount = this.byteCounts[this.lineIndex + i];
const finish = this.file.tell() + byteCount;
while (this.file.tell() < finish) {
let len = this.file.read(1)[0];
if (len < 128) {
len += 1;
const data = this.file.read(len);
this.channelData.set(data, this.chanPos);
this.chanPos += len;
} else if (len > 128) {
len ^= 255;
len += 2;
const val = this.file.read(1)[0];
this.channelData.fill(val, this.chanPos, this.chanPos + len);
this.chanPos += len;
}
}
}
}
setGreyscaleChannels() {
this.channelsInfo = [
{
id: 0
}
];
if (this.channels() === 2) {
return this.channelsInfo.push({
id: -1
});
}
}
combineGreyscaleChannel() {
for (let i = 0; i < this.numPixels; i++) {
const grey = this.channelData[i];
const alpha = this.channels() === 2 ? this.channelData[this.channelLength + i] : 25;
this.pixelData.set([grey, grey, grey, alpha], i * 4);
}
}
setRgbChannels() {
this.channelsInfo = [
{
id: 0
},
{
id: 1
},
{
id: 2
}
];
if (this.channels() === 4) {
return this.channelsInfo.push({
id: -1
});
}
}
combineRgbChannel() {
const rgbChannels = this.channelsInfo.map((ch) => ch.id).filter((ch) => ch >= -1);
for (let i = 0; i < this.numPixels; i++) {
let r = 0;
let g = 0;
let b = 0;
let a = 255;
const len = rgbChannels.length;
for (let j = 0; j < len; j++) {
const chan = rgbChannels[j];
const val = this.channelData[i + this.channelLength * j];
switch (chan) {
case -1:
a = val;
break;
case 0:
r = val;
break;
case 1:
g = val;
break;
case 2:
b = val;
}
}
const [r1, g1, b1] = Color.rgbToRgb(r, g, b);
this.pixelData.set([r1, g1, b1, a], i * 4);
}
return this.readMaskData(rgbChannels);
}
readMaskData(channels) {
if (this.hasMask) {
let val;
const maskPixels = this.layer.mask.width * this.layer.mask.height;
const offset = this.channelLength * channels.length;
if (this.layer.mask.isSingleChannel) {
val = new Uint8Array(this.channelData.buffer, offset, maskPixels);
this.maskData.set(val);
return;
}
let i = 0;
while (i < maskPixels) {
val = this.channelData[i + offset];
this.maskData.set([0, 0, 0, val], i * 4);
i++;
}
}
}
setCmykChannels() {
this.channelsInfo = [
{
id: 0
},
{
id: 1
},
{
id: 2
},
{
id: 3
}
];
if (this.channels() === 5) {
return this.channelsInfo.push({
id: -1
});
}
}
combineCmykChannel() {
const cmykChannels = this.channelsInfo.map((ch) => ch.id).filter((ch) => ch >= -1);
for (let i = 0; i < this.numPixels; i++) {
let c = 0;
let m = 0;
let y = 0;
let k = 0;
let a = 255;
const len = cmykChannels.length;
for (let j = 0; j < len; j++) {
const chan = cmykChannels[j];
const val = this.channelData[i + this.channelLength * j];
switch (chan) {
case -1:
a = val;
break;
case 0:
c = val;
break;
case 1:
m = val;
break;
case 2:
y = val;
break;
case 3:
k = val;
}
}
const [r, g, b] = Color.cmykToRgb(255 - c, 255 - m, 255 - y, 255 - k);
this.pixelData.set([r, g, b, a], i * 4);
}
this.readMaskData(cmykChannels);
}
// Some helper methods that grab data from the PSD header.
width() {
return this.header.width;
}
height() {
return this.header.height;
}
channels() {
return this.header.channels;
}
depth() {
return this.header.depth;
}
mode() {
return this.header.mode;
}
}
function includes(target, ext) {
Object.keys(ext).forEach((key) => {
target.prototype[key] = ext[key];
});
}
includes(Image, ExportPNG);
function getAllObjectKeys(obj) {
const methods = /* @__PURE__ */ new Set();
while (obj && obj.constructor !== Object) {
const keys = Reflect.ownKeys(obj);
for (const key of keys) {
methods.add(key);
}
obj = Reflect.getPrototypeOf(obj);
}
return [...methods];
}
class LazyExecute {
constructor(obj, file) {
this.obj = obj;
this.file = file;
this.startPos = this.file.tell();
this.loaded = false;
this.loadMethod = null;
this.loadArgs = [];
this.passthru = [];
}
// This describes the method that we want to run at object instantiation. Typically, this
// will skip over the data that we will parse on-demand later. We can pass any arguments
// we want to the method as well.
now(method, ...args) {
this.obj[method].apply(this.obj, args);
return this;
}
// Here we describe the method we want to run when the first method/property on the object
// is accessed. We can also define any arguments that need to be passed to the function.
later(method, ...args) {
this.loadMethod = method;
this.loadArgs = args;
return this;
}
// Sometimes we don't have to parse the data in order to get some important information.
// For example, we can get the width/height from the full preview image without parsing the
// image itself, since that data comes from the header. Purely convenience, but helps to
// optimize usage.
// The arguments are a list of method/property names we don't want to trigger on-demand parsing.
ignore(...args) {
this.passthru.concat(args);
return this;
}
// This is called once all the parameters of the proxy have been set up, i.e. now, later, and skip.
// This defines all items on the proxies objects prototype on this object, and checks to make sure
// the proxies object has been loaded before passing on the call.
get() {
const keys = getAllObjectKeys(this.obj);
for (const key of keys) {
if (!this[key]) {
Object.defineProperty(this, key, {
get: function() {
if (!this.loaded && !this.passthru.includes(key)) {
this.load();
}
return this.obj[key];
}
});
}
}
return this;
}
// If we are accessing a property for the first time, then this will call the load method, which
// was defined during setup with `later()`. The steps this performs are:
// 1. Records the current file position.
// 2. Jumps to the recorded start position for the proxies' data.
// 3. Calls the load method, which was defined with `later()`.
// 4. Jumps back to the original file position.
// 5. Sets the `@loaded` flag to true, so we know this object has been parsed.
load() {
const origPos = this.file.tell();
this.file.seek(this.startPos);
this.obj[this.loadMethod].apply(this.obj, this.loadArgs);
this.file.seek(origPos);
this.loaded = true;
}
}
class Descriptor {
// Creates a new Descriptor.
constructor(file) {
this.file = file;
this.data = {};
}
// Parses the Descriptor at the current location in the file.
parse() {
this.data.class = this.parseClass();
const numItems = this.file.readInt();
for (let i = 0; i < numItems; i++) {
const [id, value] = this.parseKeyItem();
this.data[id] = value;
}
return this.data;
}
// ## Note
// The rest of the methods in this class are considered private. You will never
// call any of them from outside this class.
// Parses a class representation, which consists of a name and a unique ID.
parseClass() {
return {
name: this.file.readUnicodeString(),
id: this.parseId()
};
}
// Parses an ID, which is a unique String.
parseId() {
const len = this.file.readInt();
if (len === 0) {
return this.file.readString(4);
} else {
return this.file.readString(len);
}
}
// Parses a key/item value, which consists of an ID and an Item of any type.
parseKeyItem() {
const id = this.parseId();
const value = this.parseItem();
return [id, value];
}
// Parses an Item, which can be one of many types of data, depending on the key.
parseItem(type = null) {
if (type == null) {
type = this.file.readString(4);
}
switch (type) {
case "bool":
return this.parseBoolean();
case "type":
case "GlbC":
return this.parseClass();
case "Objc":
case "GlbO":
return new Descriptor(this.file).parse();
case "doub":
return this.parseDouble();
case "enum":
return this.parseEnum();
case "alis":
return this.parseAlias();
case "Pth":
return this.parseFilePath();
case "long":
return this.parseInteger();
case "comp":
return this.parseLargeInteger();
case "VlLs":
return this.parseList();
case "ObAr":
return this.parseObjectArray();
case "tdta":
return this.parseRawData();
case "obj ":
return this.parseReference();
case "TEXT":
return this.file.readUnicodeString();
case "UntF":
return this.parseUnitDouble();
case "UnFl":
return this.parseUnitFloat();
case "uglg":
return this.parseBoolean();
}
}
parseBoolean() {
return this.file.readBoolean();
}
parseDouble() {
return this.file.readDouble();
}
parseInteger() {
return this.file.readInt();
}
parseLargeInteger() {
return this.file.readLongLong();
}
parseIdentifier() {
return this.file.readInt();
}
parseIndex() {
return this.file.readInt();
}
parseOffset() {
return this.file.readInt();
}
// Parses a Property, which consists of a class and a unique ID.
parseProperty() {
return {
class: this.parseClass(),
id: this.parseId()
};
}
// Parses an enumerator, which consists of 2 IDs, one of which is
// the type, and the other is the value.
parseEnum() {
return {
type: this.parseId(),
value: this.parseId()
};
}
// Parses an enumerator reference, which consists of a class and
// 2 IDs: a type and value.
parseEnumReference() {
return {
class: this.parseClass(),
type: this.parseId(),
value: this.parseId()
};
}
// Parses an Alias, which is a string of arbitrary length.
parseAlias() {
const len = this.file.readInt();
return this.file.readString(len);
}
// Parses a file path, which consists of a 4 character signature
// and a path.
parseFilePath() {
this.file.readInt();
const sig = this.file.readString(4);
this.file.read("<i");
const numChars = this.file.read("<i");
const path = this.file.readUnicodeString(numChars);
return {
sig,
path
};
}
// Parses a list/array of Items.
parseList() {
const count = this.file.readInt();
const items = [];
for (let i = 0; i < count; i++) {
items.push(this.parseItem());
}
return items;
}
// Not documented anywhere and unsure of the data format. Luckily, this
// type is extremely rare. In fact, it's so rare, that I've never run into it
// among any of my PSDs.
parseObjectArray() {
throw `Descriptor object array not implemented yet @ ${this.file.tell()}`;
}
// Parses raw byte data of arbitrary length.
parseRawData() {
const len = this.file.readInt();
return this.file.read(len);
}
_getReferenceValue(type) {
switch (type) {
case "prop":
return this.parseProperty();
case "Clss":
return this.parseClass();
case "Enmr":
return this.parseEnumReference();
case "Idnt":
return this.parseIdentifier();
case "indx":
return this.parseIndex();
case "name":
return this.file.readUnicodeString();
case "rele":
return this.parseOffset();
}
}
// Parses a Reference, which is an array of items of multiple types.
parseReference() {
const numItems = this.file.readInt();
const items = [];
for (let i = 0; i < numItems; i++) {
const type = this.file.readString(4);
const value = this._getReferenceValue(type);
items.push({
type,
value
});
}
return items;
}
// Parses a double with a unit, such as angle, percent, pixels, etc.
// Returns an object with an ID, a unit, and a value.
parseUnitDouble() {
const unitId = this.file.readString(4);
const unit = function() {
switch (unitId) {
case "#Ang":
return "Angle";
case "#Rsl":
return "Density";
case "#Rlt":
return "Distance";
case "#Nne":
return "None";
case "#Prc":
return "Percent";
case "#Pxl":
return "Pixels";
case "#Mlm":
return "Millimeters";
case "#Pnt":
return "Points";
}
}();
const value = this.file.readDouble();
return {
id: unitId,
unit,
value
};
}
// Parses a float with a unit, such as angle, percent, pixels, etc.
// Returns an object with an ID, a unit, and a value.
parseUnitFloat() {
const unitId = this.file.readString(4);
const unit = function() {
switch (unitId) {
case "#Ang":
return "Angle";
case "#Rsl":
return "Density";
case "#Rlt":
return "Distance";
case "#Nne":
return "None";
case "#Prc":
return "Percent";
case "#Pxl":
return "Pixels";
case "#Mlm":
return "Millimeters";
case "#Pnt":
return "Points";
}
}();
const value = this.file.readFloat();
return {
id: unitId,
unit,
value
};
}
}
class LayerInfoBase {
constructor(layer, length) {
this.layer = layer;
this.length = length;
this.file = layer.file;
this.section_end = this.file.tell() + length;
this.data = {};
}
skip() {
this.file.seek(this.section_end);
}
parse() {
this.skip();
}
}
class Artboard extends LayerInfoBase {
static shouldParse(key) {
return key === "artb";
}
parse() {
this.file.seek(4, true);
this.data = new Descriptor(this.file).parse();
return this.data;
}
export() {
return {
coords: {
left: this.data.artboardRect["Left"],
top: this.data.artboardRect["Top "],
right: this.data.artboardRect["Rght"],
bottom: this.data.artboardRect["Btom"]
}
};
}
}
class BlendClippingElements extends LayerInfoBase {
static shouldParse(key) {
return key === "clbl";
}
parse() {
this.enabled = this.file.readBoolean();
this.file.seek(3, true);
}
}
class BlendInteriorElements extends LayerInfoBase {
static shouldParse(key) {
return key === "infx";
}
parse() {
this.enabled = this.file.readBoolean();
this.file.seek(3, true);
}
}
class FillOpacity extends LayerInfoBase {
static shouldParse(key) {
return key === "iOpa";
}
parse() {
const value = this.file.readByte();
const opacity = Math.round(value * 100 / 255);
const unit = "Percent";
this.data.opacity = opacity;
this.data.unit = unit;
}
}
class GradientFill extends LayerInfoBase {
static shouldParse(key) {
return key === "GdFl";
}
parse() {
this.file.seek(4, true);
this.data = new Descriptor(this.file).parse();
return this.data;
}
}
class LayerId extends LayerInfoBase {
constructor(layer, length) {
super(layer, length);
this.pos = this.file.tell();
}
static shouldParse(key) {
return key === "lyid";
}
parse() {
const cur = this.file.tell();
this.file.seek(this.pos);
this.id = this.file.readInt();
this.file.seek(cur);
return this.id;
}
}
class LayerNameSource extends LayerInfoBase {
static shouldParse(key) {
return key === "lnsr";
}
parse() {
this.id = this.file.readString(4);
return this.id;
}
}
const utf16Decoder = new TextDecoder("utf-16be");
const MATCH_TYPE = [
hashStart,
hashEnd,
multiLineArrayStart,
multiLineArrayEnd,
property,
propertyWithData,
singleLineArray,
boolean,
number,
numberWithDecimal,
string
];
let nodeStack = [];
let propertyStack = [];
let currentNode = [];
const engineDataParser = function(engineData) {
nodeStack = propertyStack = currentNode = [];
textReg(textSegment(codeToString(engineData)));
return currentNode.shift();
};
function codeToString(engineData) {
return String.fromCharCode.apply(null, engineData);
}
function textSegment(text) {
return text.split("\n");
}
function textReg(textArr) {
textArr.map(function(currentText) {
typeMatch(currentText.replace(/^\t+/g, ""));
});
}
function typeMatch(currentText) {
for (const currentType in MATCH_TYPE) {
const t = new MATCH_TYPE[currentType](currentText);
if (t.match) {
return t.parse();
}
}
return currentText;
}
function Match(reg, text) {
return reg.test(text);
}
function isArray(o) {
return Object.prototype.toString.call(o) === "[object Array]";
}
function hashStart(text) {
const reg = /^<<$/;
return {
match: Match(reg, text),
parse: function() {
stackPush({});
}
};
}
function hashEnd(text) {
const reg = /^>>$/;
return {
match: Match(reg, text),
parse: function() {
updateNode();
}
};
}
function multiLineArrayStart(text) {
const reg = /^\/(\w+) \[$/;
return {
match: Match(reg, text),
parse: function() {
propertyStack.push(text.match(reg)[1]);
stackPush([]);
}
};
}
function multiLineArrayEnd(text) {
const reg = /^]$/;
return {
match: Match(reg, text),
parse: function() {
updateNode();
}
};
}
function property(text) {
const reg = /^\/([_A-Z0-9]+)$/i;
return {
match: Match(reg, text),
parse: function() {
propertyStack.push(text.match(reg)[1]);
}
};
}
function propertyWithData(text) {
const reg = /^\/([_A-Z0-9]+)\s((.|\r)*)$/i;
return {
match: Match(reg, text),
parse: function() {
const match = text.match(reg);
pushKeyValue(match[1], typeMatch(match[2]));
}
};
}
function boolean(text) {
const reg = /^(true|false)$/;
return {
match: Match(reg, text),
parse: function() {
return text === "true";
}
};
}
function number(text) {
const reg = /^-?\d+$/;
return {
match: Match(reg, text),
parse: function() {
return Number(text);
}
};
}
function numberWithDecimal(text) {
const reg = /^(-?\d*)\.(\d+)$/;
return {
match: Match(reg, text),
parse: function() {
return Number(text);
}
};
}
function singleLineArray(text) {
const reg = /^\[(.*)\]$/;
return {
match: Match(reg, text),
parse: function() {
const items = text.match(reg)[1].trim().split(" ");
const tempArr = [];
const len = items.length;
for (let i = 0; i < len; i++) {
tempArr.push(typeMatch(items[i]));
}
return tempArr;
}
};
}
function string(text) {
const reg = /^\(((.|\r)*)\)$/;
return {
match: Match(reg, text),
parse: function() {
const txt = text.match(reg)[1];
const bf = [];
const len = txt.length;
for (let i = 0; i < len; i++) {
bf.push(txt.charCodeAt(i));
}
return utf16Decoder.decode(new Uint8Array(bf));
}
};
}
function stackPush(node) {
nodeStack.push(currentNode);
currentNode = node;
}
function updateNode() {
const node = nodeStack.pop();
if (isArray(node)) {
node.push(currentNode);
} else {
node[propertyStack.pop()] = currentNode;
}
currentNode = node;
}
function pushKeyValue(key, value) {
currentNode[key] = value;
}
let COORDS_VALUE;
let TRANSFORM_VALUE;
class TextElements extends LayerInfoBase {
static shouldParse(key) {
return key === "TySh";
}
constructor(layer, length) {
super(layer, length);
this.version = null;
this.transform = {};
this.textVersion = null;
this.descriptorVersion = null;
this.textData = null;
this.engineData = null;
this.textValue = null;
this.warpVersion = null;
this.descriptorVersion = null;
this.warpData = null;
this.coords = {};
}
parse() {
this.version = this.file.readShort();
this.parseTransformInfo();
this.textVersion = this.file.readShort();
this.descriptorVersion = this.file.readInt();
this.textData = new Descriptor(this.file).parse();
this.textValue = this.textData["Txt "];
this.engineData = engineDataParser(this.textData.EngineData);
this.warpVersion = this.file.readShort();
this.descriptorVersion = this.file.readInt();
this.warpData = new Descriptor(this.file).parse();
const len = COORDS_VALUE.length;
for (let i = 0; i < len; i++) {
const name = COORDS_VALUE[i];
this.coords[name] = this.file.readInt();
}
}
parseTransformInfo() {
const len = TRANSFORM_VALUE.length;
for (let index = 0; index < len; index++) {
const name = TRANSFORM_VALUE[index];
this.transform[name] = this.file.readDouble();
}
}
fonts() {
if (this.engineData == null) {
return [];
}
return this.engineData.ResourceDict.FontSet.map(function(f) {
return f.Name;
});
}
lengthArray() {
const arr = this.engineData.EngineDict.StyleRun.RunLengthArray;
const sum = arr.reduce((m, o) => m + o);
if (sum - this.textValue.length === 1) {
arr[arr.length - 1] = arr[arr.length - 1] - 1;
}
return arr;
}
fontStyles() {
const data = this.engineData.EngineDict.StyleRun.RunArray.map(function(r) {
return r.StyleSheet.StyleSheetData;
});
return data.map(function(f) {
let style;
if (f.FauxItalic) {
style = "italic";
} else {
style = "normal";
}
return style;
});
}
fontWeights() {
const data = this.engineData.EngineDict.StyleRun.RunArray.map(function(r) {
return r.StyleSheet.StyleSheetData;
});
return data.map(function(f) {
let weight;
if (f.FauxBold) {
weight = "bold";
} else {
weight = "normal";
}
return weight;
});
}
textDecoration() {
const data = this.engineData.EngineDict.StyleRun.RunArray.map(function(r) {
return r.StyleSheet.StyleSheetData;
});
return data.map(function(f) {
let decoration;
if (f.Underline) {
decoration = "underline";
} else {
decoration = "none";
}
return decoration;
});
}
leading() {
const data = this.engineData.EngineDict.StyleRun.RunArray.map(function(r) {
return r.StyleSheet.StyleSheetData;
});
return data.map(function(f) {
let leading;
if (f.Leading) {
leading = f.Leading;
} else {
leading = "auto";
}
return leading;
});
}
sizes() {
if (this.engineData == null && this.styles().FontSize == null) {
return [];
}
return this.styles().FontSize;
}
alignment() {
if (this.engineData == null) {
return [];
}
const alignments = ["left", "right", "center", "justify"];
return this.engineData.EngineDict.ParagraphRun.RunArray.map(function(s) {
return alignments[Math.min(parseInt(s.ParagraphSheet.Properties.Justification, 10), 3)];
});
}
// Return all colors used for text in this layer. The colors are returned in RGBA
// format as an array of arrays.
colors() {
if (this.engineData == null || this.styles().FillColor == null) {
return [[0, 0, 0, 255]];
}
return this.styles().FillColor.map(function(s) {
const values = s.Values.map(function(v) {
return Math.round(v * 255);
});
values.push(values.shift());
return values;
});
}
fillTypes() {
if (this.styles().FillColor) {
return this.styles().FillColor.map(function(s) {
return s.Type;
});
} else {
return 1;
}
}
styles() {
if (this.engineData == null) {
return {};
}
if (this._styles != null) {
return this._styles;
}
const data = this.engineData.EngineDict.StyleRun.RunArray.map(function(r) {
return r.StyleSheet.StyleSheetData;
});
return this._styles = data.reduce(function(m, o) {
for (const k in o) {
if (!o.hasOwnProperty(k)) {
continue;
}
const v = o[k];
m[k] || (m[k] = []);
m[k].push(v);
}
return m;
}, {});
}
// Creates the CSS string and returns it. Each property is newline separated
// and not all properties may be present depending on the document.
// Colors are returned in rgba() format and fonts may include some internal
// Photoshop fonts.
toCSS() {
const definition = {
"font-family": this.fonts().join(", "),
"font-size": `${this.sizes()[0]}pt`,
"color": `rgba(${this.colors()[0].join(", ")})`,
"text-align": this.alignment()[0]
};
const css = [];
for (const k in definition) {
const v = definition[k];
if (v == null) {
continue;
}
css.push(`${k}: ${v};`);
}
return css.join("\n");
}
export() {
return {
value: this.textValue,
font: {
lengthArray: this.lengthArray(),
styles: this.fontStyles(),
weights: this.fontWeights(),
names: this.fonts(),
sizes: this.sizes(),
colors: this.colors(),
alignment: this.alignment(),
textDecoration: this.textDecoration(),
leading: this.leading()
},
left: this.coords.left,
top: this.coords.top,
right: this.coords.right,
bottom: this.coords.bottom,
transform: this.transform
};
}
}
TRANSFORM_VALUE = ["xx", "xy", "yx", "yy", "tx", "ty"];
COORDS_VALUE = ["left", "top", "right", "bottom"];
class LegacyTypeTool extends TextElements {
static shouldParse(key) {
return key === "tySh";
}
constructor(layer, length) {
super(layer, length);
this.transform = {};
this.faces = [];
this.styles = [];
this.lines = [];
this.type = 0;
this.scalingFactor = 0;
this.characterCount = 0;
this.horzPlace = 0;
this.vertPlace = 0;
this.selectStart = 0;
this.selectEnd = 0;
this.color = null;
this.antialias = null;
}
parse() {
this.file.seek(2, true);
this.parseTransformInfo();
this.file.seek(2, true);
const facesCount = this.file.readShort();
for (let i = 0; i < facesCount; i++) {
const face = {};
face.mark = this.file.readShort();
face.fontType = this.file.readInt();
face.fontName = this.file.readString(this.file.readByte());
face.fontFamilyName = this.file.readString(this.file.readByte());
face.fontStyleName = this.file.readString(this.file.readByte());
face.script = this.file.readShort();
face.numberAxesVector = this.file.readInt();
face.vector = [];
for (let j = 0; j < face.numberAxesVector; j++) {
face.vector.push(this.file.readInt());
}
this.faces.push(face);
}
const stylesCount = this.file.readShort();
for (let i = 0; i < stylesCount; i++) {
const style = {};
style.mark = this.file.readShort();
style.faceMark = this.file.readShort();
style.size = this.file.readInt();
style.tracking = this.file.readInt();
style.kerning = this.file.readInt();
style.leading = this.file.readInt();
style.baseShift = this.file.readInt();
style.autoKern = this.file.readBoolean();
this.file.seek(1, true);
style.rotate = this.file.readBoolean();
this.styles.push(style);
}
this.type = this.file.readShort();
this.scalingFactor = this.file.readInt();
this.characterCount = this.file.readInt();
this.horzPlace = this.file.readInt();
this.vertPlace = this.file.readInt();
this.selectStart = this.file.readInt();
this.selectEnd = this.file.readInt();
const linesCount = this.file.readShort();
for (let i = 0; i < linesCount; i++) {
const line = {};
line.charCount = this.file.readInt();
line.orientation = this.file.readShort();
line.alignment = this.file.readShort();
line.actualChar = this.file.readShort();
line.style = this.file.readShort();
this.lines.push(line);
}
this.color = this.file.readSpaceColor();
this.antialias = this.file.readBoolean();
}
}
class Locked extends LayerInfoBase {
static shouldParse(key) {
return key === "lspf";
}
constructor(layer, length) {
super(layer, length);
this.transparencyLocked = false;
this.compositeLocked = false;
this.positionLocked = false;
this.allLocked = false;
}
parse() {
const locked = this.file.readInt();
this.transparencyLocked = (locked & 1 << 0) > 0 || locked === -2147483648;
this.compositeLocked = (locked & 1 << 1) > 0 || locked === -2147483648;
this.positionLocked = (locked & 1 << 2) > 0 || locked === -2147483648;
this.allLocked = this.transparencyLocked && this.compositeLocked && this.positionLocked;
}
}
class Metadata extends LayerInfoBase {
static shouldParse(key) {
return key === "shmd";
}
parse() {
this.data = {};
const count = this.file.readInt();
for (let i = 0; i < count; i++) {
this.file.seek(4, true);
const key = this.file.readString(4);
this.file.readByte();
this.file.seek(3, true);
const len = this.file.readInt();
const end = this.file.tell() + len;
if (["cust", "cmls", "extn", "mlst", "tmln"].includes(key)) {
let padNum = this.file.readInt();
if (padNum !== 16) {
padNum = this.file.readInt();
}
if (padNum === 16) {
this.data[key] = new Descriptor(this.file).parse();
}
} else {
console.log("Unhandled meta key: ", key);
}
this.file.seek(end);
}
}
}
class UnicodeName extends LayerInfoBase {
static shouldParse(key) {
return key === "luni";
}
parse() {
const pos = this.file.tell();
this.data = this.file.readUnicodeString();
this.file.seek(pos + this.length);
return this;
}
}
class NestedSectionDivider extends LayerInfoBase {
static shouldParse(key) {
return key === "lsdk";
}
constructor(layer, length) {
super(layer, length);
this.isFolder = false;
this.isHidden = false;
}
parse() {
const code = this.file.readInt();
switch (code) {
case 1:
case 2:
return this.isFolder = true;
case 3:
return this.isHidden = true;
}
}
}
class ObjectEffects extends LayerInfoBase {
static shouldParse(key) {
return key === "lfx2" || key === "lmfx";
}
parse() {
this.file.seek(8, true);
this.data = new Descriptor(this.file).parse();
return this.data;
}
}
const SECTION_DIVIDER_TYPES = ["other", "open folder", "closed folder", "bounding section divider"];
class SectionDivider extends LayerInfoBase {
static shouldParse(key) {
return key === "lsct";
}
constructor(layer, length) {
super(layer, length);
this.isFolder = false;
this.isHidden = false;
this.layerType = null;
this.blendMode = null;
this.subType = null;
}
parse() {
const code = this.file.readInt();
this.layerType = SECTION_DIVIDER_TYPES[code];
switch (code) {
case 1:
case 2:
this.isFolder = true;
break;
case 3:
this.isHidden = true;
}
if (!(this.length >= 12)) {
return;
}
this.file.seek(4, true);
this.blendMode = this.file.readString(4);
if (!(this.length >= 16)) {
return;
}
this.subType = this.file.readInt() === 0 ? "normal" : "scene group";
}
}
class PathRecord {
constructor(file) {
this.file = file;
this.recordType = null;
}
parse() {
this.recordType = this.file.readShort();
switch (this.recordType) {
case 0:
case 3:
this._readPathRecord();
break;
case 1:
case 2:
case 4:
case 5:
this._readBezierPoint();
break;
case 7:
this._readClipboardRecord();
break;
case 8:
this._readInitialFill();
break;
default:
this.file.seek(24, true);
}
}
export() {
const getRecordByType = () => {
switch (this.recordType) {
case 0:
case 3:
return {
numPoints: this.numPoints,
operation: this.operation
};
case 1:
case 2:
case 4:
case 5:
return {
linked: this.linked,
closed: this.recordType === 1 || this.recordType === 2,
preceding: {
vert: this.precedingVert,
horiz: this.precedingHoriz
},
anchor: {
vert: this.anchorVert,
horiz: this.anchorHoriz
},
leaving: {
vert: this.leavingVert,
horiz: this.leavingHoriz
}
};
case 7:
return {
clipboard: {
top: this.clipboardTop,
left: this.clipboardLeft,
bottom: this.clipboardBottom,
right: this.clipboardRight,
resolution: this.clipboardResolution
}
};
case 8:
return {
initialFill: this.initialFill
};
default:
return {};
}
};
const record = getRecordByType();
record.recordType = this.recordType;
return record;
}
isBezierPoint() {
const t = this.recordType;
return t === 1 || t === 2 || t === 4 || t === 5;
}
_readPathRecord() {
this.numPoints = this.file.readShort();
this.operation = this.file.readShort();
this.file.seek(20, true);
}
_readBezierPoint() {
const type = this.recordType;
this.linked = type === 1 || type === 4;
this.precedingVert = this.file.readPathNumber();
this.precedingHoriz = this.file.readPathNumber();
this.anchorVert = this.file.readPathNumber();
this.anchorHoriz = this.file.readPathNumber();
this.leavingVert = this.file.readPathNumber();
this.leavingHoriz = this.file.readPathNumber();
}
_readClipboardRecord() {
this.clipboardTop = this.file.readPathNumber();
this.clipboardLeft = this.file.readPathNumber();
this.clipboardBottom = this.file.readPathNumber();
this.clipboardRight = this.file.readPathNumber();
this.clipboardResolution = this.file.readPathNumber();
this.file.seek(4, true);
}
_readInitialFill() {
this.initialFill = this.file.readShort();
this.file.seek(22, true);
}
}
class VectorMask extends LayerInfoBase {
static shouldParse(key) {
return key === "vmsk" || key === "vsms";
}
constructor(layer, length) {
super(layer, length);
this.invert = null;
this.notLink = null;
this.disable = null;
this.paths = [];
}
parse() {
this.file.seek(4, true);
const tag = this.file.readInt();
this.invert = (tag & 1) > 0;
this.notLink = (tag & 1 << 1) > 0;
this.disable = (tag & 1 << 2) > 0;
const numRecords = (this.length - 10) / 26;
for (let i = 0; i < numRecords; i++) {
const record = new PathRecord(this.file);
record.parse();
this.paths.push(record);
}
}
export() {
return {
invert: this.invert,
notLink: this.notLink,
disable: this.disable,
paths: this.paths.map((p) => p.export())
};
}
}
class VectorOrigination extends LayerInfoBase {
static shouldParse(key) {
return key === "vogk";
}
parse() {
this.file.seek(8, true);
this.data = new Descriptor(this.file).parse();
return this.data;
}
}
class VectorStroke extends LayerInfoBase {
static shouldParse(key) {
return key === "vstk";
}
parse() {
this.file.seek(4, true);
this.data = new Descriptor(this.file).parse();
return this.data;
}
}
class VectorStrokeContent extends LayerInfoBase {
static shouldParse(key) {
return key === "vscg";
}
parse() {
this.file.seek(8, true);
this.data = new Descriptor(this.file).parse();
return this.data;
}
}
class PlacedLayer extends LayerInfoBase {
static shouldParse(key) {
return key === "PlLd";
}
constructor(layer, length) {
super(layer, length);
this.Trnf = [];
}
parse() {
this.identifier = this.file.readString(this.file.readByte());
this.version = this.file.readInt();
const len = Util.pad2(this.file.readByte());
this.Idnt = this.file.readString(len);
this.PgNm = this.file.parseInt();
this.totalPages = this.file.parseInt();
this.Annt = this.file.readInt();
this.Type = this.file.readInt();
this.parseTransformInfo();
this.warpValue = this.file.readInt();
this.file.seek(4, true);
this.warpData = new Descriptor(this.file).parse();
return this;
}
parseTransformInfo() {
const count = 8;
for (let i = 0; i < count; i++) {
this.Trnf.push(this.file.readDouble());
}
}
}
class LinkedLayer extends LayerInfoBase {
static shouldParse(key) {
return key === "lnk2";
}
constructor(layer, length) {
super(layer, length);
}
parse() {
const end = this.file.tell() + this.length;
this.files = [];
while (this.file.tell() < end) {
const obj = {};
this.file.seek(4, true);
const length = 1 + Util.pad4(this.file.readInt());
const fileEnd = length + this.file.tell();
const kind = this.file.readString(4);
const version = this.file.readInt();
obj.uuid = this.file.readString(this.file.readByte());
obj.fileName = this.file.readUnicodeString();
obj.fileType = this.file.readString(4);
obj.creator = this.file.readString(4);
this.file.seek(4, true);
obj.datasize = this.file.readInt();
obj.openFile = this.file.readBoolean();
if (obj.openFile === true) {
this.file.seek(4, true);
obj.openFile = new Descriptor(this.file).parse();
}
if (kind === "liFD") {
obj.fileData = this.file.read(obj.datasize);
}
if (version >= 5) {
obj.childId = this.file.readUnicodeString();
}
if (version >= 6) {
obj.modTime = this.file.readDouble();
}
if (version >= 7) {
obj.lockedState = this.file.readBoolean();
}
this.files.push(obj);
this.file.seek(fileEnd);
}
this.file.seek(end);
return this.files;
}
}
class SmartObject extends LayerInfoBase {
static shouldParse(key) {
return key === "SoLd";
}
constructor(layer, length) {
super(layer, length);
}
parse() {
this.identifier = this.file.readString(this.file.readByte());
this.version = this.file.readInt();
this.file.seek(4, true);
this.data = new Descriptor(this.file).parse();
return this;
}
}
class Exposure extends LayerInfoBase {
constructor(layer, length) {
super(layer, length);
this.pos = this.file.tell();
}
static shouldParse(key) {
return key === "expA";
}
parse() {
const cur = this.file.tell();
this.file.seek(this.pos);
this.version = this.file.readShort();
this.exposure = this.file.readFloat();
this.offset = this.file.readFloat();
this.gamma = this.file.readFloat();
this.file.seek(cur);
return {
version: this.version,
exposure: this.exposure,
offset: this.offset,
gamma: this.gamma
};
}
}
class Pattern extends LayerInfoBase {
static shouldParse(key) {
return key === "PtFl";
}
parse() {
this.data = "调节图层:PtFl";
}
}
class BrightnessContrast extends LayerInfoBase {
static shouldParse(key) {
return key === "brit";
}
parse() {
this.data = "调节图层:brit";
}
}
class Levels extends LayerInfoBase {
static shouldParse(key) {
return key === "levl";
}
parse() {
this.data = "调节图层:levl";
}
}
class Curves extends LayerInfoBase {
static shouldParse(key) {
return key === "curv";
}
parse() {
this.data = "调节图层:curv";
}
}
class Hue2 extends LayerInfoBase {
static shouldParse(key) {
return key === "hue2";
}
parse() {
this.data = "调节图层:hue2";
}
}
class ColorBalance extends LayerInfoBase {
static shouldParse(key) {
return key === "blnc";
}
parse() {
this.data = "调节图层:blnc";
}
}
class BlackWhite extends LayerInfoBase {
static shouldParse(key) {
return key === "blwh";
}
parse() {
this.data = "调节图层:blwh";
}
}
class PhotoFilter extends LayerInfoBase {
static shouldParse(key) {
return key === "phfl";
}
parse() {
this.data = "调节图层:phfl";
}
}
class ColorLookup extends LayerInfoBase {
static shouldParse(key) {
return key === "clrL";
}
parse() {
this.data = "调节图层:clrL";
}
}
class Invert extends LayerInfoBase {
static shouldParse(key) {
return key === "nvrt";
}
parse() {
this.data = "调节图层:nvrt";
}
}
class Threshold extends LayerInfoBase {
static shouldParse(key) {
return key === "thrs";
}
parse() {
this.data = "调节图层:thrs";
}
}
class GradientMap extends LayerInfoBase {
static shouldParse(key) {
return key === "grdm";
}
parse() {
this.data = "调节图层:grdm";
}
}
class SelectiveColor extends LayerInfoBase {
static shouldParse(key) {
return key === "selc";
}
parse() {
this.data = "调节图层:selc";
}
}
class SolidColor extends LayerInfoBase {
static shouldParse(key) {
return key === "SoCo";
}
constructor(layer, length) {
super(layer, length);
this.r = this.g = this.b = 0;
}
parse() {
this.file.seek(4, true);
this.data = new Descriptor(this.file).parse();
this.r = Math.round(this.colorData()["Rd "]);
this.g = Math.round(this.colorData()["Grn "]);
this.b = Math.round(this.colorData()["Bl "]);
}
colorData() {
return this.data["Clr "];
}
color() {
return [this.r, this.g, this.b];
}
}
let ChannelMixer$1 = class ChannelMixer extends LayerInfoBase {
static shouldParse(key) {
return key === "mixr";
}
parse() {
this.data = "调节图层:mixr";
}
};
class ChannelMixer2 extends LayerInfoBase {
static shouldParse(key) {
return key === "post";
}
parse() {
this.file.seek(4, true);
this.data = "调节图层:post";
}
}
class Vibrance extends LayerInfoBase {
static shouldParse(key) {
return key === "vibA";
}
parse() {
this.data = "调节图层:vibA";
}
}
const LAYER_INFO = {
artboard: Artboard,
blendClippingElements: BlendClippingElements,
blendInteriorElements: BlendInteriorElements,
fillOpacity: FillOpacity,
gradientFill: GradientFill,
layerId: LayerId,
layerNameSource: LayerNameSource,
legacyTypetool: LegacyTypeTool,
locked: Locked,
metadata: Metadata,
name: UnicodeName,
nestedSectionDivider: NestedSectionDivider,
objectEffects: ObjectEffects,
sectionDivider: SectionDivider,
typeTool: TextElements,
vectorMask: VectorMask,
vectorOrigination: VectorOrigination,
vectorStroke: VectorStroke,
vectorStrokeContent: VectorStrokeContent,
placedLayer: PlacedLayer,
linkedLayer: LinkedLayer,
smartObject: SmartObject,
exposure: Exposure,
pattern: Pattern,
brightnessContrast: BrightnessContrast,
levels: Levels,
curves: Curves,
hue2: Hue2,
colorBalance: ColorBalance,
blackWhite: BlackWhite,
photoFilter: PhotoFilter,
colorLookup: ColorLookup,
invert: Invert,
threshold: Threshold,
gradientMap: GradientMap,
selectiveColor: SelectiveColor,
solidColor: SolidColor,
channelMixer: ChannelMixer$1,
posterize: ChannelMixer2,
vibrance: Vibrance
};
const INFO_BYTES_8 = ["LMsk", "Lr16", "Lr32", "Layr", "Mt16", "Mt32", "Mtrn", "Alph", "FMsk", "Ink2", "FEid", "FXid", "PxSD"];
function parseLayerInfo() {
while (this.file.tell() < this.layerEnd) {
this.file.seek(4, true);
const key = this.file.readString(4);
let length;
if (this.header.version === 2 && INFO_BYTES_8.includes(key)) {
length = Util.pad2(this.file.readLongLong());
} else {
length = Util.pad2(this.file.readInt());
}
this.file.tell();
if (length <= 0) {
continue;
}
let keyParseable = false;
for (const layerInfoKey in LAYER_INFO) {
const LayerInfoConstructor = LAYER_INFO[layerInfoKey];
if (!LayerInfoConstructor.shouldParse(key)) {
continue;
}
const layerInfoInstance = new LayerInfoConstructor(this, length);
if (this.externalFiles && key === "lnk2") {
this.externalFiles.push(layerInfoInstance.parse());
break;
}
this.adjustments[layerInfoKey] = new LazyExecute(layerInfoInstance, this.file).now("skip").later("parse").get();
if (this[layerInfoKey] == null) {
this[layerInfoKey] = () => {
return this.adjustments[layerInfoKey];
};
}
this.infoKeys.push(key);
keyParseable = true;
break;
}
if (!keyParseable) {
this.file.seek(length, true);
}
}
}
const BLEND_MODES = {
norm: "normal",
dark: "darken",
lite: "lighten",
hue: "hue",
sat: "saturation",
colr: "color",
lum: "luminosity",
mul: "multiply",
scrn: "screen",
diss: "dissolve",
over: "overlay",
hLit: "hard_light",
sLit: "soft_light",
diff: "difference",
smud: "exclusion",
div: "color_dodge",
idiv: "color_burn",
lbrn: "linear_burn",
lddg: "linear_dodge",
vLit: "vivid_light",
lLit: "linear_light",
pLit: "pin_light",
hMix: "hard_mix",
pass: "passthru",
dkCl: "darker_color",
lgCl: "lighter_color",
fsub: "subtract",
fdiv: "divide"
};
class BlendMode {
constructor(file) {
this.file = file;
this.blendKey = null;
this.opacity = null;
this.clipping = null;
this.clipped = null;
this.flags = null;
this.blendingMode = null;
this.visible = null;
}
// Parses the blend mode data.
parse() {
this.file.seek(4, true);
this.blendKey = this.file.readString(4).trim();
this.opacity = this.file.readByte();
this.clipping = this.file.readByte();
this.flags = this.file.readByte();
this.blendingMode = BLEND_MODES[this.blendKey];
this.clipped = this.clipping === 1;
this.visible = !((this.flags & 1 << 1) > 0);
this.file.seek(1, true);
}
get mode() {
console.warn("mode is deprecated, use blendingMode instead.");
return this.blendingMode;
}
// Returns the layer opacity as a percentage.
opacityPercentage() {
return this.opacity * 100 / 255;
}
}
class Mask {
constructor(file, hasRealUserSuppliedLayerMask) {
this.file = file;
this.hasRealUserSuppliedLayerMask = hasRealUserSuppliedLayerMask;
this.top = 0;
this.right = 0;
this.bottom = 0;
this.left = 0;
}
parse() {
this.size = this.file.readInt();
if (this.size === 0) {
return this;
}
const maskEnd = this.file.tell() + this.size;
this.top = this.file.readInt();
this.left = this.file.readInt();
this.bottom = this.file.readInt();
this.right = this.file.readInt();
this.defaultColor = this.file.readByte();
this.flags = this.file.readByte();
this.relative = (this.flags >> 0 & 1) === 0;
this.isEnabled = (this.flags >> 1 & 1) === 0;
this.disabled = !this.isEnabled;
this.invert = (this.flags >> 3 & 1) === 1;
this.hasParam = this.flags >> 4 & 1;
this.external = this.hasParam;
if (this.hasParam) {
let userMaskDensity = 255;
let userMaskFeather = 0;
let vectorMaskDensity = 255;
let vectorMaskFeather = 0;
if (this.hasRealUserSuppliedLayerMask) {
this.readRealUserSuppliedLayerMask();
}
const paramStart = this.file.tell();
const maskParamBitFlags = this.file.readByte();
if (maskParamBitFlags >> 0 & 1) {
userMaskDensity = this.file.readByte();
}
if (maskParamBitFlags >> 1 & 1) {
userMaskFeather = this.file.readDouble();
}
if (maskParamBitFlags >> 2 & 1) {
vectorMaskDensity = this.file.readByte();
}
if (maskParamBitFlags >> 3 & 1) {
vectorMaskFeather = this.file.readDouble();
}
if ((this.file.tell() - paramStart & 1) === 1) {
this.file.readByte();
}
this.maskParameters = [userMaskDensity, userMaskFeather, vectorMaskDensity, vectorMaskFeather];
} else {
if (this.size === 20) {
this.file.seek(2, true);
} else {
this.readRealUserSuppliedLayerMask();
}
}
this.width = this.right - this.left;
this.height = this.bottom - this.top;
this.file.seek(maskEnd);
return this;
}
readRealUserSuppliedLayerMask() {
const flags = this.file.readByte();
const color = this.file.readByte();
const top = this.file.readInt();
const left = this.file.readInt();
const bottom = this.file.readInt();
const right = this.file.readInt();
const width = right - left;
const height = bottom - top;
this.vmask = {
// bit 0 = position relative to layer
relative: (flags >> 0 & 1) === 0,
// bit 1 = layer mask disabled
isEnabled: (flags >> 1 & 1) === 0,
// bit 3 = indicates that the user mask actually came from rendering other data
invert: (flags >> 3 & 1) === 1,
// bit 4 = indicates that the user and/or vector masks have parameters applied to them
hasParam: flags >> 4 & 1,
color,
top,
left,
bottom,
right,
width,
height
};
}
export() {
if (this.size === 0) {
return {};
}
return {
top: this.top,
left: this.left,
bottom: this.bottom,
right: this.right,
width: this.width,
height: this.height,
defaultColor: this.defaultColor,
relative: this.relative,
disabled: this.disabled,
invert: this.invert,
padding: this.padding,
external: this.external
};
}
}
class ChannelImage extends Image {
// Creates a new ChannelImage.
constructor(file, header, layer) {
super(file, header, layer);
this.layer = layer;
this._width = this.layer.width;
this._height = this.layer.height;
this.width = function() {
return this._width;
};
this.height = function() {
return this._height;
};
this.channelsInfo = this.layer.channelsInfo;
this.hasMask = this.channelsInfo.some((c) => c.id < -1);
this.opacity = this.layer.opacity / 255;
}
// Skip parsing this image by jumping to the end of the data.
skip() {
const channelsInfo = this.channelsInfo;
const len = channelsInfo.length;
for (let i = 0; i < len; i++) {
const chan = channelsInfo[i];
this.file.seek(chan.length, true);
}
}
// The width of the image.
// width() {
// return this._width;
// }
// The height of the image.
// height() {
// return this._height;
// }
// The number of color channels in the image.
channels() {
return this.layer.channels;
}
// Parse the image data. The resulting image data will be formatted to match the Javascript
// Canvas color format, e.g. `[R, G, B, A, R, G, B, A]`.
parse() {
var _a;
this.chanPos = 0;
const channelsInfo = this.channelsInfo;
const len = channelsInfo.length;
for (let i = 0; i < len; i++) {
const chan = channelsInfo[i];
if (chan.length <= 0) {
this.parseCompression();
continue;
}
this.chan = chan;
if (chan.id < -1) {
this._width = this.layer.mask.width;
this._height = this.layer.mask.height;
if (chan.id === -2) {
const { width, height } = this.layer.mask;
if (!width || !height) {
throw new Error("Incorrect layer mask size");
}
this._width = width;
this._height = height;
}
if (chan.id === -3) {
const { width, height } = (_a = this.layer.mask) == null ? void 0 : _a.vmask;
if (!width || !height) {
throw new Error("Incorrect vector mask size");
}
this._width = width;
this._height = height;
}
} else {
this._width = this.layer.width;
this._height = this.layer.height;
}
this.length = this._width * this._height;
const start = this.file.tell();
this.parseImageData();
const finish = this.file.tell();
if (finish !== start + this.chan.length) {
this.file.seek(start + this.chan.length);
}
}
this._width = this.layer.width;
this._height = this.layer.height;
this.processImageData();
}
// Initiates parsing of the image data, which is based on the compression type of the channel. Every
// channel defines its own compression type, unlike the full PSD preview, which has a single compression
// type for the entire image.
parseImageData() {
this.compression = this.parseCompression();
switch (this.compression) {
case 0:
this.parseRaw();
break;
case 1:
this.parseRLE();
break;
case 2:
case 3:
this.parseZip();
break;
default:
this.file.seek(this.endPos);
}
}
parseRaw() {
const chanEnd = this.chanPos + this.chan.length - 2;
for (let i = this.chanPos; i < chanEnd; i++) {
this.channelData[i] = this.file.readByte();
}
this.chanPos += this.chan.length - 2;
}
parseByteCounts() {
const height = this.height();
const byteCounts = [];
for (let i = 0; i < height; i++) {
if (this.header.version === 1) {
byteCounts.push(this.file.readShort());
} else {
byteCounts.push(this.file.readInt());
}
}
return byteCounts;
}
parseChannelData() {
this.lineIndex = 0;
this.decodeRLEChannel();
}
}
let Layer$1 = class Layer {
constructor(file, header) {
this.file = file;
this.header = header;
this.mask = {};
this.blendingRanges = {};
this.adjustments = {};
this.channelsInfo = [];
this.blendMode = {};
this.groupLayer = null;
this.infoKeys = [];
Object.defineProperty(this, "name", {
get: function() {
if (this.adjustments["name"] != null) {
return this.adjustments["name"].data;
} else {
return this.legacyName;
}
}
});
}
// Every layer starts with the same set of data, and ends with a dynamic
// number of layer info blocks.
parse() {
this.parsePositionAndChannels();
this.parseBlendModes();
const extraLen = this.file.readInt();
this.layerEnd = this.file.tell() + extraLen;
this.hasRealUserSuppliedLayerMask = this.channelsInfo.some((ch) => ch.id === -3);
this.parseMaskData();
this.parseBlendingRanges();
this.parseLegacyLayerName();
this.parseLayerInfo();
this.file.seek(this.layerEnd);
return this;
}
export() {
return {
name: this.name,
top: this.top,
right: this.right,
bottom: this.bottom,
left: this.left,
width: this.width,
height: this.height,
opacity: this.opacity,
visible: this.visible,
clipped: this.clipped,
mask: this.mask.export()
};
}
// Every layer starts with the basics. Here we have the layer dimensions,
// the number of color channels for the image data, and information about
// the color channels.
parsePositionAndChannels() {
this.top = this.file.readInt();
this.left = this.file.readInt();
this.bottom = this.file.readInt();
this.right = this.file.readInt();
this.channels = this.file.readShort();
this.rows = this.height = this.bottom - this.top;
this.cols = this.width = this.right - this.left;
for (let i = 0; i < this.channels; i++) {
const id = this.file.readShort();
let length;
if (this.header.version === 1) {
length = this.file.readInt();
} else {
length = this.file.readLongLong();
}
this.channelsInfo.push({
id,
length
});
}
}
// Every layer defines how it's blended with the rest of the document.
// This is represented in the Photoshop UI above the layer list as
// a drop-down. It also defines the layer opacity and whether it's a
// part of a clipping mask.
parseBlendModes() {
this.blendMode = new BlendMode(this.file);
this.blendMode.parse();
this.opacity = this.blendMode.opacity;
this.visible = this.blendMode.visible;
this.clipped = this.blendMode.clipped;
}
hidden() {
return !this.visible;
}
// TODO: check section divider
blendingMode() {
return this.blendMode.blendingMode;
}
// Every layer has a mask section, whether the layer actually
// has a mask defined. If there is no mask, then the mask size will be
// 0, and we'll move on to the next thing.
parseMaskData() {
this.mask = new Mask(this.file, this.hasRealUserSuppliedLayerMask);
this.mask.parse();
return this.mask;
}
// Blending ranges let you control which pixels from this layer and which
// pixels from the underlying layers appear in the final image. This describes
// the ranges in both greyscale and for each color channel.
parseBlendingRanges() {
const length = this.file.readInt();
if (length === 0) {
return;
}
this.blendingRanges.grey = {
source: {
black: [this.file.readByte(), this.file.readByte()],
white: [this.file.readByte(), this.file.readByte()]
},
dest: {
black: [this.file.readByte(), this.file.readByte()],
white: [this.file.readByte(), this.file.readByte()]
}
};
const numChannels = (length - 8) / 8;
this.blendingRanges.channels = [];
for (let i = 0; i < numChannels; i++) {
this.blendingRanges.channels.push({
source: {
black: [this.file.readByte(), this.file.readByte()],
white: [this.file.readByte(), this.file.readByte()]
},
dest: {
black: [this.file.readByte(), this.file.readByte()],
white: [this.file.readByte(), this.file.readByte()]
}
});
}
}
// Every Photoshop document has what we can consider to be the "legacy" name.
// This used to be the sole place that Photoshop stored the layer name, but once
// people started using fancy UTF-8 characters, they moved the layer name out into
// a layer info block. This stayed behind for compatibility reasons. The newer layer
// name is always preferred since it covers all possible characters (even emojis),
// while this has a much more limited character set.
parseLegacyLayerName() {
const len = Util.pad4(this.file.readByte());
this.legacyName = this.file.readString(len);
return this.legacyName;
}
parseLayerInfo() {
parseLayerInfo.call(this);
}
isFolder() {
if (this.adjustments["sectionDivider"] != null) {
return this.adjustments["sectionDivider"].isFolder;
} else if (this.adjustments["nestedSectionDivider"] != null) {
return this.adjustments["nestedSectionDivider"].isFolder;
} else {
return this.name === "<Layer group>";
}
}
isFolderEnd() {
if (this.adjustments["sectionDivider"] != null) {
return this.adjustments["sectionDivider"].isHidden;
} else if (this.adjustments["nestedSectionDivider"] != null) {
return this.adjustments["nestedSectionDivider"].isHidden;
} else {
return this.name === "</Layer group>";
}
}
parseChannelImage() {
const image = new ChannelImage(this.file, this.header, this);
this.image = new LazyExecute(image, this.file).now("skip").later("parse").get();
}
};
class UnicodePath extends LayerInfoBase {
static shouldParse(key) {
return key === "pths";
}
parse() {
this.file.seek(4);
this.data = new Descriptor(this.file).parse();
return this;
}
}
function isStringBoundary(code) {
return code === 9 || code === 10 || code === 32;
}
function readUnicodeStringEscape92(buff, pos, end) {
const arr = [];
let str = "";
while (pos < end) {
const code = buff[pos++];
if (code === 92) {
arr.push(buff[pos++]);
} else {
arr.push(code);
}
}
for (let i = 0; i < arr.length; i += 2) {
str += String.fromCharCode(arr[i] << 8 | arr[i + 1]);
}
return str;
}
function readString(buff, pos, len) {
let str = "";
for (let i = 0; i < len; i++) {
str += String.fromCharCode(buff[pos + i]);
}
return str;
}
function readObject(buff, target, pos) {
while (buff[pos] !== 60) {
pos++;
console.log("šipka");
}
pos += 2;
pos = parseKVMap(buff, target, pos);
return pos;
}
function readValueAsObject(buff, pos, key) {
const startPos = pos;
const valueObj = {
type: "",
size: 0,
value: 0
};
while (isStringBoundary(buff[pos])) {
pos++;
}
if (buff[pos] === 60) {
valueObj.type = "Object";
valueObj.value = {};
pos = readObject(buff, valueObj.value, pos);
} else if (buff[pos] === 40) {
valueObj.type = "String";
pos++;
if (buff[pos] === 41) {
valueObj.value = "e";
pos++;
} else {
pos += 2;
let end = pos;
while (true) {
if (buff[end] === 41 && buff[end - 1] !== 92) {
break;
} else {
end += 1;
}
}
valueObj.value = "s" + readUnicodeStringEscape92(buff, pos, end);
pos = end + 2;
}
} else if (buff[pos] === 91) {
pos++;
valueObj.value = [];
valueObj.type = "Array";
while (isStringBoundary(buff[pos])) {
pos++;
}
while (buff[pos] !== 93) {
const arrItem = readValueAsObject(buff, pos);
if (arrItem === -1) {
return -1;
}
valueObj.value.push(arrItem.value);
pos += arrItem.size;
delete arrItem.size;
while (isStringBoundary(buff[pos])) {
pos++;
}
}
pos++;
} else {
let valEndPos = pos;
while (!isStringBoundary(buff[valEndPos])) {
valEndPos++;
}
const stringValue = readString(buff, pos, valEndPos - pos);
const floatValue = parseFloat(stringValue);
if (!isNaN(floatValue) && stringValue.indexOf(".") !== -1) {
valueObj.type = "Float";
valueObj.value = "f" + parseFloat(stringValue);
} else if (!isNaN(floatValue) && stringValue.indexOf(".") === -1) {
valueObj.type = "Integer";
valueObj.value = "i" + parseInt(stringValue);
} else if (stringValue === "true" || stringValue === "false") {
valueObj.type = "Boolean";
valueObj.value = stringValue === "true";
} else if (stringValue.charAt(0) === "/") {
valueObj.type = "BString";
valueObj.value = stringValue;
} else if (stringValue === "NaN" || stringValue === "undefined") {
valueObj.type = "Float";
valueObj.value = "f0";
} else {
console.log("unknown value", JSON.stringify(stringValue));
throw "e";
}
pos = valEndPos + 1;
}
valueObj.size = pos - startPos;
return valueObj;
}
function parseKVMap(buff, target, pos) {
while (true) {
while (isStringBoundary(buff[pos]) || buff[pos] === 0) {
pos++;
}
if (pos >= buff.length) {
break;
}
if (buff[pos] === 47) {
pos++;
let keyEnd = pos;
while (!isStringBoundary(buff[keyEnd])) {
keyEnd++;
}
const key = readString(buff, pos, keyEnd - pos);
pos = keyEnd + 1;
const res = readValueAsObject(buff, pos);
target["_" + key] = res.value;
pos += res.size;
} else if (buff[pos] === 62) {
pos += 2;
break;
} else {
const char = buff[pos];
console.log(readString(buff, pos, pos + 100));
console.log("unknown byte: " + char + ", char: " + String.fromCharCode(char) + ", offset: " + pos);
pos++;
throw "e";
}
}
return pos;
}
function readGlobalEngineData(buff) {
const res = {};
parseKVMap(buff, res, 0);
return res;
}
const defaultColor = {
_Color: [0, {
_Type: [0],
_Values: [1]
}],
_CAIKnownStyleID: [5],
_StreamTag: [99]
};
const defaultTextStyle = {
_Font: [0],
_FontSize: [1],
_FauxBold: [2],
_FauxItalic: [3],
_AutoLeading: [4],
_Leading: [5],
_HorizontalScale: [6],
_VerticalScale: [7],
_Tracking: [8],
_BaselineShift: [9],
_CharacterRotation: [10],
_AutoKern: [11],
_FontCaps: [12],
_FontBaseline: [13],
_FontOTPosition: [14],
_StrikethroughPosition: [15],
_UnderlinePosition: [16],
_UnderlineOffset: [17],
_Ligatures: [18],
_DiscretionaryLigatures: [19],
_ContextualLigatures: [20],
_AlternateLigatures: [21],
_OldStyle: [22],
_Fractions: [23],
_Ordinals: [24],
_Swash: [25],
_Titling: [26],
_ConnectionForms: [27],
_StylisticAlternates: [28],
_Ornaments: [29],
_FigureStyle: [30],
_ProportionalMetrics: [31],
_Kana: [32],
_Italics: [33],
_Ruby: [34],
_BaselineDirection: [35],
_Tsume: [36],
_StyleRunAlignment: [37],
_Language: [38],
_JapaneseAlternateFeature: [39],
_EnableWariChu: [40],
_WariChuLineCount: [41],
_WariChuLineGap: [42],
_WariChuSubLineAmount: [43, {
_WariChuSubLineScale: [0]
}],
_WariChuWidowAmount: [44],
_WariChuOrphanAmount: [45],
_WariChuJustification: [46],
_TCYUpDownAdjustment: [47],
_TCYLeftRightAdjustment: [48],
_LeftAki: [49],
_RightAki: [50],
_JiDori: [51],
_NoBreak: [52],
_FillColor: [53, defaultColor],
_StrokeColor: [54, defaultColor],
_Blend: [55, {
_1: [1],
_3: [3],
_Knockout: [4],
_StreamTag: [99]
}],
_FillFlag: [56],
_StrokeFlag: [57],
_FillFirst: [58],
_FillOverPrint: [59],
_StrokeOverPrint: [60],
_LineCap: [61],
_LineJoin: [62],
_LineWidth: [63],
_MiterLimit: [64],
_LineDashOffset: [65],
_LineDashArray: [66],
_Type1EncodingNames: [67],
_Kashidas: [68],
_DirOverride: [69],
_DigitSet: [70],
_DiacVPos: [71],
_DiacXOffset: [72],
_DiacYOffset: [73],
_OverlapSwash: [74],
_JustificationAlternates: [75],
_StretchedAlternates: [76],
_FillVisibleFlag: [77],
_StrokeVisibleFlag: [78],
_FillBackgroundColor: [79, defaultColor],
_FillBackgroundFlag: [80],
_UnderlineStyle: [81],
_DashedUnderlineGapLength: [82],
_DashedUnderlineDashLength: [83],
_SlashedZero: [84],
_StylisticSets: [85],
_CustomFeature: [86, {
_StreamTag: [99]
}],
_MarkYDistFromBaseline: [87],
_AutoMydfb: [88],
_RefFontSize: [89],
_FontSizeRefType: [90],
_91: [91],
_92: [92],
_93: [93]
};
const defaultParagraphStyle = {
_Justification: [0],
_FirstLineIndent: [1],
_StartIndent: [2],
_EndIndent: [3],
_SpaceBefore: [4],
_SpaceAfter: [5],
_DropCaps: [6],
_AutoLeading: [7],
_LeadingType: [8],
_AutoHyphenate: [9],
_HyphenatedWordSize: [10],
_PreHyphen: [11],
_PostHyphen: [12],
_ConsecutiveHyphens: [13],
_Zone: [14],
_HyphenateCapitalized: [15],
_HyphenationPreference: [16],
_WordSpacing: [17],
_LetterSpacing: [18],
_GlyphSpacing: [19],
_SingleWordJustification: [20],
_Hanging: [21],
_AutoTCY: [22],
_KeepTogether: [23],
_BurasagariType: [24],
_KinsokuOrder: [25],
_Kinsoku: [27],
_KurikaeshiMojiShori: [26],
_MojiKumiTable: [28],
_EveryLineComposer: [29],
_TabStops: [30],
_DefaultTabWidth: [31],
_DefaultStyle: [32, defaultTextStyle],
_ParagraphDirection: [33],
_JustificationMethod: [34],
_ComposerEngine: [35],
_ListStyle: [36],
_ListTier: [37],
_ListSkip: [38],
_ListOffset: [39],
_KashidaWidth: [40]
};
const defaultParagraphData = {
_Name: [0],
_Features: [5, defaultParagraphStyle],
_Parent: [6],
_UUID: [97]
};
const defaultTextData = {
_Name: [0],
_Parent: [5],
_Features: [6, defaultTextStyle],
_UUID: [97]
};
const defaultRawEngineData = {
_98: [98, {
_0: [0]
}],
_DocumentResources: [
0,
{
_0: [0],
_FontSet: [
1,
{
_Resources: [
0,
{
_Resource: [
0,
{
_StreamTag: [99],
_Identifier: [
0,
{
_Name: [0],
_ScriptType: [1],
_Type: [2],
_Synthetic: [3],
_4: [4],
_MMAxis: [5]
}
],
_UUID: [97]
}
]
}
],
_DisplayList: [1, {
_Resource: [0]
}]
}
],
_MojiKumiCodeToClassSet: [
2,
{
_Resources: [0, {
_Resource: [0, {
_Name: [0],
_Members: [5],
_UUID: [97]
}]
}],
_DisplayList: [1, {
_Resource: [0]
}]
}
],
_MojiKumiTableSet: [
3,
{
_Resources: [
0,
{
_Resource: [
0,
{
_Name: [0],
_Members: [
5,
{
_CodeToClass: [0],
_AutoTsume: [
1,
{
_TsumeMappings: [
0,
{
_Before: [0],
_After: [1],
_Code: [2]
}
]
}
],
_Table: [
2,
{
_DataArray: [
0,
{
_SparseArray: [
0,
{
_Index: [0],
_Elements: [
1,
{
_P: [0],
_Data: [
1,
{
_A: [0, {
_R: [0],
_P: [1]
}],
_B: [1, {
_R: [0],
_P: [1]
}]
}
]
}
]
}
]
}
]
}
],
_PredefinedTag: [3]
}
],
_UUID: [97]
}
]
}
],
_DisplayList: [1, {
_Resource: [0]
}]
}
],
_KinsokuSet: [
4,
{
_Resources: [
0,
{
_Resource: [
0,
{
_Name: [0],
_Data: [
5,
{
_NoStart: [0],
_NoEnd: [1],
_Keep: [2],
_Hanging: [3],
_PredefinedTag: [4]
}
],
_UUID: [97]
}
]
}
],
_DisplayList: [1, {
_Resource: [0]
}]
}
],
_StyleSheetSet: [5, {
_Resources: [0, {
_Resource: [0, defaultTextData]
}],
_DisplayList: [1, {
_Resource: [0]
}]
}],
_ParagraphSheetSet: [6, {
_Resources: [0, {
_Resource: [0, defaultParagraphData]
}],
_DisplayList: [1, {
_Resource: [0]
}]
}],
_7: [7, {
_Resources: [0, {
_Resource: [0, {
_0: [0, {
_0: [0],
_1: [1, {
_0: [0]
}]
}],
_1: [1]
}]
}]
}],
_TextFrameSet: [
8,
{
_Resources: [
0,
{
_Resource: [
0,
{
_0: [0],
_Bezier: [1, {
_Points: [0]
}],
_Data: [
2,
{
_Type: [0],
_LineOrientation: [1],
_FrameMatrix: [2],
_4: [4],
_TextOnPathTRange: [6],
_RowGutter: [7],
_ColumnGutter: [8],
_9: [9],
_FirstBaselineAlignment: [10, {
_Flag: [0],
_Min: [1]
}],
_PathData: [
11,
{
_1: [1],
_Reversed: [0],
_2: [2],
_3: [3],
_Spacing: [4],
_5: [5],
_6: [6],
_7: [7],
_18: [18]
}
],
_12: [12],
_13: [13]
}
],
_3: [3, {
_0: [0]
}],
_UUID: [97]
}
]
}
]
}
],
_ListStyleSet: [
9,
{
_Resources: [
0,
{
_Resource: [
0,
{
_Name: [0],
_LevelStyle: [
5,
{
_IndentUnits: [0],
_TextIndent: [1],
_LabelIndent: [2],
_LabelAlignment: [3],
_SequenceGenerator: [
5,
{
_Prefix: [0],
_Postfix: [1],
_2: [2],
_CaseType: [3],
_Bullet: [9],
_StreamTag: [99]
}
],
_Font: [6],
_7: [7]
}
],
_PredefinedTag: [6],
_UUID: [97]
}
]
}
],
_DisplayList: [1, {
_Resource: [0]
}]
}
]
}
],
_DocumentObjects: [
1,
{
_DocumentSettings: [
0,
{
_HiddenGlyphFont: [
0,
{
_AlternateGlyphFont: [0],
_WhitespaceCharacterMapping: [1, {
_WhitespaceCharacter: [0],
_AlternateCharacter: [1]
}]
}
],
_NormalStyleSheet: [1],
_NormalParagraphSheet: [2],
_SuperscriptSize: [3],
_SuperscriptPosition: [4],
_SubscriptSize: [5],
_SubscriptPosition: [6],
_SmallCapSize: [7],
_UseSmartQuotes: [8],
_SmartQuoteSets: [
9,
{
_Language: [0],
_OpenDoubleQuote: [1],
_CloseDoubleQuote: [2],
_OpenSingleQuote: [3],
_CloseSingleQuote: [4]
}
],
_10: [10],
_11: [11],
_LinguisticSettings: [15, {
_PreferredProvider: [0],
_LinguisticProviderInfo: [1]
}],
_13: [13],
_UseSmartLists: [16],
_DefaultStoryDir: [17],
_18: [18],
_GreekingSize: [20]
}
],
_TextObjects: [
1,
{
_Model: [
0,
{
_Text: [0],
_ParagraphRun: [
5,
{
_RunArray: [
0,
{
_RunData: [0, {
_ParagraphSheet: [0, defaultParagraphData]
}],
_Length: [1]
}
]
}
],
_StyleRun: [6, {
_RunArray: [0, {
_RunData: [0, {
_StyleSheet: [0, defaultTextData]
}],
_Length: [1]
}]
}],
_FirstKern: [7],
_8: [8],
_AlternateGlyphRun: [
9,
{
_RunArray: [
0,
{
_RunData: [
0,
{
_AlternateGlyphSheet: [
0,
{
_Glyph: [0],
_Name: [1],
_2: [2]
}
]
}
],
_Length: [1]
}
]
}
],
_StorySheet: [
10,
{
_AntiAlias: [0],
_1: [1],
_UseFractionalGlyphWidths: [2],
_3: [3],
_4: [4]
}
],
_KernRun: [15],
_HyperlinkRun: [16]
}
],
_View: [
1,
{
_Frames: [0, {
_Resource: [0]
}],
_RenderedData: [
1,
{
_RunArray: [
0,
{
_RunData: [0, {
_0: [0],
_LineCount: [1]
}],
_Length: [1]
}
]
}
],
_Strikes: [2]
}
],
_OpticalAlignment: [2]
}
],
_OriginalNormalStyleFeatures: [2, defaultTextStyle],
_OriginalNormalParagraphFeatures: [3, defaultParagraphStyle]
}
]
};
const normalize = function(rawEngineData, defaultRawEngineData2, depth) {
let result;
if (typeof rawEngineData == "string") {
return rawEngineData;
}
if (rawEngineData instanceof Array) {
result = [];
for (let i = 0; i < rawEngineData.length; i++) {
result[i] = normalize(rawEngineData[i], defaultRawEngineData2);
}
} else {
result = {};
const existKeyMap = {};
for (const key in defaultRawEngineData2) {
const data = defaultRawEngineData2[key];
const engineKey = "_" + data[0];
if (rawEngineData[engineKey] != null) {
result[key] = data[1] ? normalize(rawEngineData[engineKey], data[1]) : rawEngineData[engineKey];
existKeyMap[engineKey] = true;
}
}
for (const key in rawEngineData) {
if (existKeyMap[key] == null) {
if (key.length > 3) {
continue;
}
console.log(defaultRawEngineData2, existKeyMap);
console.log(key, rawEngineData);
throw "e";
}
}
}
return result;
};
function normalizeEngineData(rawEngineData) {
return normalize(rawEngineData, defaultRawEngineData);
}
class TextEngineData extends LayerInfoBase {
static shouldParse(key) {
return key === "Txt2";
}
constructor(layer, length) {
super(layer, length);
this.textEngineData = null;
}
parse() {
const data = this.file.read(this.length);
const rawEngineData = readGlobalEngineData(data);
this.textEngineData = normalizeEngineData(rawEngineData);
}
export() {
return {
textEngineData: this.textEngineData
};
}
}
class FilterMask extends LayerInfoBase {
static shouldParse(key) {
return key === "FMsk";
}
constructor(layer, length) {
super(layer, length);
}
parse() {
this.color = this.file.readSpaceColor();
this.opacity = this.file.readShort();
}
}
class PatternsData extends LayerInfoBase {
static shouldParse(key) {
return ["Patt", "Pat2", "Pat3"].includes(key);
}
constructor(layer, length) {
super(layer, length);
this.length = length;
this.patterns = {};
}
readVirtualMemoryArrayList() {
const { file } = this;
file.seek(4, true);
const VMALEnd = file.readInt() + file.tell();
const pattern = {
top: file.readInt(),
left: file.readInt(),
bottom: file.readInt(),
right: file.readInt(),
channels: file.readInt(),
data: []
};
pattern.width = pattern.right - pattern.left;
pattern.height = pattern.bottom - pattern.top;
for (let i = 0; i < pattern.channels + 2; i++) {
let lineIndex = 0;
let chanPos = 0;
if (!file.readInt()) {
continue;
}
const currentPatternLen = file.readInt();
const endChannel = currentPatternLen + file.tell();
file.readInt();
file.readInt();
file.readInt();
file.readInt();
file.readInt();
file.readShort();
const compressed = file.readByte();
if (compressed) {
const byteCounts = [];
pattern.data[i] = new Uint8Array(pattern.width * pattern.height);
for (let j = 0; j < pattern.height; j++) {
byteCounts.push(file.readShort());
}
for (let j = 0; j < pattern.height; j++) {
const finish = file.tell() + byteCounts[lineIndex + j];
while (file.tell() < finish) {
let len = file.read(1)[0];
if (len < 128) {
len += 1;
const data = file.read(len);
pattern.data[i].set(data, chanPos);
chanPos += len;
} else if (len > 128) {
len ^= 255;
len += 2;
const val = file.read(1)[0];
pattern.data[i].fill(val, chanPos, chanPos + len);
chanPos += len;
}
}
}
lineIndex += pattern.height;
} else {
pattern.data[i] = new Uint8Array(file.read(currentPatternLen - 23));
}
file.seek(endChannel);
}
file.seek(VMALEnd);
return pattern;
}
readPattern() {
const file = this.file;
const len = file.readInt();
const patternEnd = (len + 3 & ~3) + file.tell();
file.readInt();
const mode = file.readInt();
const point = [file.readShort(), file.readShort()];
const pattern = {
name: file.readUnicodeString(),
id: file.readString(file.readByte()),
mode,
palette: [],
width: point[1],
height: point[0]
};
if (mode === 2) {
pattern.palette = file.read(256 * 3);
file.seek(4, true);
}
pattern.data = this.readVirtualMemoryArrayList();
this.patterns[pattern.id] = pattern;
file.seek(patternEnd);
}
parse() {
const file = this.file;
const patternsEnd = this.length + file.tell();
while (file.tell() < patternsEnd) {
this.readPattern();
}
file.seek(patternsEnd);
}
}
const ADDITION_INFO = {
linkedLayer: LinkedLayer,
unicodePath: UnicodePath,
textEngineData: TextEngineData,
filterMask: FilterMask,
patternsData: PatternsData
};
class LayerMask {
constructor(file, header) {
this.file = file;
this.header = header;
this.layers = [];
this.mergedAlpha = false;
this.globalMask = null;
this.infoKeys = [];
this.adjustments = {};
this.obj = this;
}
skip() {
this.file.seek(this.file.readInt(), true);
}
parse() {
let maskSize;
if (this.header.version === 1) {
maskSize = this.file.readInt();
} else {
maskSize = this.file.readLongLong();
}
this.layerEnd = maskSize + this.file.tell();
if (maskSize <= 0) {
return;
}
this.parseLayers();
this.parseGlobalMask();
this.layers.reverse();
this.parseAdditionalLayerInformation();
this.assignGlobalEngineData();
this.file.seek(this.layerEnd);
}
parseLayers() {
let layerInfoSize;
if (this.header.version === 1) {
layerInfoSize = Util.pad2(this.file.readInt());
} else {
layerInfoSize = Util.pad2(this.file.readLongLong());
}
const layerEnd = this.file.tell() + layerInfoSize;
if (layerInfoSize > 0) {
let layerCount = this.file.readShort();
if (layerCount < 0) {
layerCount = Math.abs(layerCount);
this.mergedAlpha = true;
}
for (let i = 0; i < layerCount; i++) {
const layer = new Layer$1(this.file, this.header);
layer.parse();
this.layers.push(layer);
}
const len = this.layers.length;
for (let i = 0; i < len; i++) {
const layer = this.layers[i];
layer.parseChannelImage();
}
}
this.file.seek(layerEnd);
}
parseGlobalMask() {
const length = this.file.readInt();
if (length <= 0) {
return;
}
const maskEnd = Util.pad2(this.file.tell() + length);
const mask = {};
mask.overlayColorSpace = this.file.readShort();
mask.colorComponents = [this.file.readShort() >> 8, this.file.readShort() >> 8, this.file.readShort() >> 8, this.file.readShort() >> 8];
mask.opacity = this.file.readShort() / 16;
mask.kind = this.file.readByte();
this.globalMask = mask;
this.file.seek(maskEnd);
}
parseAdditionalLayerInformation() {
while (this.file.tell() < this.layerEnd) {
this.file.seek(4, true);
const key = this.file.readString(4);
const length = Util.pad2(this.file.readInt());
this.file.tell();
if (length <= 0) {
continue;
}
let keyParseable = false;
for (const additionInfoKey in ADDITION_INFO) {
const AddInfoConstructor = ADDITION_INFO[additionInfoKey];
if (!AddInfoConstructor.shouldParse(key)) {
continue;
}
const addInfoInstance = new AddInfoConstructor(this, length);
this.adjustments[additionInfoKey] = new LazyExecute(addInfoInstance, this.file).now("skip").later("parse").get();
if (this[additionInfoKey] == null) {
this[additionInfoKey] = () => this.adjustments[additionInfoKey];
}
this.infoKeys.push(key);
keyParseable = true;
break;
}
if (!keyParseable) {
this.file.seek(length, true);
}
}
}
/**
* There is a Global text engine data outside text element.
* So, we need to pick global engine data and set to per text element.
*/
assignGlobalEngineData() {
var _a, _b, _c, _d;
const globalEngineData = (_b = (_a = this.adjustments) == null ? void 0 : _a.textEngineData) == null ? void 0 : _b.textEngineData;
const resources = (_d = (_c = globalEngineData == null ? void 0 : globalEngineData._DocumentResources) == null ? void 0 : _c._TextFrameSet) == null ? void 0 : _d._Resources;
if (!resources || !this.layers.length) {
return;
}
let textIndex = resources.length - 1;
this.layers.forEach((layer) => {
var _a2, _b2;
const engineData = (_b2 = (_a2 = layer.adjustments) == null ? void 0 : _a2.typeTool) == null ? void 0 : _b2.engineData;
if (engineData) {
engineData.curve = resources[textIndex--];
}
});
}
// @TODO this function looks unused.
parseLayerInfo() {
throw new Error("layerMask.parseLayerInfo is not implemented");
}
}
class ICC {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
this.start = this.file.pos;
this.end = this.start + this.resource.length;
}
parse() {
this.data = this.file.data.subarray(this.start, this.end);
return this.data;
}
}
ICC.prototype.id = 1039;
ICC.prototype.name = "icc profile";
class LayerComps {
static visibilityCaptured(comp) {
return comp.capturedInfo & parseInt("001", 2) > 0;
}
static positionCaptured(comp) {
return comp.positionCaptured & parseInt("010", 2) > 0;
}
static appearanceCaptured(comp) {
return comp.appearanceCaptured & parseInt("100", 2) > 0;
}
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
}
parse() {
this.file.seek(4, true);
this.data = new Descriptor(this.file).parse();
return this.data;
}
names() {
return this.data.list.map((comp) => comp["Nm "]);
}
export() {
return this.data.list.map((comp) => ({
id: comp.compID,
name: comp["Nm "],
capturedInfo: comp.capturedInfo
}));
}
}
LayerComps.prototype.id = 1065;
LayerComps.prototype.name = "layerComps";
let GlobalLight$1 = class GlobalLight {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
}
parse() {
this.angle = this.file.readInt();
return this.angle;
}
};
GlobalLight$1.prototype.id = 1037;
GlobalLight$1.prototype.name = "globalLightAngle";
class GlobalLight2 {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
}
parse() {
this.altitude = this.file.readInt();
return this.altitude;
}
}
GlobalLight2.prototype.id = 1049;
GlobalLight2.prototype.name = "globalLightAngle";
class OriginPathInfo {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
}
parse() {
this.file.seek(4, true);
this.data = new Descriptor(this.file).parse();
return this.data;
}
}
OriginPathInfo.prototype.id = 3e3;
OriginPathInfo.prototype.name = "originPathInfo";
class PathSelection {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
}
parse() {
this.file.seek(4, true);
this.data = new Descriptor(this.file).parse();
return this.data;
}
}
PathSelection.prototype.id = 1088;
PathSelection.prototype.name = "pathSelection";
class Path {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
this.paths = [];
}
parse() {
const numRecords = this.resource.length / 26;
for (let i = 0; i < numRecords; i++) {
const record = new PathRecord(this.file);
record.parse();
this.paths.push(record);
}
}
export() {
return this.paths;
}
}
Path.prototype.id = 2e3;
Path.prototype.name = "path";
class Thumbnail {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
}
parse() {
this.format = this.file.readString(4);
this.width = this.file.readInt();
this.height = this.file.readInt();
this.widthBytes = this.file.readInt();
this.totalSize = this.file.readInt();
this.compressedSize = this.file.readInt();
this.bitsPerPixel = this.file.readShort();
this.numberOfPlanes = this.file.readShort();
this.jfif = this.file.read(this.compressedSize);
this.binaryString = String.fromCharCode.apply(null, this.jfif);
}
}
Thumbnail.prototype.id = 1036;
Thumbnail.prototype.name = "thumbnail";
class LinkLayers {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
this.linkArray = [];
}
parse() {
const end = this.file.tell() + this.resource.length;
while (end > this.file.tell()) {
this.linkArray.push(this.file.readShort());
}
this.linkArray.reverse();
return this.linkArray;
}
}
LinkLayers.prototype.id = 1026;
LinkLayers.prototype.name = "LinkLayers";
class ResolutionInfo {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
}
parse() {
this.h_res = this.file.readUInt() / 65536;
this.h_res_unit = this.file.readUShort();
this.width_unit = this.file.readUShort();
this.v_res = this.file.readUInt() / 65536;
this.v_res_unit = this.file.readUShort();
this.height_unit = this.file.readUShort();
this.resource.data = {
h_res: this.h_res,
h_res_unit: this.h_res_unit,
width_unit: this.width_unit,
v_res: this.v_res,
v_res_unit: this.v_res_unit,
height_unit: this.height_unit
};
return this.resource.data;
}
export() {
const data = {};
const keys = ["h_res", "h_res_unit", "width_unit", "v_res", "v_res_unit", "height_unit"];
const len = keys.length;
for (let i = 0; i < len; i++) {
const key = keys[i];
data[key] = this[key];
}
return data;
}
}
ResolutionInfo.prototype.id = 1005;
ResolutionInfo.prototype.name = "resolutionInfo";
class Guides {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
this.data = [];
}
parse() {
this.file.seek(4, true);
this.file.seek(8, true);
const num_guides = this.file.readInt();
for (let i = 0; i < num_guides; i++) {
const location = (this.file.readInt() / 32).toFixed(1);
const direction = this.file.readByte() ? "horizontal" : "vertical";
this.data.push({
location,
direction
});
}
}
export() {
return this.data;
}
}
Guides.prototype.id = 1032;
Guides.prototype.name = "guides";
class XMP {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
}
parse() {
this.xmp = this.file.read(this.resource.length);
return this.xmp;
}
}
XMP.prototype.id = 1060;
XMP.prototype.name = "xmp";
class Timeline {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
}
parse() {
this.file.seek(4, true);
this.data = new Descriptor(this.file).parse();
}
}
Timeline.prototype.id = 1075;
Timeline.prototype.name = "timeline";
class WorkingPath {
constructor(resource) {
this.resource = resource;
this.file = this.resource.file;
this.paths = [];
}
parse() {
const numRecords = (this.resource.length - 10) / 26;
for (let i = 0; i < numRecords; i++) {
const record = new PathRecord(this.file);
record.parse();
this.paths.push(record);
}
}
}
WorkingPath.prototype.id = 1025;
WorkingPath.prototype.name = "WorkingPath";
const RESOURCES = [
ICC,
LayerComps,
GlobalLight$1,
GlobalLight2,
OriginPathInfo,
PathSelection,
Path,
Thumbnail,
LinkLayers,
ResolutionInfo,
Guides,
XMP,
Timeline,
WorkingPath
];
class ResourceSection {
static factory(resource) {
const len = RESOURCES.length;
for (let i = 0; i < len; i++) {
const Section = RESOURCES[i];
if (Section.prototype.id === resource.id || resource.id >= 2e3 && resource.id < 2998 && Section.prototype.id === 2e3) {
const res = new Section(resource);
res.parse();
return res;
}
}
return null;
}
}
class Resource {
constructor(file) {
this.file = file;
this.id = null;
this.type = null;
this.length = 0;
}
parse() {
this.type = this.file.readString(4);
this.id = this.file.readShort();
const nameLength = Util.pad2(this.file.readByte() + 1) - 1;
this.name = this.file.readString(nameLength);
this.length = Util.pad2(this.file.readInt());
}
}
Resource.Section = ResourceSection;
class Resources {
constructor(file) {
this.file = file;
this.resources = {};
this.typeIndex = {};
this.length = null;
}
skip() {
this.length = this.file.readInt();
this.file.seek(this.length, true);
}
parse() {
this.length = this.file.readInt();
const finish = this.length + this.file.tell();
while (this.file.tell() < finish) {
const resource = new Resource(this.file);
resource.parse();
const resourceEnd = this.file.tell() + resource.length;
const section = Resource.Section.factory(resource);
if (section == null) {
this.file.seek(resourceEnd);
continue;
}
this.resources[section.id] = section;
if (section.name != null) {
this.typeIndex[section.name] = section.id;
}
this.file.seek(resourceEnd);
}
this.file.seek(finish);
}
resource(search) {
if (typeof search === "string") {
return this.byType(search);
} else {
return this.resources[search];
}
}
byType(name) {
return this.resources[this.typeIndex[name]];
}
load() {
console.warn("Need not to call load() since version 3.6.6");
}
}
class NodeBase {
// Every node gets a reference to the layer/group and its parent, which allows us to
// traverse the tree structure. It also builds references to all of its children.
constructor(layer, parent) {
// Each Node subclass defines a type, which makes it easier to identity what we're
// dealing with, since `constructor.name` can get mangled during minification and
// wreak havoc.
__publicField(this, "type", "node");
this.parent = parent;
this.layer = layer;
this.layer.node = this;
this._children = [];
this.name = this.layer.name;
this.forceVisible = null;
this.coords = {
top: this.layer.top,
bottom: this.layer.bottom,
left: this.layer.left,
right: this.layer.right
};
this.topOffset = 0;
this.leftOffset = 0;
this.createProperties();
}
createProperties() {
Object.defineProperty(this, "top", {
get: function() {
return this.coords.top + this.topOffset;
},
set: function(val) {
return this.coords.top = val;
}
});
Object.defineProperty(this, "right", {
get: function() {
return this.coords.right + this.leftOffset;
},
set: function(val) {
return this.coords.right = val;
}
});
Object.defineProperty(this, "bottom", {
get: function() {
return this.coords.bottom + this.topOffset;
},
set: function(val) {
return this.coords.bottom = val;
}
});
Object.defineProperty(this, "left", {
get: function() {
return this.coords.left + this.leftOffset;
},
set: function(val) {
return this.coords.left = val;
}
});
Object.defineProperty(this, "width", {
get: function() {
return this.right - this.left;
}
});
return Object.defineProperty(this, "height", {
get: function() {
return this.bottom - this.top;
}
});
}
// **All properties should be accessed through `get()`**. While many things can be
// accessed without it, using `get()` provides 2 things:
// * Consistency
// * Access to both data on the Node and the Layer through the same interface.
// This makes it much cleaner to access stuff like layer info blocks, since you just
// give the name of the block you want to access. For example:
// ``` coffeescript
// node.get('typeTool').export()
// # vs
// node.layer.typeTool().export()
// ```
get(prop) {
const value = this[prop] != null ? this[prop] : this.layer[prop];
if (typeof value === "function") {
return value();
} else {
return value;
}
}
// Is this layer/group visible? This checks all possible places that could define
// whether this is true, e.g. clipping masks. It also checks the current
// layer comp visibility override (not implemented yet).
visible() {
if (this.layer.clipped && !this.clippingMask().visible()) {
return false;
}
if (this.forceVisible != null) {
return this.forceVisible;
} else {
return this.layer.visible;
}
}
hidden() {
return !this.visible();
}
isLayer() {
return this.type === "layer";
}
isGroup() {
return this.type === "group";
}
isRoot() {
return this.type === "root";
}
// Retrieves the clipping mask for this node. Because a clipping mask can be applied
// to multiple layers, we have to traverse the tree until we find the first node that
// does not have the `clipped` flag. We can do it this way because all layers that
// the clipping node affects must be siblings and in sequence.
clippingMask() {
if (!this.layer.clipped) {
return null;
}
if (!this.clippingMaskCached) {
let maskNode = this.nextSibling();
while (maskNode.clipped) {
maskNode = maskNode.nextSibling();
}
this.clippingMaskCached = maskNode;
}
return this.clippingMaskCached;
}
clippedBy() {
return this.clippingMask();
}
// We can export the most important information about this node as a plain object.
// If we're exporting a group, it will recursively export itself and all of its descendants as well.
export() {
const hash = {
type: null,
visible: this.visible(),
opacity: this.layer.opacity / 255,
blendingMode: this.layer.blendingMode()
};
const properties = NodeBase.PROPERTIES;
const len = properties.length;
for (let i = 0; i < len; i++) {
const prop = properties[i];
hash[prop] = this[prop];
}
return hash;
}
// While the PSD document does not define explicit dimensions for groups, we can generate
// them based on the bounding boxes of their layer children. When we build the tree structure,
// we update the dimensions of the group whenever a layer is added so that we finish with
// the actual bounding box of the group's contents.
updateDimensions() {
if (this.isLayer()) {
return;
}
const children = this._children;
const len = children.length;
for (let i = 0; i < len; i++) {
const child = children[i];
child.updateDimensions();
}
if (this.isRoot()) {
return;
}
const nonEmptyChildren = this._children.filter((c) => !c.isEmpty());
this.left = Math.min.call(null, nonEmptyChildren.map((c) => c.left)) || 0;
this.top = Math.min.call(null, nonEmptyChildren.map((c) => c.top)) || 0;
this.bottom = Math.max.call(null, nonEmptyChildren.map((c) => c.bottom)) || 0;
this.right = Math.max.call(null, nonEmptyChildren.map((c) => c.right)) || 0;
}
root() {
if (this.isRoot()) {
return this;
}
return this.parent.root();
}
// isRoot () {
// return this.depth() === 0;
// }
children() {
return this._children;
}
ancestors() {
if (this.parent == null || this.parent.isRoot()) {
return [];
}
return this.parent.ancestors().concat([this.parent]);
}
hasChildren() {
return this._children.length > 0;
}
childless() {
return !this.hasChildren();
}
siblings() {
if (this.parent == null) {
return [];
}
return this.parent.children();
}
nextSibling() {
if (this.parent == null) {
return null;
}
const index = this.siblings().indexOf(this);
return this.siblings()[index + 1];
}
prevSibling() {
if (this.parent == null) {
return null;
}
const index = this.siblings().indexOf(this);
return this.siblings()[index - 1];
}
hasSiblings() {
return this.siblings().length > 1;
}
onlyChild() {
return !this.hasSiblings();
}
descendants() {
return this._children.map((c) => c.subtree()).flat();
}
subtree() {
return [this].concat(this.descendants());
}
depth() {
return this.ancestors().length + 1;
}
path(asArray = false) {
const path = this.ancestors().map(function(n) {
return n.name;
}).concat([this.name]);
if (asArray) {
return path;
} else {
return path.join("/");
}
}
toPng() {
return this.layer.image.toPng();
}
saveAsPng(output) {
return this.layer.image.saveAsPng(output);
}
childrenAtPath(path, opts = {}) {
if (!Array.isArray(path)) {
path = path.split("/").filter((p) => p.length > 0);
}
path = [...path];
const query = path.shift();
const matches = this.children().filter(function(c) {
if (opts.caseSensitive) {
return c.name === query;
} else {
return c.name.toLowerCase() === query.toLowerCase();
}
});
if (path.length === 0) {
return matches;
} else {
return matches.map((m) => m.childrenAtPath([...path], opts)).flat();
}
}
}
NodeBase.PROPERTIES = ["name", "left", "right", "top", "bottom", "height", "width"];
class Group extends NodeBase {
constructor() {
super(...arguments);
__publicField(this, "type", "group");
}
passthruBlending() {
return this.get("blendingMode") === "passthru";
}
isEmpty() {
const children = this._children;
const len = children.length;
for (let i = 0; i < len; i++) {
const child = children[i];
if (!child.isEmpty()) {
return false;
}
}
return true;
}
export() {
const data = super.export();
data.type = "group";
data.children = this._children.map(function(c) {
return c.export();
});
return data;
}
}
class Layer2 extends NodeBase {
constructor() {
super(...arguments);
__publicField(this, "type", "layer");
}
hasMask() {
if (!this.layer.adjustments) {
return false;
}
const vectorMask = this.layer.adjustments.vectorMask;
const vectorStroke = this.layer.adjustments.vectorStroke;
return vectorMask || vectorStroke;
}
isEmpty() {
return (this.width === 0 || this.height === 0) && !this.hasMask();
}
export() {
const typeTool = this.get("typeTool");
const data = super.export();
data.type = "layer";
data.mask = this.layer.mask.export();
data.text = typeTool ? typeTool.export() : void 0;
data.image = {};
return data;
}
}
class Root extends NodeBase {
constructor(psd) {
super(Root.layerForPsd(psd));
__publicField(this, "type", "root");
this.psd = psd;
this.buildHierarchy();
}
// since NodeBase's first param require a `Layer` object
// forge a layer for psd here
static layerForPsd(psd) {
const layer = {};
const properties = NodeBase.PROPERTIES;
const len = properties.length;
for (let i = 0; i < len; i++) {
const prop = properties[i];
layer[prop] = null;
}
layer.top = 0;
layer.left = 0;
layer.right = psd.header.width;
layer.bottom = psd.header.height;
return layer;
}
documentDimensions() {
return [this.width, this.height];
}
depth() {
return 0;
}
opacity() {
return 255;
}
fillOpacity() {
return 255;
}
export() {
const layerComps = this.psd.resources.resource("layerComps");
const resolutionInfo = this.psd.resources.resource("resolutionInfo");
const guides = this.psd.resources.resource("guides");
return {
children: this._children.map((c) => c.export()),
document: {
width: this.width,
height: this.height,
resources: {
layerComps: layerComps ? layerComps.export() : [],
resolutionInfo: resolutionInfo ? resolutionInfo.export() : [],
guides: guides ? guides.export() : null,
slices: []
}
}
};
}
buildHierarchy() {
let currentGroup = this;
const parseStack = [];
const layers = this.psd.layers;
const len = layers.length;
for (let i = 0; i < len; i++) {
const layer = layers[i];
if (layer.isFolder()) {
parseStack.push(currentGroup);
const last = parseStack[parseStack.length - 1];
currentGroup = new Group(layer, last);
} else if (layer.isFolderEnd() && parseStack.length) {
const parent = parseStack.pop();
parent.children().push(currentGroup);
currentGroup = parent;
} else {
currentGroup.children().push(new Layer2(layer, currentGroup));
}
}
this.updateDimensions();
}
}
const fs = require("fs");
const psdInit = {
extended: function(PSD2) {
PSD2.fromFile = function(file) {
return new PSD2(fs.readFileSync(file));
};
PSD2.fromBuffer = function(buffer) {
const psd = new PSD2(buffer);
psd.parse();
return psd;
};
PSD2.open = function(file) {
return new Promise(function(resolve, reject) {
return fs.readFile(file, (err, data) => {
if (err) {
reject(err);
}
try {
const psd = new PSD2(data);
psd.parse();
resolve(psd);
} catch (error) {
err = error;
reject(err);
}
});
});
};
}
};
class PSD {
// Creates a new PSD object. Typically, you will use a helper method to instantiate
// the PSD object. However, if you already have the PSD data stored as a Uint8Array,
// you can instantiate the PSD object directly.
constructor(data) {
this.file = new File(data);
this.parsed = false;
this.header = null;
Object.defineProperty(this, "layers", {
get: function() {
return this.layerMask.layers;
}
});
}
// Parses the PSD. You must call this method before attempting to
// access PSD data. It will not reparse the PSD if it has already
// been parsed.
parse() {
if (this.parsed) {
return;
}
this.parseHeader();
this.parseResources();
this.parseLayerMask();
this.parseImage();
this.parsed = true;
}
// The next 4 methods are responsible for parsing the 4 main sections of the PSD.
// These are private, and you should never call them from your own code.
parseHeader() {
this.header = new Header(this.file);
this.header.parse();
}
parseResources() {
const resources = new Resources(this.file);
resources.parse();
this.resources = resources;
}
parseLayerMask() {
const layerMask = new LayerMask(this.file, this.header);
layerMask.parse();
this.layerMask = layerMask;
}
parseImage() {
const image = new Image(this.file, this.header);
image.parse();
this.image = image;
}
// Returns a tree representation of the PSD document, which is the
// preferred way of accessing most of the PSD's data.
tree() {
return new PSD.Node.Root(this);
}
// In some cases, User need to customized color mode transformation.
setColorConverters() {
Color.setColorConverters(...arguments);
}
}
PSD.Node = {
Root
};
psdInit.extended(PSD);
module.exports = PSD;