diff --git a/packages/psd/src/sections/LayerAndMaskInformation/index.ts b/packages/psd/src/sections/LayerAndMaskInformation/index.ts index 88f1bbf..e9e9e5a 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/index.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/index.ts @@ -57,7 +57,7 @@ export function parseLayerAndMaskInformation( // Skip over Global layer mask info // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17115 - cursor.pass(cursor.read("u32")); + skipGlobalLayerMaskInfo(cursor); const globalAdditionalLayerInformation = readGlobalAdditionalLayerInformation( cursor, @@ -140,3 +140,22 @@ export function parseLayerAndMaskInformation( return {layers, groups, orders, globalAdditionalLayerInformation}; } + +function skipGlobalLayerMaskInfo(cursor: Cursor): void { + // Some PSD writers omit the Global Layer Mask Info block entirely and leave + // only alignment padding after LayerInfo. In those cases, treat the block as + // empty instead of forcing a 4-byte length read past the end of the section. + if (cursor.position + 4 > cursor.length) { + return; + } + + const length = cursor.read("u32"); + const remaining = cursor.length - cursor.position; + + if (length > remaining) { + cursor.unpass(4); + return; + } + + cursor.pass(length); +} diff --git a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts index fdd8a2d..db134c2 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts @@ -216,7 +216,7 @@ export function readGlobalAdditionalLayerInformation( fileVersionSpec: FileVersionSpec ): AdditionalLayerProperties { const additionalLayerInfos = []; - while (cursor.position < cursor.length) { + while (cursor.position + 12 <= cursor.length) { try { additionalLayerInfos.push( readAdditionalLayerInfo(cursor, fileVersionSpec, /* padding */ 4) diff --git a/packages/psd/tests/integration/fixtures/procreate-missing-global-mask-info.psd b/packages/psd/tests/integration/fixtures/procreate-missing-global-mask-info.psd new file mode 100644 index 0000000..d4e4126 Binary files /dev/null and b/packages/psd/tests/integration/fixtures/procreate-missing-global-mask-info.psd differ diff --git a/packages/psd/tests/integration/procreateMissingGlobalMaskInfo.test.ts b/packages/psd/tests/integration/procreateMissingGlobalMaskInfo.test.ts new file mode 100644 index 0000000..ea309eb --- /dev/null +++ b/packages/psd/tests/integration/procreateMissingGlobalMaskInfo.test.ts @@ -0,0 +1,29 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +import * as fs from "fs"; +import * as path from "path"; +import {beforeAll, describe, expect, it} from "vitest"; + +import type Psd from "../../src/index"; +import PSD from "../../src/index"; + +const FIXTURE_DIR = path.join(__dirname, "fixtures"); + +describe("Procreate PSD without global mask info", () => { + let psd: Psd; + + beforeAll(() => { + const data = fs.readFileSync( + path.resolve(FIXTURE_DIR, "procreate-missing-global-mask-info.psd") + ); + psd = PSD.parse(data.buffer); + }); + + it("parses successfully", () => { + expect(psd.width).toBe(128); + expect(psd.height).toBe(128); + expect(psd.layers.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/psd/tests/unit/layerAndMaskInformation.test.ts b/packages/psd/tests/unit/layerAndMaskInformation.test.ts new file mode 100644 index 0000000..f63979a --- /dev/null +++ b/packages/psd/tests/unit/layerAndMaskInformation.test.ts @@ -0,0 +1,37 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +import {describe, expect, it} from "vitest"; + +import {PsdSpec} from "../../src/interfaces/FileVersionSpec"; +import {parseLayerAndMaskInformation} from "../../src/sections/LayerAndMaskInformation"; + +describe("parseLayerAndMaskInformation", () => { + it("accepts PSD sections without a readable global layer mask info block", () => { + const data = new Uint8Array([ + 0x00, + 0x00, + 0x00, + 0x08, // Layer and Mask Information section length + 0x00, + 0x00, + 0x00, + 0x02, // LayerInfo length + 0x00, + 0x00, // layer count = 0 + 0x00, + 0x00, // alignment padding; no room for GlobalLayerMaskInfo length field + ]); + + const section = parseLayerAndMaskInformation( + new DataView(data.buffer), + PsdSpec + ); + + expect(section.layers).toStrictEqual([]); + expect(section.groups).toStrictEqual([]); + expect(section.orders).toStrictEqual([]); + expect(section.globalAdditionalLayerInformation).toStrictEqual({}); + }); +});