"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(">$/; 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 === ""; } } 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 === ""; } } 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;