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
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;
|