From 06ada1f9e1bb3f6b9a885bd74b61a07906110e67 Mon Sep 17 00:00:00 2001 From: Yasuhiro Yamada Date: Sun, 5 Apr 2026 10:29:24 +0900 Subject: [PATCH 1/4] Handle PSDs without global layer mask info --- .../sections/LayerAndMaskInformation/index.ts | 23 ++++++++++++++++--- .../readLayerRecordsAndChannels.ts | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/psd/src/sections/LayerAndMaskInformation/index.ts b/packages/psd/src/sections/LayerAndMaskInformation/index.ts index 88f1bbf..1299b3f 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/index.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/index.ts @@ -55,9 +55,7 @@ export function parseLayerAndMaskInformation( cursor.padding(cursor.position, 4); - // 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 +138,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. Treat that as an empty block + // 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) From 0f35552984e0ab5a1cfd337118adeeb2298a1cd6 Mon Sep 17 00:00:00 2001 From: Yasuhiro Yamada Date: Sun, 5 Apr 2026 10:35:28 +0900 Subject: [PATCH 2/4] Add regression test for missing global mask info --- .../unit/layerAndMaskInformation.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 packages/psd/tests/unit/layerAndMaskInformation.test.ts 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({}); + }); +}); From a3a6bf1c972dfde01958e600abd6f8eaa5274c45 Mon Sep 17 00:00:00 2001 From: Yasuhiro Yamada Date: Sun, 5 Apr 2026 10:43:28 +0900 Subject: [PATCH 3/4] Add Procreate fixture for missing global mask info --- .../procreate-missing-global-mask-info.psd | Bin 0 -> 11420 bytes .../procreateMissingGlobalMaskInfo.test.ts | 29 ++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 packages/psd/tests/integration/fixtures/procreate-missing-global-mask-info.psd create mode 100644 packages/psd/tests/integration/procreateMissingGlobalMaskInfo.test.ts 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 0000000000000000000000000000000000000000..d4e4126e20bcdd3048c19450ee86df6f072ad890 GIT binary patch literal 11420 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 Y;eqyj27=bN^D{vE{{4g2xAQviZ^r^RD*ylh literal 0 HcmV?d00001 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); + }); +}); From 57f9cab1da0cc78d70b057d714dbb158b9e89dc2 Mon Sep 17 00:00:00 2001 From: Yasuhiro Yamada Date: Sun, 5 Apr 2026 10:45:24 +0900 Subject: [PATCH 4/4] Document missing global mask info workaround --- packages/psd/src/sections/LayerAndMaskInformation/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/psd/src/sections/LayerAndMaskInformation/index.ts b/packages/psd/src/sections/LayerAndMaskInformation/index.ts index 1299b3f..e9e9e5a 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/index.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/index.ts @@ -55,6 +55,8 @@ export function parseLayerAndMaskInformation( cursor.padding(cursor.position, 4); + // Skip over Global layer mask info + // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_17115 skipGlobalLayerMaskInfo(cursor); const globalAdditionalLayerInformation = readGlobalAdditionalLayerInformation( @@ -141,8 +143,8 @@ export function parseLayerAndMaskInformation( function skipGlobalLayerMaskInfo(cursor: Cursor): void { // Some PSD writers omit the Global Layer Mask Info block entirely and leave - // only alignment padding after LayerInfo. Treat that as an empty block - // instead of forcing a 4-byte length read past the end of the section. + // 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; }