From 5e33004116228317ad6f3040b696d577b1f387ea Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Thu, 28 Jul 2022 17:12:07 +0200 Subject: [PATCH 1/7] feature: implement EngineData parsing fixes https://github.com/webtoon/psd/issues/6 --- packages/psd/src/classes/Layer.ts | 10 +- packages/psd/src/engineData/index.ts | 7 + packages/psd/src/engineData/lexer.ts | 217 +++++++ packages/psd/src/engineData/parser.ts | 106 ++++ packages/psd/src/engineData/validator.ts | 40 ++ packages/psd/src/interfaces/EngineData.ts | 10 + packages/psd/src/interfaces/index.ts | 1 + packages/psd/src/methods/index.ts | 1 + packages/psd/src/methods/parseEngineData.ts | 21 + .../LayerAndMaskInformation/interfaces.ts | 7 + .../readLayerRecordsAndChannels.ts | 11 + packages/psd/src/utils/bytes.ts | 12 + packages/psd/src/utils/error.ts | 12 + .../psd/tests/integration/engineData.test.ts | 23 + .../tests/integration/fixtures/engineData.psd | Bin 0 -> 493834 bytes packages/psd/tests/unit/engineData.test.ts | 183 ++++++ .../psd/tests/unit/fixtures/engineData.bin | Bin 0 -> 12298 bytes .../psd/tests/unit/fixtures/engineData.json | 533 ++++++++++++++++++ 18 files changed, 1193 insertions(+), 1 deletion(-) create mode 100644 packages/psd/src/engineData/index.ts create mode 100644 packages/psd/src/engineData/lexer.ts create mode 100644 packages/psd/src/engineData/parser.ts create mode 100644 packages/psd/src/engineData/validator.ts create mode 100644 packages/psd/src/interfaces/EngineData.ts create mode 100644 packages/psd/src/methods/parseEngineData.ts create mode 100644 packages/psd/tests/integration/engineData.test.ts create mode 100644 packages/psd/tests/integration/fixtures/engineData.psd create mode 100644 packages/psd/tests/unit/engineData.test.ts create mode 100644 packages/psd/tests/unit/fixtures/engineData.bin create mode 100644 packages/psd/tests/unit/fixtures/engineData.json diff --git a/packages/psd/src/classes/Layer.ts b/packages/psd/src/classes/Layer.ts index c462a22..2c49a04 100644 --- a/packages/psd/src/classes/Layer.ts +++ b/packages/psd/src/classes/Layer.ts @@ -2,7 +2,7 @@ // Copyright 2021-present NAVER WEBTOON // MIT License -import {ImageData} from "../interfaces"; +import {EngineData, ImageData} from "../interfaces"; import {LayerFrame} from "../sections"; import {NodeParent} from "./Node"; import {NodeBase} from "./NodeBase"; @@ -65,6 +65,14 @@ export class Layer return this.layerFrame.layerProperties.text; } + /** + * If this layer is a text layer, this property retrieves its text properties. + * Otherwise, this property is `undefined`. + */ + get textProperties(): EngineData | undefined { + return this.layerFrame.layerProperties.textProperties; + } + protected get imageData(): ImageData { const {red, green, blue, alpha} = this.layerFrame; diff --git a/packages/psd/src/engineData/index.ts b/packages/psd/src/engineData/index.ts new file mode 100644 index 0000000..26fbb75 --- /dev/null +++ b/packages/psd/src/engineData/index.ts @@ -0,0 +1,7 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +export * from "./lexer"; +export * from "./parser"; +export * from "./validator"; diff --git a/packages/psd/src/engineData/lexer.ts b/packages/psd/src/engineData/lexer.ts new file mode 100644 index 0000000..719b9fe --- /dev/null +++ b/packages/psd/src/engineData/lexer.ts @@ -0,0 +1,217 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +// Based on PDF grammar: https://web.archive.org/web/20220226063926/https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf +// Section 7.2 - Lexical Conventions + +import { + Cursor, + InvalidEngineDataBoolean, + InvalidEngineDataNumber, + InvalidEngineDataTextBOM, +} from "../utils"; + +export enum TokenType { + String, + DictBeg, + DictEnd, + ArrBeg, + ArrEnd, + Name, + Number, + Boolean, +} + +export type Token = + | {type: TokenType.String; value: string} + | {type: TokenType.DictBeg} + | {type: TokenType.DictEnd} + | {type: TokenType.ArrBeg} + | {type: TokenType.ArrEnd} + | {type: TokenType.Name; value: string} + | {type: TokenType.Number; value: number} + | {type: TokenType.Boolean; value: boolean}; + +const WhitespaceCharacters = new Set([ + 0, + 9, + 12, + 32, // ' ' + 10, // \n + 13, // \r +]); + +const BooleanStartCharacters = new Set([ + 0x66, // f + 0x74, // t +]); + +const Delimiters = { + "(": 0x28, + ")": 0x29, + "<": 0x3c, + ">": 0x3e, + "[": 0x5b, + "]": 0x5d, + "/": 0x2f, + "\\": 0x5c, + // NOTE: These have meaning within PDF. Are they used here? + // "{": 123, + // "}": 125, + // "%": 37, +}; + +const DelimiterCharacters = new Set(Object.values(Delimiters)); + +export class Lexer { + constructor(private cursor: Cursor) {} + + *tokens(): Generator { + while (!this.done()) { + const val = this.cursor.read("u8"); + + if (WhitespaceCharacters.has(val)) { + while (!this.done() && WhitespaceCharacters.has(this.cursor.peek())) + this.cursor.pass(1); + continue; + } + if (DelimiterCharacters.has(val)) { + if (val === Delimiters["("]) { + yield {type: TokenType.String, value: this.text()}; + continue; + } + if (val === Delimiters["["]) { + yield {type: TokenType.ArrBeg}; + continue; + } + if (val === Delimiters["]"]) { + yield {type: TokenType.ArrEnd}; + continue; + } + if (val === Delimiters["<"]) { + // NOTE: assert that it is < indeed? + this.cursor.pass(1); + yield {type: TokenType.DictBeg}; + continue; + } + if (val === Delimiters[">"]) { + // NOTE: assert that it is > indeed? + this.cursor.pass(1); + yield {type: TokenType.DictEnd}; + continue; + } + if (val === Delimiters["/"]) { + yield {type: TokenType.Name, value: this.string()}; + continue; + } + console.assert( + false, + "Unhandled delimiter: '%s'", + String.fromCharCode(val) + ); + continue; + } + // only two types left: number or boolean + // we need to return val first since it starts value + this.cursor.unpass(1); + if (BooleanStartCharacters.has(val)) { + yield {type: TokenType.Boolean, value: this.boolean()}; + } else { + yield {type: TokenType.Number, value: this.number()}; + } + } + } + + private done(): boolean { + return this.cursor.position >= this.cursor.length; + } + + private text(): string { + const firstByte = this.cursor.peek(); + if (firstByte === Delimiters[")"]) { + this.cursor.pass(1); + return ""; + } + const hasBom = firstByte === 0xff || firstByte === 0xfe; + let decoder = new TextDecoder("utf-16be"); + if (hasBom) { + decoder = this.textDecoderFromBOM(); + } + const textParts = [] as string[]; + const readAhead = this.cursor.clone(); + while (readAhead.peek() !== Delimiters[")"]) { + readAhead.pass(1); + if (readAhead.peek() === Delimiters["\\"]) { + const length = readAhead.position - this.cursor.position; + let raw = this.cursor.take(length); + if (raw.at(-1) === 0x00) { + // Sometimes there's extra padding before - we need to remove it + raw = raw.subarray(0, -1); + } + textParts.push(decoder.decode(raw)); + readAhead.pass(1); // skip over \\ + textParts.push(String.fromCharCode(readAhead.take(1)[0])); // un-escape character + this.cursor.pass(2); // skip over escaped character to avoid decoding it in subsequent part + } + } + const length = readAhead.position - this.cursor.position; + const raw = this.cursor.take(length); + textParts.push(decoder.decode(raw)); + this.cursor.pass(1); // final ) + return textParts.join(""); + } + + private textDecoderFromBOM(): TextDecoder { + const firstBomPart = this.cursor.read("u8"); + const sndBomPart = this.cursor.read("u8"); + // https://en.wikipedia.org/wiki/Byte_order_mark#UTF-16 + // LE is FF FE + if (firstBomPart === 0xff && sndBomPart === 0xfe) + return new TextDecoder("utf-16le"); + // BE is FE FF + if (firstBomPart === 0xfe && sndBomPart === 0xff) + return new TextDecoder("utf-16be"); + throw new InvalidEngineDataTextBOM( + `Unknown BOM value: [${firstBomPart}, ${sndBomPart}]` + ); + } + + private string(): string { + const decoder = new TextDecoder("ascii"); + const readAhead = this.cursor.clone(); + while ( + !this.done() && + !WhitespaceCharacters.has(this.cursor.peek()) && + !DelimiterCharacters.has(this.cursor.peek()) + ) { + this.cursor.pass(1); + } + const text = decoder.decode( + readAhead.take(this.cursor.position - readAhead.position) + ); + return text; + } + + private number(): number { + const text = this.string(); + const value = Number(text); + if (Number.isNaN(value)) { + throw new InvalidEngineDataNumber(`parsing '${text}' as Number failed`); + } + return value; + } + + private boolean(): boolean { + const text = this.string(); + if (text === "true") { + return true; + } + if (text === "false") { + return false; + } + throw new InvalidEngineDataBoolean( + `'${text}' is neither 'true' nor 'false'` + ); + } +} diff --git a/packages/psd/src/engineData/parser.ts b/packages/psd/src/engineData/parser.ts new file mode 100644 index 0000000..acf962d --- /dev/null +++ b/packages/psd/src/engineData/parser.ts @@ -0,0 +1,106 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +import { + InvalidEngineDataDictKey, + InvalidTopLevelEngineDataValue, + UnexpectedEndOfEngineData, + UnexpectedEngineDataToken, +} from "../utils"; +import {Token, TokenType} from "./lexer"; + +export type RawEngineData = { + [key: string]: RawEngineValue; +}; +export type RawEngineValue = + | string + | number + | boolean + | RawEngineValue[] + | RawEngineData; + +export class Parser { + // private done: boolean = false + constructor(private tokens: Generator) {} + + parse(): RawEngineData { + const value = this.value(); + // TODO: for this to be true we'd need to force lexer somehow into reaching end-of-file + // console.assert(this.done, "not all tokens from engine data were consumed") + if (typeof value === "object" && !Array.isArray(value)) { + return value; + } + throw new InvalidTopLevelEngineDataValue( + `EngineData top-level value is not a dict; is ${typeof value}` + ); + } + + private value(it?: Token): RawEngineValue { + /** + * NOTE: this is recursive descent parser - simplest solution in terms of code complexity + * In case we ever start to run into stack-depth issues + * ("RangeError: Maximum call stack size exceeded" ) + * due to parsing data that's too big, this can be re-written into stack-based one. + * That's because EngineData can be thought about as reverse-polish notation: + * ] - end of array requires popping values from stack until you hit [ + * (and pushing new value - an array - onto stack) + * same for << and >>. + */ + if (!it) { + it = this.advance(); + } + switch (it.type) { + case TokenType.Name: + case TokenType.Number: + case TokenType.Boolean: + case TokenType.String: + return it.value; + case TokenType.DictBeg: + return this.dict(); + case TokenType.ArrBeg: + return this.arr(); + } + throw new UnexpectedEngineDataToken( + `Unexpected token: ${TokenType[it.type]}` + ); + } + + private advance(): Token { + const it = this.tokens.next(); + // this.done = Boolean(it.done); + if (!it.value) { + throw new UnexpectedEndOfEngineData("End of stream"); + } + return it.value; + } + + private dict(): RawEngineData { + const val = {} as RawEngineData; + for (;;) { + const it = this.advance(); + if (it.type === TokenType.DictEnd) { + return val; + } + if (it.type !== TokenType.Name) { + throw new InvalidEngineDataDictKey( + `Dict key is not Name; is ${TokenType[it.type]}` + ); + } + const value = this.value(); + val[it.value] = value; + } + } + + private arr(): RawEngineValue[] { + const val = [] as RawEngineValue[]; + for (;;) { + const it = this.advance(); + if (it.type === TokenType.ArrEnd) { + return val; + } + const value = this.value(it); + val.push(value); + } + } +} diff --git a/packages/psd/src/engineData/validator.ts b/packages/psd/src/engineData/validator.ts new file mode 100644 index 0000000..9eb670f --- /dev/null +++ b/packages/psd/src/engineData/validator.ts @@ -0,0 +1,40 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +import {EngineData} from "../interfaces"; + +const REQUIRED_KEYS = new Set([ + "DocumentResources", + "EngineDict", + "ResourceDict", +]); + +function hasOwnProperty( + obj: unknown, + prop: K +): obj is Record { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +export function validateEngineData( + engineData: unknown +): engineData is EngineData { + let ok = true; + if (typeof engineData !== "object") { + return false; + } + if (!engineData) { + return false; + } + for (const key of REQUIRED_KEYS) { + if (hasOwnProperty(engineData, key)) { + const value = engineData[key]; + ok &&= + typeof value === "object" && !Array.isArray(value) && Boolean(value); + } else { + return false; + } + } + return ok; +} diff --git a/packages/psd/src/interfaces/EngineData.ts b/packages/psd/src/interfaces/EngineData.ts new file mode 100644 index 0000000..c2784e5 --- /dev/null +++ b/packages/psd/src/interfaces/EngineData.ts @@ -0,0 +1,10 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +export interface EngineData { + // TODO: Check if these properties are consistent between PS versions + DocumentResources: Readonly>; + EngineDict: Readonly>; + ResourceDict: Readonly>; +} diff --git a/packages/psd/src/interfaces/index.ts b/packages/psd/src/interfaces/index.ts index 62d7c69..40f09c3 100644 --- a/packages/psd/src/interfaces/index.ts +++ b/packages/psd/src/interfaces/index.ts @@ -17,3 +17,4 @@ export * from "./GroupDivider"; export * from "./ParsingResult"; export * from "./resources"; export * from "./Reference"; +export * from "./EngineData"; diff --git a/packages/psd/src/methods/index.ts b/packages/psd/src/methods/index.ts index 87a5736..a25b05d 100644 --- a/packages/psd/src/methods/index.ts +++ b/packages/psd/src/methods/index.ts @@ -6,3 +6,4 @@ export * from "./applyOpacity"; export * from "./generateRgba"; export * from "./parse"; export * from "./readDescriptor"; +export * from "./parseEngineData"; diff --git a/packages/psd/src/methods/parseEngineData.ts b/packages/psd/src/methods/parseEngineData.ts new file mode 100644 index 0000000..0f5d51c --- /dev/null +++ b/packages/psd/src/methods/parseEngineData.ts @@ -0,0 +1,21 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +import {Lexer, Parser, validateEngineData} from "../engineData"; +import {EngineData} from "../interfaces"; +import {Cursor, MissingEngineDataProperties} from "../utils"; + +export function parseEngineData(raw: Uint8Array): EngineData { + const value = new Parser( + new Lexer(new Cursor(new DataView(raw.buffer, raw.byteOffset))).tokens() + ).parse(); + if (validateEngineData(value)) { + return value; + } + throw new MissingEngineDataProperties( + `Object with keys ${JSON.stringify( + Object.keys(value) + )} is not valid EngineData` + ); +} diff --git a/packages/psd/src/sections/LayerAndMaskInformation/interfaces.ts b/packages/psd/src/sections/LayerAndMaskInformation/interfaces.ts index fb156a5..3d9ae5f 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/interfaces.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/interfaces.ts @@ -8,6 +8,7 @@ import { ChannelBytes, ChannelKind, Clipping, + EngineData, GroupDivider, } from "../../interfaces"; @@ -30,6 +31,8 @@ export interface LayerRecord { dividerType?: GroupDivider; /** If defined, contains the text of a Text Layer. */ layerText?: string; + /** If defined, containts extra text properties */ + engineData?: EngineData; } export type LayerChannels = Map; @@ -55,6 +58,8 @@ export interface LayerProperties { groupId?: number; /** Text content of text layers */ text?: string; + /** Text properties */ + textProperties?: EngineData; } export const createLayerProperties = ( @@ -73,6 +78,7 @@ export const createLayerProperties = ( transparencyLocked, blendMode, layerText, + engineData, } = layerRecord; return { @@ -88,5 +94,6 @@ export const createLayerProperties = ( blendMode, groupId, text: layerText, + textProperties: engineData, }; }; diff --git a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts index 4610ffc..95b7b70 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts @@ -15,7 +15,9 @@ import { matchBlendMode, matchChannelCompression, matchClipping, + RawDataDescriptorValue, } from "../../interfaces"; +import {parseEngineData} from "../../methods"; import {Cursor, InvalidBlendingModeSignature} from "../../utils"; import {readAdditionalLayerInfo} from "./AdditionalLayerInfo"; import {LayerChannels, LayerRecord} from "./interfaces"; @@ -137,6 +139,7 @@ function readLayerRecord( // properties on the LayerRecord object let dividerType: LayerRecord["dividerType"]; let layerText: LayerRecord["layerText"]; + let engineData: LayerRecord["engineData"]; for (const ali of additionalLayerInfos) { if (ali._isUnknown) continue; @@ -150,6 +153,13 @@ function readLayerRecord( if (textValue && textValue.type === DescriptorValueType.String) { layerText = textValue.value; } + const rawEngineData = ali.textData.descriptor.items.get("EngineData"); + if ( + rawEngineData && + rawEngineData.type === DescriptorValueType.RawData + ) { + engineData = parseEngineData(rawEngineData.data); + } break; } case AliKey.UnicodeLayerName: @@ -174,6 +184,7 @@ function readLayerRecord( additionalLayerInfos, dividerType, layerText, + engineData, }; } diff --git a/packages/psd/src/utils/bytes.ts b/packages/psd/src/utils/bytes.ts index 94cadcd..c719feb 100644 --- a/packages/psd/src/utils/bytes.ts +++ b/packages/psd/src/utils/bytes.ts @@ -118,6 +118,10 @@ export class Cursor { this.position += length; } + unpass(length: number): void { + this.position -= length; + } + /** * Creates a `Uint8Array` that covers the underlying `ArrayBuffer` of this * cursor, starting at the current cursor position and spanning a @@ -154,6 +158,14 @@ export class Cursor { return bytes; } + /** + * Returns subsequent byte, without advancing position + */ + peek(): number { + // dataView throws RangeError if position is outside bounds + return this.dataView.getUint8(this.position); + } + /** * Reads a number at the current cursor position, using the given {@link type} * (big endian). diff --git a/packages/psd/src/utils/error.ts b/packages/psd/src/utils/error.ts index 9bf915c..b2308bc 100644 --- a/packages/psd/src/utils/error.ts +++ b/packages/psd/src/utils/error.ts @@ -77,3 +77,15 @@ export class MissingDescriptorKey extends PsdError {} export class UnexpectedDescriptorValueType extends PsdError {} export class InvalidReferenceType extends PsdError {} export class InvalidUnitFloatType extends PsdError {} + +// EngineData errors +/** Thrown when lexer fails to parse respective type */ +export class InvalidEngineDataBoolean extends PsdError {} +export class InvalidEngineDataNumber extends PsdError {} +/** Thrown when top-level value is not a dict */ +export class InvalidTopLevelEngineDataValue extends PsdError {} +export class UnexpectedEndOfEngineData extends PsdError {} +export class UnexpectedEngineDataToken extends PsdError {} +export class InvalidEngineDataDictKey extends PsdError {} +export class InvalidEngineDataTextBOM extends PsdError {} +export class MissingEngineDataProperties extends PsdError {} diff --git a/packages/psd/tests/integration/engineData.test.ts b/packages/psd/tests/integration/engineData.test.ts new file mode 100644 index 0000000..c908dd0 --- /dev/null +++ b/packages/psd/tests/integration/engineData.test.ts @@ -0,0 +1,23 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +import * as fs from "fs"; +import * as path from "path"; +import {beforeAll, describe, it} from "vitest"; + +import PSD from "../../src/index"; + +const FIXTURE_DIR = path.join(__dirname, "fixtures"); + +describe(`@webtoon/psd reads EngineData`, () => { + let data: ArrayBuffer; + + beforeAll(() => { + data = fs.readFileSync(path.resolve(FIXTURE_DIR, "engineData.psd")).buffer; + }); + + it(`should parse the file successfully`, () => { + PSD.parse(data); + }); +}); diff --git a/packages/psd/tests/integration/fixtures/engineData.psd b/packages/psd/tests/integration/fixtures/engineData.psd new file mode 100644 index 0000000000000000000000000000000000000000..a560ab9ff61a45c3eb7ee2be207269cd0712c263 GIT binary patch literal 493834 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�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.$%1BK4Ui_fQOAGZ{@4 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�tjRC;aS?0O~#DmRu~tiM>ve_e%qsh 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 { + it("should parse complex file", () => { + const data = fs.readFileSync(path.resolve(FIXTURE_DIR, "engineData.bin")); + const expected = JSON.parse( + fs.readFileSync(path.resolve(FIXTURE_DIR, "engineData.json"), { + encoding: "utf8", + }) + ); + expect(parseEngineData(data)).toStrictEqual(expected); + }); + + it("should blame invalid file", () => { + const data = new Uint8Array([ + 0x3c, // < + 0x3c, // < + 0x2f, // / + 0x61, // a + 0x62, // b + 0x63, // c + 0x20, // ' ' + 0x2f, // / + 0x61, // a + 0x62, // b + 0x63, // c + 0x3e, // > + 0x3e, // > + ]); + expect(() => parseEngineData(data)).toThrowError( + MissingEngineDataProperties + ); + }); +}); + +describe("Lexer", () => { + it("should decode text", () => { + const data = [ + 0x28, // ( + 0xfe, // BOM - first marker + 0xff, // BOM - 2nd marker + 0x00, // padding + 0x61, // a + 0x00, // padding + 0x62, // b + 0x00, // padding + 0x63, // c + 0x29, // ) + ]; + const result = new Lexer( + new Cursor(new DataView(new Uint8Array(data).buffer)) + ).tokens(); + const tokens = Array.from(result); + expect(tokens).toStrictEqual([{type: TokenType.String, value: "abc"}]); + }); + + it("should recognize opening and closing of structures", () => { + const data = [ + 0x3c, // < + 0x3c, // < + 0x3e, // > + 0x3e, // > + 0x5b, // [ + 0x5d, // ] + ]; + const result = new Lexer( + new Cursor(new DataView(new Uint8Array(data).buffer)) + ).tokens(); + const tokens = Array.from(result); + expect(tokens).toStrictEqual([ + {type: TokenType.DictBeg}, + {type: TokenType.DictEnd}, + {type: TokenType.ArrBeg}, + {type: TokenType.ArrEnd}, + ]); + }); + + it("should recognize names", () => { + const data = [ + 0x2f, // / + 0x61, // a + 0x62, // b + 0x63, // c + 0x20, // ' ' + 0x2f, // / + 0x61, // a + 0x62, // b + 0x63, // c + ]; + const result = new Lexer( + new Cursor(new DataView(new Uint8Array(data).buffer)) + ).tokens(); + const tokens = Array.from(result); + expect(tokens).toStrictEqual([ + {type: TokenType.Name, value: "abc"}, + {type: TokenType.Name, value: "abc"}, + ]); + }); + + it("should recognize numbers", () => { + const data = [ + 0x2e, // . + 0x38, // 8 + 0x20, // ' ' + 0x31, // 1 + 0x2e, // . + 0x32, // 2 + 0x20, // ' ' + 0x33, // 3 + ]; + const result = new Lexer( + new Cursor(new DataView(new Uint8Array(data).buffer)) + ).tokens(); + const tokens = Array.from(result); + expect(tokens).toStrictEqual([ + {type: TokenType.Number, value: 0.8}, + {type: TokenType.Number, value: 1.2}, + {type: TokenType.Number, value: 3}, + ]); + }); + + it("should recognize booleans", () => { + const data = [ + 0x66, + 0x61, + 0x6c, + 0x73, + 0x65, // false + 0x20, // ' ' + 0x74, + 0x72, + 0x75, + 0x65, // true + ]; + const result = new Lexer( + new Cursor(new DataView(new Uint8Array(data).buffer)) + ).tokens(); + const tokens = Array.from(result); + expect(tokens).toStrictEqual([ + {type: TokenType.Boolean, value: false}, + {type: TokenType.Boolean, value: true}, + ]); + }); + + it("should treat delimiters within text properly", () => { + const data = [ + 0x28, // ( + 0xfe, // BOM - first marker + 0xff, // BOM - 2nd marker + 0x00, // padding + 0x61, // a + 0x5c, // \ + 0x29, // ) - escaped so should be ignored + 0x00, // padding + 0x62, // b + 0x00, // padding + 0x63, // c + 0x00, // padding - erronous - should be ignored + 0x5c, // \ + 0x29, // ) - escaped so should be ignored + 0x00, // padding + 0x64, // d + 0x29, // ) + ]; + const result = new Lexer( + new Cursor(new DataView(new Uint8Array(data).buffer)) + ).tokens(); + const tokens = Array.from(result); + expect(tokens).toStrictEqual([{type: TokenType.String, value: "a)bc)d"}]); + }); +}); diff --git a/packages/psd/tests/unit/fixtures/engineData.bin b/packages/psd/tests/unit/fixtures/engineData.bin new file mode 100644 index 0000000000000000000000000000000000000000..1e1a9b22d63d56844cc9aae95c93936ddeb056f6 GIT binary patch literal 12298 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 Le)($p#*P00vBP$D literal 0 HcmV?d00001 diff --git a/packages/psd/tests/unit/fixtures/engineData.json b/packages/psd/tests/unit/fixtures/engineData.json new file mode 100644 index 0000000..5f47bc5 --- /dev/null +++ b/packages/psd/tests/unit/fixtures/engineData.json @@ -0,0 +1,533 @@ +{ + "DocumentResources": { + "FontSet": [ + { + "FontType": 0, + "Name": "TrajanColor-Concept", + "Script": 0, + "Synthetic": 0 + }, + { + "FontType": 0, + "Name": "AdobeInvisFont", + "Script": 0, + "Synthetic": 0 + }, + { + "FontType": 0, + "Name": "MyriadPro-Regular", + "Script": 0, + "Synthetic": 0 + } + ], + "KinsokuSet": [ + { + "Hanging": "、。.,", + "Keep": "―‥", + "Name": "PhotoshopKinsokuHard", + "NoEnd": "‘“(〔[{〈《「『【([{¥$£@§〒#", + "NoStart": "、。,.・:;?!ー―’”)〕]}〉》」』】ヽヾゝゞ々ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ゛゜?!)]},.:;℃℉¢%‰" + }, + { + "Hanging": "、。.,", + "Keep": "―‥", + "Name": "PhotoshopKinsokuSoft", + "NoEnd": "‘“(〔[{〈《「『【", + "NoStart": "、。,.・:;?!’”)〕]}〉》」』】ヽヾゝゞ々" + } + ], + "MojiKumiSet": [ + { + "InternalName": "Photoshop6MojiKumiSet1" + }, + { + "InternalName": "Photoshop6MojiKumiSet2" + }, + { + "InternalName": "Photoshop6MojiKumiSet3" + }, + { + "InternalName": "Photoshop6MojiKumiSet4" + } + ], + "ParagraphSheetSet": [ + { + "DefaultStyleSheet": 0, + "Name": "Normal RGB", + "Properties": { + "AutoHyphenate": true, + "AutoLeading": 1.2, + "Burasagari": false, + "ConsecutiveHyphens": 8, + "EndIndent": 0.0, + "EveryLineComposer": false, + "FirstLineIndent": 0.0, + "GlyphSpacing": [1.0, 1.0, 1.0], + "Hanging": false, + "HyphenatedWordSize": 6, + "Justification": 0, + "KinsokuOrder": 0, + "LeadingType": 0, + "LetterSpacing": [0.0, 0.0, 0.0], + "PostHyphen": 2, + "PreHyphen": 2, + "SpaceAfter": 0.0, + "SpaceBefore": 0.0, + "StartIndent": 0.0, + "WordSpacing": [0.8, 1.0, 1.33], + "Zone": 36.0 + } + } + ], + "SmallCapSize": 0.7, + "StyleSheetSet": [ + { + "Name": "Normal RGB", + "StyleSheetData": { + "AutoKerning": true, + "AutoLeading": true, + "BaselineDirection": 2, + "BaselineShift": 0.0, + "CharacterDirection": 0, + "DLigatures": false, + "DiacriticPos": 2, + "FauxBold": false, + "FauxItalic": false, + "FillColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "FillFirst": true, + "FillFlag": true, + "Font": 2, + "FontBaseline": 0, + "FontCaps": 0, + "FontSize": 12.0, + "HindiNumbers": false, + "HorizontalScale": 1.0, + "Kashida": 1, + "Kerning": 0, + "Language": 0, + "Leading": 0.0, + "Ligatures": true, + "NoBreak": false, + "OutlineWidth": 1.0, + "Strikethrough": false, + "StrokeColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "StrokeFlag": false, + "StyleRunAlignment": 2, + "Tracking": 0, + "Tsume": 0.0, + "Underline": false, + "VerticalScale": 1.0, + "YUnderline": 1 + } + } + ], + "SubscriptPosition": 0.333, + "SubscriptSize": 0.583, + "SuperscriptPosition": 0.333, + "SuperscriptSize": 0.583, + "TheNormalParagraphSheet": 0, + "TheNormalStyleSheet": 0 + }, + "EngineDict": { + "AntiAlias": 1, + "Editor": { + "Text": "Morbi vitae diam bibendum, ultricies nulla ut, tempor tellus. Praesent nec maximus nunc. Morbi varius a elit a egestas. Morbi efficitur metus purus. Etiam consectetur non tortor sit amet feugiat. Quisque vel varius mi, vel consequat leo. Suspendisse placerat mauris auctor nulla porta lobortis. Praesent egestas justo nisi, ac pellentesque mauris vulputate vitae. Morbi ac lorem ornare, venenatis tortor facilisis, gravida ipsum. Nunc a dictum felis. Aliquam imperdiet eget erat quis malesuada. Phasellus nisl ipsum, aliquam bibendum viverra nec, bibendum sit amet diam. Cras nunc ligula, vulputate a erat et, fringilla egestas diam.\r" + }, + "GridInfo": { + "AlignLineHeightToGridFlags": false, + "GridColor": { + "Type": 1, + "Values": [0.0, 0.0, 0.0, 1.0] + }, + "GridIsOn": false, + "GridLeading": 22.0, + "GridLeadingFillColor": { + "Type": 1, + "Values": [0.0, 0.0, 0.0, 1.0] + }, + "GridSize": 18.0, + "ShowGrid": false + }, + "ParagraphRun": { + "DefaultRunData": { + "Adjustments": { + "Axis": [1.0, 0.0, 1.0], + "XY": [0.0, 0.0] + }, + "ParagraphSheet": { + "DefaultStyleSheet": 0, + "Properties": {} + } + }, + "IsJoinable": 1, + "RunArray": [ + { + "Adjustments": { + "Axis": [1.0, 0.0, 1.0], + "XY": [0.0, 0.0] + }, + "ParagraphSheet": { + "DefaultStyleSheet": 0, + "Properties": { + "AutoHyphenate": false, + "AutoLeading": 1.2, + "Burasagari": false, + "ConsecutiveHyphens": 8, + "EndIndent": 0.0, + "EveryLineComposer": false, + "FirstLineIndent": 0.0, + "GlyphSpacing": [1.0, 1.0, 1.0], + "Hanging": false, + "HyphenatedWordSize": 6, + "Justification": 0, + "KinsokuOrder": 0, + "LeadingType": 0, + "LetterSpacing": [0.0, 0.0, 0.0], + "PostHyphen": 2, + "PreHyphen": 2, + "SpaceAfter": 0.0, + "SpaceBefore": 0.0, + "StartIndent": 0.0, + "WordSpacing": [0.8, 1.0, 1.33], + "Zone": 36.0 + } + } + } + ], + "RunLengthArray": [634] + }, + "Rendered": { + "Shapes": { + "Children": [ + { + "Cookie": { + "Photoshop": { + "Base": { + "ShapeType": 1, + "TransformPoint0": [1.0, 0.0], + "TransformPoint1": [0.0, 1.0], + "TransformPoint2": [0.0, 0.0] + }, + "BoxBounds": [0.0, 0.0, 1512.0, 2668.93262], + "ShapeType": 1 + } + }, + "Lines": { + "Children": [], + "WritingDirection": 0 + }, + "Procession": 0, + "ShapeType": 1 + } + ], + "WritingDirection": 0 + }, + "Version": 1 + }, + "StyleRun": { + "DefaultRunData": { + "StyleSheet": { + "StyleSheetData": {} + } + }, + "IsJoinable": 2, + "RunArray": [ + { + "StyleSheet": { + "StyleSheetData": { + "AutoKerning": true, + "AutoLeading": false, + "BaselineDirection": 1, + "BaselineShift": 0.0, + "DLigatures": false, + "DiacriticPos": 2, + "FauxBold": false, + "FauxItalic": false, + "FillColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "Font": 0, + "FontBaseline": 0, + "FontCaps": 0, + "FontSize": 124.99998, + "HindiNumbers": false, + "HorizontalScale": 1.0, + "Kashida": 1, + "Kerning": 0, + "Language": 2, + "Leading": 50.0, + "Ligatures": true, + "Strikethrough": false, + "StrokeColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "Tracking": 0, + "Underline": false, + "VerticalScale": 1.0, + "YUnderline": 1 + } + } + }, + { + "StyleSheet": { + "StyleSheetData": { + "AutoKerning": true, + "AutoLeading": false, + "BaselineDirection": 1, + "BaselineShift": 0.0, + "DLigatures": false, + "DiacriticPos": 2, + "FauxBold": false, + "FauxItalic": false, + "FillColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "Font": 2, + "FontBaseline": 0, + "FontCaps": 0, + "FontSize": 32.0, + "HindiNumbers": false, + "HorizontalScale": 1.0, + "Kashida": 1, + "Kerning": 0, + "Language": 2, + "Leading": 50.0, + "Ligatures": true, + "Strikethrough": false, + "StrokeColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "Tracking": 0, + "Underline": false, + "VerticalScale": 1.0, + "YUnderline": 1 + } + } + }, + { + "StyleSheet": { + "StyleSheetData": { + "AutoKerning": true, + "AutoLeading": false, + "BaselineDirection": 1, + "BaselineShift": 0.0, + "DLigatures": false, + "DiacriticPos": 2, + "FauxBold": false, + "FauxItalic": false, + "FillColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "Font": 2, + "FontBaseline": 0, + "FontCaps": 0, + "FontSize": 45.83333, + "HindiNumbers": false, + "HorizontalScale": 1.0, + "Kashida": 1, + "Kerning": 0, + "Language": 2, + "Leading": 50.0, + "Ligatures": true, + "Strikethrough": false, + "StrokeColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "Tracking": 0, + "Underline": false, + "VerticalScale": 1.0, + "YUnderline": 1 + } + } + }, + { + "StyleSheet": { + "StyleSheetData": { + "AutoKerning": true, + "AutoLeading": false, + "BaselineDirection": 1, + "BaselineShift": 0.0, + "DLigatures": false, + "DiacriticPos": 2, + "FauxBold": false, + "FauxItalic": false, + "FillColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "Font": 2, + "FontBaseline": 0, + "FontCaps": 0, + "FontSize": 32.0, + "HindiNumbers": false, + "HorizontalScale": 1.0, + "Kashida": 1, + "Kerning": 0, + "Language": 2, + "Leading": 50.0, + "Ligatures": true, + "Strikethrough": false, + "StrokeColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "Tracking": 0, + "Underline": false, + "VerticalScale": 1.0, + "YUnderline": 1 + } + } + } + ], + "RunLengthArray": [1, 450, 43, 140] + }, + "UseFractionalGlyphWidths": true + }, + "ResourceDict": { + "FontSet": [ + { + "FontType": 0, + "Name": "TrajanColor-Concept", + "Script": 0, + "Synthetic": 0 + }, + { + "FontType": 0, + "Name": "AdobeInvisFont", + "Script": 0, + "Synthetic": 0 + }, + { + "FontType": 0, + "Name": "MyriadPro-Regular", + "Script": 0, + "Synthetic": 0 + } + ], + "KinsokuSet": [ + { + "Hanging": "、。.,", + "Keep": "―‥", + "Name": "PhotoshopKinsokuHard", + "NoEnd": "‘“(〔[{〈《「『【([{¥$£@§〒#", + "NoStart": "、。,.・:;?!ー―’”)〕]}〉》」』】ヽヾゝゞ々ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ゛゜?!)]},.:;℃℉¢%‰" + }, + { + "Hanging": "、。.,", + "Keep": "―‥", + "Name": "PhotoshopKinsokuSoft", + "NoEnd": "‘“(〔[{〈《「『【", + "NoStart": "、。,.・:;?!’”)〕]}〉》」』】ヽヾゝゞ々" + } + ], + "MojiKumiSet": [ + { + "InternalName": "Photoshop6MojiKumiSet1" + }, + { + "InternalName": "Photoshop6MojiKumiSet2" + }, + { + "InternalName": "Photoshop6MojiKumiSet3" + }, + { + "InternalName": "Photoshop6MojiKumiSet4" + } + ], + "ParagraphSheetSet": [ + { + "DefaultStyleSheet": 0, + "Name": "Normal RGB", + "Properties": { + "AutoHyphenate": true, + "AutoLeading": 1.2, + "Burasagari": false, + "ConsecutiveHyphens": 8, + "EndIndent": 0.0, + "EveryLineComposer": false, + "FirstLineIndent": 0.0, + "GlyphSpacing": [1.0, 1.0, 1.0], + "Hanging": false, + "HyphenatedWordSize": 6, + "Justification": 0, + "KinsokuOrder": 0, + "LeadingType": 0, + "LetterSpacing": [0.0, 0.0, 0.0], + "PostHyphen": 2, + "PreHyphen": 2, + "SpaceAfter": 0.0, + "SpaceBefore": 0.0, + "StartIndent": 0.0, + "WordSpacing": [0.8, 1.0, 1.33], + "Zone": 36.0 + } + } + ], + "SmallCapSize": 0.7, + "StyleSheetSet": [ + { + "Name": "Normal RGB", + "StyleSheetData": { + "AutoKerning": true, + "AutoLeading": true, + "BaselineDirection": 2, + "BaselineShift": 0.0, + "CharacterDirection": 0, + "DLigatures": false, + "DiacriticPos": 2, + "FauxBold": false, + "FauxItalic": false, + "FillColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "FillFirst": true, + "FillFlag": true, + "Font": 2, + "FontBaseline": 0, + "FontCaps": 0, + "FontSize": 12.0, + "HindiNumbers": false, + "HorizontalScale": 1.0, + "Kashida": 1, + "Kerning": 0, + "Language": 0, + "Leading": 0.0, + "Ligatures": true, + "NoBreak": false, + "OutlineWidth": 1.0, + "Strikethrough": false, + "StrokeColor": { + "Type": 1, + "Values": [1.0, 0.0, 0.0, 0.0] + }, + "StrokeFlag": false, + "StyleRunAlignment": 2, + "Tracking": 0, + "Tsume": 0.0, + "Underline": false, + "VerticalScale": 1.0, + "YUnderline": 1 + } + } + ], + "SubscriptPosition": 0.333, + "SubscriptSize": 0.583, + "SuperscriptPosition": 0.333, + "SuperscriptSize": 0.583, + "TheNormalParagraphSheet": 0, + "TheNormalStyleSheet": 0 + } +} From 5c63fc90b1b2721f8c08905570cca0a2025dd816 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Wed, 3 Aug 2022 12:38:47 +0200 Subject: [PATCH 2/7] avoid .at() to target Node<16 --- packages/psd/src/engineData/lexer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/psd/src/engineData/lexer.ts b/packages/psd/src/engineData/lexer.ts index 719b9fe..ff23a52 100644 --- a/packages/psd/src/engineData/lexer.ts +++ b/packages/psd/src/engineData/lexer.ts @@ -145,7 +145,7 @@ export class Lexer { if (readAhead.peek() === Delimiters["\\"]) { const length = readAhead.position - this.cursor.position; let raw = this.cursor.take(length); - if (raw.at(-1) === 0x00) { + if (raw[length -1] === 0x00) { // Sometimes there's extra padding before - we need to remove it raw = raw.subarray(0, -1); } From 05c4599cf4cf99118cbee370920fc5f5337947d2 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Wed, 3 Aug 2022 13:28:30 +0200 Subject: [PATCH 3/7] fix CJK handling --- packages/psd/src/engineData/lexer.ts | 15 +++++------- .../psd/tests/integration/engineData.test.ts | 14 ++++++----- .../psd/tests/integration/fixtures/CJK.psd | Bin 0 -> 323890 bytes packages/psd/tests/unit/engineData.test.ts | 22 +++++++++++++++++- 4 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 packages/psd/tests/integration/fixtures/CJK.psd diff --git a/packages/psd/src/engineData/lexer.ts b/packages/psd/src/engineData/lexer.ts index ff23a52..893f589 100644 --- a/packages/psd/src/engineData/lexer.ts +++ b/packages/psd/src/engineData/lexer.ts @@ -144,15 +144,12 @@ export class Lexer { readAhead.pass(1); if (readAhead.peek() === Delimiters["\\"]) { const length = readAhead.position - this.cursor.position; - let raw = this.cursor.take(length); - if (raw[length -1] === 0x00) { - // Sometimes there's extra padding before - we need to remove it - raw = raw.subarray(0, -1); - } - textParts.push(decoder.decode(raw)); - readAhead.pass(1); // skip over \\ - textParts.push(String.fromCharCode(readAhead.take(1)[0])); // un-escape character - this.cursor.pass(2); // skip over escaped character to avoid decoding it in subsequent part + textParts.push( + decoder.decode(this.cursor.take(length), {stream: true}) + ); + readAhead.pass(2); // skip over \\ + this.cursor.pass(1); // skip over escaped character to avoid decoding it in subsequent part + textParts.push(decoder.decode(this.cursor.take(1), {stream: true})); // push un-escaped character } } const length = readAhead.position - this.cursor.position; diff --git a/packages/psd/tests/integration/engineData.test.ts b/packages/psd/tests/integration/engineData.test.ts index c908dd0..5991c62 100644 --- a/packages/psd/tests/integration/engineData.test.ts +++ b/packages/psd/tests/integration/engineData.test.ts @@ -4,20 +4,22 @@ import * as fs from "fs"; import * as path from "path"; -import {beforeAll, describe, it} from "vitest"; +import {describe, it} from "vitest"; import PSD from "../../src/index"; const FIXTURE_DIR = path.join(__dirname, "fixtures"); describe(`@webtoon/psd reads EngineData`, () => { - let data: ArrayBuffer; - - beforeAll(() => { - data = fs.readFileSync(path.resolve(FIXTURE_DIR, "engineData.psd")).buffer; + it(`should parse the file successfully`, () => { + const data = fs.readFileSync( + path.resolve(FIXTURE_DIR, "engineData.psd") + ).buffer; + PSD.parse(data); }); - it(`should parse the file successfully`, () => { + it(`should parse CJK text successfully`, () => { + const data = fs.readFileSync(path.resolve(FIXTURE_DIR, "CJK.psd")).buffer; PSD.parse(data); }); }); diff --git a/packages/psd/tests/integration/fixtures/CJK.psd b/packages/psd/tests/integration/fixtures/CJK.psd new file mode 100644 index 0000000000000000000000000000000000000000..1c69a85a48d30f124cacb7e9c7c780828149e3e9 GIT binary patch literal 323890 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 literal 0 HcmV?d00001 diff --git a/packages/psd/tests/unit/engineData.test.ts b/packages/psd/tests/unit/engineData.test.ts index abcd1be..9ff63c6 100644 --- a/packages/psd/tests/unit/engineData.test.ts +++ b/packages/psd/tests/unit/engineData.test.ts @@ -161,13 +161,14 @@ describe("Lexer", () => { 0xff, // BOM - 2nd marker 0x00, // padding 0x61, // a + 0x00, // padding 0x5c, // \ 0x29, // ) - escaped so should be ignored 0x00, // padding 0x62, // b 0x00, // padding 0x63, // c - 0x00, // padding - erronous - should be ignored + 0x00, // padding 0x5c, // \ 0x29, // ) - escaped so should be ignored 0x00, // padding @@ -180,4 +181,23 @@ describe("Lexer", () => { const tokens = Array.from(result); expect(tokens).toStrictEqual([{type: TokenType.String, value: "a)bc)d"}]); }); + + it("should parse CJK text properly", () => { + const data = [ + 0x28, // ( + 0xfe, // BOM - first marker + 0xff, // BOM - 2nd marker + 0xd4, + 0x5c, + 0x5c, + 0xc9, + 0x00, + 0x29, // ) + ]; + const result = new Lexer( + new Cursor(new DataView(new Uint8Array(data).buffer)) + ).tokens(); + const tokens = Array.from(result); + expect(tokens).toStrictEqual([{type: TokenType.String, value: "표준"}]); + }); }); From afe4b76a83a660addcb5b94c323ab4f47a31daa9 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Wed, 3 Aug 2022 15:04:55 +0200 Subject: [PATCH 4/7] migrate parser to stack-based --- packages/psd/src/engineData/parser.ts | 108 ++++++++++---------- packages/psd/src/methods/parseEngineData.ts | 4 +- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/packages/psd/src/engineData/parser.ts b/packages/psd/src/engineData/parser.ts index acf962d..d94c0ab 100644 --- a/packages/psd/src/engineData/parser.ts +++ b/packages/psd/src/engineData/parser.ts @@ -6,7 +6,6 @@ import { InvalidEngineDataDictKey, InvalidTopLevelEngineDataValue, UnexpectedEndOfEngineData, - UnexpectedEngineDataToken, } from "../utils"; import {Token, TokenType} from "./lexer"; @@ -20,14 +19,20 @@ export type RawEngineValue = | RawEngineValue[] | RawEngineData; +const ARR_BOUNDARY = Symbol(TokenType[TokenType.ArrBeg]); +const DICT_BOUNDARY = Symbol(TokenType[TokenType.DictBeg]); + export class Parser { - // private done: boolean = false + private stack: ( + | RawEngineValue + | typeof ARR_BOUNDARY + | typeof DICT_BOUNDARY + )[] = []; constructor(private tokens: Generator) {} parse(): RawEngineData { - const value = this.value(); - // TODO: for this to be true we'd need to force lexer somehow into reaching end-of-file - // console.assert(this.done, "not all tokens from engine data were consumed") + this.runParser(); + const [value] = this.stack; if (typeof value === "object" && !Array.isArray(value)) { return value; } @@ -36,71 +41,70 @@ export class Parser { ); } - private value(it?: Token): RawEngineValue { - /** - * NOTE: this is recursive descent parser - simplest solution in terms of code complexity - * In case we ever start to run into stack-depth issues - * ("RangeError: Maximum call stack size exceeded" ) - * due to parsing data that's too big, this can be re-written into stack-based one. - * That's because EngineData can be thought about as reverse-polish notation: - * ] - end of array requires popping values from stack until you hit [ - * (and pushing new value - an array - onto stack) - * same for << and >>. - */ - if (!it) { - it = this.advance(); - } - switch (it.type) { - case TokenType.Name: - case TokenType.Number: - case TokenType.Boolean: - case TokenType.String: - return it.value; - case TokenType.DictBeg: - return this.dict(); - case TokenType.ArrBeg: - return this.arr(); - } - throw new UnexpectedEngineDataToken( - `Unexpected token: ${TokenType[it.type]}` - ); - } - - private advance(): Token { - const it = this.tokens.next(); - // this.done = Boolean(it.done); - if (!it.value) { - throw new UnexpectedEndOfEngineData("End of stream"); + private runParser() { + for (const it of this.tokens) { + switch (it.type) { + case TokenType.Name: + case TokenType.Number: + case TokenType.Boolean: + case TokenType.String: + this.stack.push(it.value); + continue; + case TokenType.DictBeg: + this.stack.push(DICT_BOUNDARY); + continue; + case TokenType.ArrBeg: + this.stack.push(ARR_BOUNDARY); + continue; + case TokenType.DictEnd: + this.stack.push(this.dict()); + continue; + case TokenType.ArrEnd: + this.stack.push(this.array().reverse()); + continue; + } } - return it.value; } private dict(): RawEngineData { const val = {} as RawEngineData; for (;;) { - const it = this.advance(); - if (it.type === TokenType.DictEnd) { + const value = this.stack.pop(); + // TODO: new error types? + if (value === undefined) { + throw new UnexpectedEndOfEngineData("Stack empty when parsing dict"); + } + if (value === DICT_BOUNDARY) { return val; } - if (it.type !== TokenType.Name) { + if (value === ARR_BOUNDARY) { + throw new InvalidEngineDataDictKey("Got ArrBeg while parsing a dict"); + } + const it = this.stack.pop(); + if (typeof it !== "string") { throw new InvalidEngineDataDictKey( - `Dict key is not Name; is ${TokenType[it.type]}` + `Dict key is not Name; is ${typeof it}` ); } - const value = this.value(); - val[it.value] = value; + val[it] = value; } } - private arr(): RawEngineValue[] { + private array(): RawEngineValue[] { const val = [] as RawEngineValue[]; for (;;) { - const it = this.advance(); - if (it.type === TokenType.ArrEnd) { + const it = this.stack.pop(); + // TODO: new error types? + if (it === undefined) { + throw new UnexpectedEndOfEngineData("Stack empty when parsing array"); + } + if (it === DICT_BOUNDARY) { + throw new InvalidEngineDataDictKey("Got DictBeg while parsing array"); + } + if (it === ARR_BOUNDARY) { return val; } - const value = this.value(it); - val.push(value); + val.push(it); } } } diff --git a/packages/psd/src/methods/parseEngineData.ts b/packages/psd/src/methods/parseEngineData.ts index 0f5d51c..7c77bea 100644 --- a/packages/psd/src/methods/parseEngineData.ts +++ b/packages/psd/src/methods/parseEngineData.ts @@ -8,7 +8,9 @@ import {Cursor, MissingEngineDataProperties} from "../utils"; export function parseEngineData(raw: Uint8Array): EngineData { const value = new Parser( - new Lexer(new Cursor(new DataView(raw.buffer, raw.byteOffset))).tokens() + new Lexer( + new Cursor(new DataView(raw.buffer, raw.byteOffset, raw.length)) + ).tokens() ).parse(); if (validateEngineData(value)) { return value; From 66d81f6aa8a92051ccef56f45f3d1ca4b16c022b Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Wed, 3 Aug 2022 18:26:31 +0200 Subject: [PATCH 5/7] fix SyntaxError: Unexpected token '&&=' on Node 14 --- packages/psd/src/engineData/validator.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/psd/src/engineData/validator.ts b/packages/psd/src/engineData/validator.ts index 9eb670f..0a2bb36 100644 --- a/packages/psd/src/engineData/validator.ts +++ b/packages/psd/src/engineData/validator.ts @@ -30,8 +30,11 @@ export function validateEngineData( for (const key of REQUIRED_KEYS) { if (hasOwnProperty(engineData, key)) { const value = engineData[key]; - ok &&= - typeof value === "object" && !Array.isArray(value) && Boolean(value); + ok = + ok && + typeof value === "object" && + !Array.isArray(value) && + Boolean(value); } else { return false; } From cd40e0848c8ca76c072c3b46b886bf719a6e9568 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Tue, 9 Aug 2022 12:23:54 +0200 Subject: [PATCH 6/7] optimize lexer - array instead of Generator - heavy optimize string() function in lexer 1) slice copies buffer, 2) creating decoder is costly, 3) memoize results for each character instead of lookup --- packages/psd/src/engineData/lexer.ts | 105 ++++++++++++++------ packages/psd/src/engineData/parser.ts | 2 +- packages/psd/src/methods/parseEngineData.ts | 8 +- packages/psd/tests/unit/engineData.test.ts | 56 ++++------- 4 files changed, 101 insertions(+), 70 deletions(-) diff --git a/packages/psd/src/engineData/lexer.ts b/packages/psd/src/engineData/lexer.ts index 893f589..8dc0e0e 100644 --- a/packages/psd/src/engineData/lexer.ts +++ b/packages/psd/src/engineData/lexer.ts @@ -6,7 +6,6 @@ // Section 7.2 - Lexical Conventions import { - Cursor, InvalidEngineDataBoolean, InvalidEngineDataNumber, InvalidEngineDataTextBOM, @@ -64,12 +63,73 @@ const Delimiters = { const DelimiterCharacters = new Set(Object.values(Delimiters)); +class CursorProxy { + public position = 0; + constructor(private cursor: Uint8Array) {} + + peek() { + return this.cursor[this.position]; + } + pass(n: number) { + this.position += n; + } + one() { + const val = this.peek(); + this.position += 1; + return val; + } + unpass() { + this.pass(-1); + } + get length() { + return this.cursor.length; + } + clone() { + const val = new CursorProxy(this.cursor); + val.position = this.position; + return val; + } + take(n: number) { + const val = this.cursor.subarray(this.position, this.position + n); + this.position += n; + return val; + } + iter() { + return this.cursor.subarray(this.position); + } +} + +const STRING_TOKEN_JT = [] as boolean[]; +for (let i = 0; i < 256; i += 1) { + STRING_TOKEN_JT[i] = + WhitespaceCharacters.has(i) || DelimiterCharacters.has(i); +} + +const STRING_DECODER = new TextDecoder("utf-8"); +function stringToken(cursor: CursorProxy): string { + const startsAt = cursor.position; + let endsAt = cursor.position; + for (const i of cursor.iter()) { + if (STRING_TOKEN_JT[i]) { + break; + } + endsAt += 1; + } + const text = STRING_DECODER.decode(cursor.take(endsAt - startsAt)); + return text; +} + export class Lexer { - constructor(private cursor: Cursor) {} + cursor: CursorProxy; + + constructor(cursor: Uint8Array) { + this.cursor = new CursorProxy(cursor); + } - *tokens(): Generator { + tokens(): Token[] { + const value = [] as Token[]; while (!this.done()) { - const val = this.cursor.read("u8"); + const val = this.cursor.one(); if (WhitespaceCharacters.has(val)) { while (!this.done() && WhitespaceCharacters.has(this.cursor.peek())) @@ -78,31 +138,31 @@ export class Lexer { } if (DelimiterCharacters.has(val)) { if (val === Delimiters["("]) { - yield {type: TokenType.String, value: this.text()}; + value.push({type: TokenType.String, value: this.text()}); continue; } if (val === Delimiters["["]) { - yield {type: TokenType.ArrBeg}; + value.push({type: TokenType.ArrBeg}); continue; } if (val === Delimiters["]"]) { - yield {type: TokenType.ArrEnd}; + value.push({type: TokenType.ArrEnd}); continue; } if (val === Delimiters["<"]) { // NOTE: assert that it is < indeed? this.cursor.pass(1); - yield {type: TokenType.DictBeg}; + value.push({type: TokenType.DictBeg}); continue; } if (val === Delimiters[">"]) { // NOTE: assert that it is > indeed? this.cursor.pass(1); - yield {type: TokenType.DictEnd}; + value.push({type: TokenType.DictEnd}); continue; } if (val === Delimiters["/"]) { - yield {type: TokenType.Name, value: this.string()}; + value.push({type: TokenType.Name, value: this.string()}); continue; } console.assert( @@ -114,13 +174,14 @@ export class Lexer { } // only two types left: number or boolean // we need to return val first since it starts value - this.cursor.unpass(1); + this.cursor.unpass(); if (BooleanStartCharacters.has(val)) { - yield {type: TokenType.Boolean, value: this.boolean()}; + value.push({type: TokenType.Boolean, value: this.boolean()}); } else { - yield {type: TokenType.Number, value: this.number()}; + value.push({type: TokenType.Number, value: this.number()}); } } + return value; } private done(): boolean { @@ -160,8 +221,8 @@ export class Lexer { } private textDecoderFromBOM(): TextDecoder { - const firstBomPart = this.cursor.read("u8"); - const sndBomPart = this.cursor.read("u8"); + const firstBomPart = this.cursor.one(); + const sndBomPart = this.cursor.one(); // https://en.wikipedia.org/wiki/Byte_order_mark#UTF-16 // LE is FF FE if (firstBomPart === 0xff && sndBomPart === 0xfe) @@ -175,19 +236,7 @@ export class Lexer { } private string(): string { - const decoder = new TextDecoder("ascii"); - const readAhead = this.cursor.clone(); - while ( - !this.done() && - !WhitespaceCharacters.has(this.cursor.peek()) && - !DelimiterCharacters.has(this.cursor.peek()) - ) { - this.cursor.pass(1); - } - const text = decoder.decode( - readAhead.take(this.cursor.position - readAhead.position) - ); - return text; + return stringToken(this.cursor); } private number(): number { diff --git a/packages/psd/src/engineData/parser.ts b/packages/psd/src/engineData/parser.ts index d94c0ab..a597da6 100644 --- a/packages/psd/src/engineData/parser.ts +++ b/packages/psd/src/engineData/parser.ts @@ -28,7 +28,7 @@ export class Parser { | typeof ARR_BOUNDARY | typeof DICT_BOUNDARY )[] = []; - constructor(private tokens: Generator) {} + constructor(private tokens: Iterable) {} parse(): RawEngineData { this.runParser(); diff --git a/packages/psd/src/methods/parseEngineData.ts b/packages/psd/src/methods/parseEngineData.ts index 7c77bea..e279266 100644 --- a/packages/psd/src/methods/parseEngineData.ts +++ b/packages/psd/src/methods/parseEngineData.ts @@ -4,14 +4,10 @@ import {Lexer, Parser, validateEngineData} from "../engineData"; import {EngineData} from "../interfaces"; -import {Cursor, MissingEngineDataProperties} from "../utils"; +import {MissingEngineDataProperties} from "../utils"; export function parseEngineData(raw: Uint8Array): EngineData { - const value = new Parser( - new Lexer( - new Cursor(new DataView(raw.buffer, raw.byteOffset, raw.length)) - ).tokens() - ).parse(); + const value = new Parser(new Lexer(raw).tokens()).parse(); if (validateEngineData(value)) { return value; } diff --git a/packages/psd/tests/unit/engineData.test.ts b/packages/psd/tests/unit/engineData.test.ts index 9ff63c6..96e5348 100644 --- a/packages/psd/tests/unit/engineData.test.ts +++ b/packages/psd/tests/unit/engineData.test.ts @@ -47,7 +47,7 @@ describe("parseEngineData", () => { describe("Lexer", () => { it("should decode text", () => { - const data = [ + const data = new Uint8Array([ 0x28, // ( 0xfe, // BOM - first marker 0xff, // BOM - 2nd marker @@ -58,26 +58,22 @@ describe("Lexer", () => { 0x00, // padding 0x63, // c 0x29, // ) - ]; - const result = new Lexer( - new Cursor(new DataView(new Uint8Array(data).buffer)) - ).tokens(); + ]); + const result = new Lexer(data).tokens(); const tokens = Array.from(result); expect(tokens).toStrictEqual([{type: TokenType.String, value: "abc"}]); }); it("should recognize opening and closing of structures", () => { - const data = [ + const data = new Uint8Array([ 0x3c, // < 0x3c, // < 0x3e, // > 0x3e, // > 0x5b, // [ 0x5d, // ] - ]; - const result = new Lexer( - new Cursor(new DataView(new Uint8Array(data).buffer)) - ).tokens(); + ]); + const result = new Lexer(data).tokens(); const tokens = Array.from(result); expect(tokens).toStrictEqual([ {type: TokenType.DictBeg}, @@ -88,7 +84,7 @@ describe("Lexer", () => { }); it("should recognize names", () => { - const data = [ + const data = new Uint8Array([ 0x2f, // / 0x61, // a 0x62, // b @@ -98,10 +94,8 @@ describe("Lexer", () => { 0x61, // a 0x62, // b 0x63, // c - ]; - const result = new Lexer( - new Cursor(new DataView(new Uint8Array(data).buffer)) - ).tokens(); + ]); + const result = new Lexer(data).tokens(); const tokens = Array.from(result); expect(tokens).toStrictEqual([ {type: TokenType.Name, value: "abc"}, @@ -110,7 +104,7 @@ describe("Lexer", () => { }); it("should recognize numbers", () => { - const data = [ + const data = new Uint8Array([ 0x2e, // . 0x38, // 8 0x20, // ' ' @@ -119,10 +113,8 @@ describe("Lexer", () => { 0x32, // 2 0x20, // ' ' 0x33, // 3 - ]; - const result = new Lexer( - new Cursor(new DataView(new Uint8Array(data).buffer)) - ).tokens(); + ]); + const result = new Lexer(data).tokens(); const tokens = Array.from(result); expect(tokens).toStrictEqual([ {type: TokenType.Number, value: 0.8}, @@ -132,7 +124,7 @@ describe("Lexer", () => { }); it("should recognize booleans", () => { - const data = [ + const data = new Uint8Array([ 0x66, 0x61, 0x6c, @@ -143,10 +135,8 @@ describe("Lexer", () => { 0x72, 0x75, 0x65, // true - ]; - const result = new Lexer( - new Cursor(new DataView(new Uint8Array(data).buffer)) - ).tokens(); + ]); + const result = new Lexer(data).tokens(); const tokens = Array.from(result); expect(tokens).toStrictEqual([ {type: TokenType.Boolean, value: false}, @@ -155,7 +145,7 @@ describe("Lexer", () => { }); it("should treat delimiters within text properly", () => { - const data = [ + const data = new Uint8Array([ 0x28, // ( 0xfe, // BOM - first marker 0xff, // BOM - 2nd marker @@ -174,16 +164,14 @@ describe("Lexer", () => { 0x00, // padding 0x64, // d 0x29, // ) - ]; - const result = new Lexer( - new Cursor(new DataView(new Uint8Array(data).buffer)) - ).tokens(); + ]); + const result = new Lexer(data).tokens(); const tokens = Array.from(result); expect(tokens).toStrictEqual([{type: TokenType.String, value: "a)bc)d"}]); }); it("should parse CJK text properly", () => { - const data = [ + const data = new Uint8Array([ 0x28, // ( 0xfe, // BOM - first marker 0xff, // BOM - 2nd marker @@ -193,10 +181,8 @@ describe("Lexer", () => { 0xc9, 0x00, 0x29, // ) - ]; - const result = new Lexer( - new Cursor(new DataView(new Uint8Array(data).buffer)) - ).tokens(); + ]); + const result = new Lexer(data).tokens(); const tokens = Array.from(result); expect(tokens).toStrictEqual([{type: TokenType.String, value: "표준"}]); }); From 59a55f6455b4ca8847b8f559b06a6a49b530e58b Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Tue, 9 Aug 2022 13:43:03 +0200 Subject: [PATCH 7/7] remove CursorProxy it doesn't improve performance --- packages/psd/src/engineData/lexer.ts | 45 ++++------------------------ packages/psd/src/utils/bytes.ts | 23 ++++++++++++++ 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/packages/psd/src/engineData/lexer.ts b/packages/psd/src/engineData/lexer.ts index 8dc0e0e..8245597 100644 --- a/packages/psd/src/engineData/lexer.ts +++ b/packages/psd/src/engineData/lexer.ts @@ -6,6 +6,7 @@ // Section 7.2 - Lexical Conventions import { + Cursor, InvalidEngineDataBoolean, InvalidEngineDataNumber, InvalidEngineDataTextBOM, @@ -63,42 +64,6 @@ const Delimiters = { const DelimiterCharacters = new Set(Object.values(Delimiters)); -class CursorProxy { - public position = 0; - constructor(private cursor: Uint8Array) {} - - peek() { - return this.cursor[this.position]; - } - pass(n: number) { - this.position += n; - } - one() { - const val = this.peek(); - this.position += 1; - return val; - } - unpass() { - this.pass(-1); - } - get length() { - return this.cursor.length; - } - clone() { - const val = new CursorProxy(this.cursor); - val.position = this.position; - return val; - } - take(n: number) { - const val = this.cursor.subarray(this.position, this.position + n); - this.position += n; - return val; - } - iter() { - return this.cursor.subarray(this.position); - } -} - const STRING_TOKEN_JT = [] as boolean[]; for (let i = 0; i < 256; i += 1) { STRING_TOKEN_JT[i] = @@ -106,7 +71,7 @@ for (let i = 0; i < 256; i += 1) { } const STRING_DECODER = new TextDecoder("utf-8"); -function stringToken(cursor: CursorProxy): string { +function stringToken(cursor: Cursor): string { const startsAt = cursor.position; let endsAt = cursor.position; for (const i of cursor.iter()) { @@ -120,10 +85,10 @@ function stringToken(cursor: CursorProxy): string { } export class Lexer { - cursor: CursorProxy; + cursor: Cursor; constructor(cursor: Uint8Array) { - this.cursor = new CursorProxy(cursor); + this.cursor = Cursor.from(cursor); } tokens(): Token[] { @@ -174,7 +139,7 @@ export class Lexer { } // only two types left: number or boolean // we need to return val first since it starts value - this.cursor.unpass(); + this.cursor.unpass(1); if (BooleanStartCharacters.has(val)) { value.push({type: TokenType.Boolean, value: this.boolean()}); } else { diff --git a/packages/psd/src/utils/bytes.ts b/packages/psd/src/utils/bytes.ts index c719feb..074bbbb 100644 --- a/packages/psd/src/utils/bytes.ts +++ b/packages/psd/src/utils/bytes.ts @@ -89,6 +89,12 @@ const INCREASE: {[type in ReadType]: number} = { * being parsed. */ export class Cursor { + static from(bytes: Uint8Array) { + return new Cursor( + new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) + ); + } + constructor(private dataView: DataView, public position = 0) {} /** @@ -145,6 +151,13 @@ export class Cursor { ); } + iter(): Uint8Array { + return new Uint8Array( + this.dataView.buffer, + this.dataView.byteOffset + this.position + ); + } + /** * Creates a `Uint8Array` that covers the underlying `ArrayBuffer` of this * cursor, starting at the current cursor position and spanning a @@ -166,6 +179,16 @@ export class Cursor { return this.dataView.getUint8(this.position); } + /** + * Returns subsequent byte + */ + one(): number { + // dataView throws RangeError if position is outside bounds + const val = this.dataView.getUint8(this.position); + this.position += 1; + return val; + } + /** * Reads a number at the current cursor position, using the given {@link type} * (big endian).