From 18b6aa242a4732caf129cdd1b9e4c34b12914496 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Wed, 12 Oct 2022 15:50:16 +0200 Subject: [PATCH 1/8] [feature] implement reading mask data --- packages/psd/src/classes/Layer.ts | 24 +++- .../LayerAndMaskInformation/classes.ts | 6 + .../LayerAndMaskInformation/interfaces.ts | 51 ++++++++ .../readLayerRecordsAndChannels.ts | 123 +++++++++++++++++- packages/psd/src/utils/boundingBox.ts | 23 ++++ packages/psd/src/utils/index.ts | 1 + .../psd/tests/integration/fixtures/mask.psd | Bin 0 -> 694395 bytes .../psd/tests/integration/userMask.test.ts | 60 +++++++++ 8 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 packages/psd/src/utils/boundingBox.ts create mode 100644 packages/psd/tests/integration/fixtures/mask.psd create mode 100644 packages/psd/tests/integration/userMask.test.ts diff --git a/packages/psd/src/classes/Layer.ts b/packages/psd/src/classes/Layer.ts index 2c49a04..8c22174 100644 --- a/packages/psd/src/classes/Layer.ts +++ b/packages/psd/src/classes/Layer.ts @@ -3,7 +3,9 @@ // MIT License import {EngineData, ImageData} from "../interfaces"; -import {LayerFrame} from "../sections"; +import {decodeGrayscale} from "../methods"; +import {LayerFrame, MaskData} from "../sections"; +import {area} from "../utils"; import {NodeParent} from "./Node"; import {NodeBase} from "./NodeBase"; import {Synthesizable} from "./Synthesizable"; @@ -48,6 +50,26 @@ export class Layer get composedOpacity(): number { return this.parent.composedOpacity * (this.opacity / 255); } + get maskData(): MaskData { + return this.layerFrame.layerProperties.maskData; + } + + async userMask(): Promise { + const userMask = this.layerFrame.userMask; + if (!userMask) { + return undefined; + } + return decodeGrayscale(area(this.maskData), userMask); + } + + async realUserMask(): Promise { + const maskData = this.maskData.realData; + const userMask = this.layerFrame.realUserMask; + if (!maskData || !userMask) { + return undefined; + } + return decodeGrayscale(area(maskData), userMask); + } get isHidden(): boolean { return this.layerFrame.layerProperties.hidden; diff --git a/packages/psd/src/sections/LayerAndMaskInformation/classes.ts b/packages/psd/src/sections/LayerAndMaskInformation/classes.ts index 076e734..7f3bb9c 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/classes.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/classes.ts @@ -48,6 +48,12 @@ export class LayerFrame { get alpha(): ChannelBytes | undefined { return this.channels.get(ChannelKind.TransparencyMask); } + get userMask(): ChannelBytes | undefined { + return this.channels.get(ChannelKind.UserSuppliedLayerMask); + } + get realUserMask(): ChannelBytes | undefined { + return this.channels.get(ChannelKind.RealUserSuppliedLayerMask); + } get width(): number { const {right, left} = this.layerProperties; diff --git a/packages/psd/src/sections/LayerAndMaskInformation/interfaces.ts b/packages/psd/src/sections/LayerAndMaskInformation/interfaces.ts index 3d9ae5f..b18cc4f 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/interfaces.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/interfaces.ts @@ -33,6 +33,7 @@ export interface LayerRecord { layerText?: string; /** If defined, containts extra text properties */ engineData?: EngineData; + maskData: MaskData; } export type LayerChannels = Map; @@ -60,6 +61,7 @@ export interface LayerProperties { text?: string; /** Text properties */ textProperties?: EngineData; + maskData: MaskData; } export const createLayerProperties = ( @@ -79,6 +81,7 @@ export const createLayerProperties = ( blendMode, layerText, engineData, + maskData, } = layerRecord; return { @@ -95,5 +98,53 @@ export const createLayerProperties = ( groupId, text: layerText, textProperties: engineData, + maskData, }; }; + +export interface MaskFlags { + // bit 0 = position relative to layer + positionRelativeToLayer: boolean; + // bit 1 = layer mask disabled + layerMaskDisabled: boolean; + // bit 2 = invert layer mask when blending (Obsolete) + invertMaskWhenBlending: boolean; + // bit 3 = indicates that the user mask actually came from rendering other data + userMaskFromRenderingOtherData: boolean; + // bit 4 = indicates that the user and/or vector masks have parameters applied to them + masksHaveParametersApplied: boolean; +} + +export interface MaskParameters { + // bit 0 = user mask density, 1 byte + userMaskDensity?: number; + // bit 1 = user mask feather, 8 byte, double + userMaskFeather?: number; + // bit 2 = vector mask density, 1 byte + vectorMaskDensity?: number; + // bit 3 = vector mask feather, 8 bytes, double + vectorMaskFeather?: number; +} + +// The spec is confusing at best... what "real" means? +// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1031423 +export interface RealMaskData { + flags: MaskFlags; + backgroundColor: number; + top: number; + left: number; + bottom: number; + right: number; +} + +export interface MaskData { + top: number; + left: number; + bottom: number; + right: number; + backgroundColor: number; + flags: MaskFlags; + parameters?: MaskParameters; + // only present if size != 20 + realData?: RealMaskData; +} diff --git a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts index 91f3cc4..35cdf0a 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts @@ -20,7 +20,14 @@ import { import {parseEngineData} from "../../methods"; import {Cursor, InvalidBlendingModeSignature} from "../../utils"; import {readAdditionalLayerInfo} from "./AdditionalLayerInfo"; -import {LayerChannels, LayerRecord} from "./interfaces"; +import { + LayerChannels, + LayerRecord, + MaskData, + MaskFlags, + MaskParameters, + RealMaskData, +} from "./interfaces"; const EXPECTED_BLENDING_MODE_SIGNATURE = "8BIM"; @@ -113,7 +120,7 @@ function readLayerRecord( // Skip the Layer Mask info segment, which we don't need for now // Read the length of the segment and skip it - cursor.pass(cursor.read("u32")); + const maskData = readMaskData(cursor); // Skip the Blending Range segment, which we don't need for now // Read the length of the segment and skip it @@ -185,6 +192,7 @@ function readLayerRecord( dividerType, layerText, engineData, + maskData, }; } @@ -276,3 +284,114 @@ function readLayerChannels( return channels; } + +function readMaskData(cursor: Cursor): MaskData { + const length = cursor.read("u32"); + const startsAt = cursor.position; + const [top, left, bottom, right] = readBounds(cursor); + const backgroundColor = cursor.read("u8"); + const flags = readFlags(cursor); + const realData = length >= 20 ? readRealData(cursor) : undefined; + const parameters = flags.masksHaveParametersApplied + ? readParameters(cursor) + : undefined; + + const remainingBytes = length - (cursor.position - startsAt); + cursor.pass(remainingBytes); + + return { + top, + left, + bottom, + right, + backgroundColor, + flags, + parameters, + realData, + }; +} + +function readBounds(cursor: Cursor): [number, number, number, number] { + return Array.from(Array(4), () => cursor.read("i32")) as [ + number, + number, + number, + number + ]; +} + +enum MaskFlagsBitmask { + PositionRelativeToLayer = 1 << 0, + LayerMaskDisabled = 1 << 1, + InvertMaskWhenBlending = 1 << 2, + UserMaskFromRenderingOtherData = 1 << 3, + MasksHaveParametersApplied = 1 << 4, +} + +function readFlags(cursor: Cursor): MaskFlags { + const flags = cursor.read("u8"); + return { + // bit 0 = position relative to layer + positionRelativeToLayer: Boolean( + flags & MaskFlagsBitmask.PositionRelativeToLayer + ), + // bit 1 = layer mask disabled + layerMaskDisabled: Boolean(flags & MaskFlagsBitmask.LayerMaskDisabled), + // bit 2 = invert layer mask when blending (Obsolete) + invertMaskWhenBlending: Boolean( + flags & MaskFlagsBitmask.InvertMaskWhenBlending + ), + // bit 3 = indicates that the user mask actually came from rendering other data + userMaskFromRenderingOtherData: Boolean( + flags & MaskFlagsBitmask.UserMaskFromRenderingOtherData + ), + // bit 4 = indicates that the user and/or vector masks have parameters applied to them + masksHaveParametersApplied: Boolean( + flags & MaskFlagsBitmask.MasksHaveParametersApplied + ), + }; +} + +enum MaskParameterBitmask { + // bit 0 = user mask density + UserMaskDensity = 1 << 0, + // bit 1 = user mask feather + UserMaskFeather = 1 << 1, + // bit 2 = vector mask density + VectorMaskDensity = 1 << 2, + // bit 3 = vector mask feather + VectorMaskFeather = 1 << 3, +} + +function readParameters(cursor: Cursor): MaskParameters { + const parameters = cursor.read("u8"); + return { + // bit 0 = user mask density, 1 byte + userMaskDensity: + parameters & MaskParameterBitmask.UserMaskDensity + ? cursor.read("u8") + : undefined, + // bit 1 = user mask feather, 8 byte, double + userMaskFeather: + parameters & MaskParameterBitmask.UserMaskFeather + ? cursor.read("f64") + : undefined, + // bit 2 = vector mask density, 1 byte + vectorMaskDensity: + parameters & MaskParameterBitmask.VectorMaskDensity + ? cursor.read("u8") + : undefined, + // bit 3 = vector mask feather, 8 bytes, double + vectorMaskFeather: + parameters & MaskParameterBitmask.VectorMaskFeather + ? cursor.read("f64") + : undefined, + }; +} + +function readRealData(cursor: Cursor): RealMaskData { + const flags = readFlags(cursor); + const backgroundColor = cursor.read("u8"); + const [top, left, bottom, right] = readBounds(cursor); + return {top, left, bottom, right, flags, backgroundColor}; +} diff --git a/packages/psd/src/utils/boundingBox.ts b/packages/psd/src/utils/boundingBox.ts new file mode 100644 index 0000000..e2f94c4 --- /dev/null +++ b/packages/psd/src/utils/boundingBox.ts @@ -0,0 +1,23 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +interface BoundingBox { + top: number; + left: number; + bottom: number; + right: number; +} +export function dimensions(boundingBox: BoundingBox): { + height: number; + width: number; +} { + const height = boundingBox.bottom - boundingBox.top; + const width = boundingBox.right - boundingBox.left; + return {width, height}; +} + +export function area(boundingBox: BoundingBox): number { + const {width, height} = dimensions(boundingBox); + return width * height; +} diff --git a/packages/psd/src/utils/index.ts b/packages/psd/src/utils/index.ts index 5bb7136..523cd74 100644 --- a/packages/psd/src/utils/index.ts +++ b/packages/psd/src/utils/index.ts @@ -6,3 +6,4 @@ export * from "./array"; export * from "./bytes"; export * from "./error"; export * from "./number"; +export * from "./boundingBox"; diff --git a/packages/psd/tests/integration/fixtures/mask.psd b/packages/psd/tests/integration/fixtures/mask.psd new file mode 100644 index 0000000000000000000000000000000000000000..35a357f80010f825c64526de3e5cba97fca35e49 GIT binary patch literal 694395 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
bm^%fA@z6~66dv5dg?Jy;pjqSNRwr_9l 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

JPL7RvE9LN>J#(xN@c2{uqX(puo}-yyA> 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 literal 0 HcmV?d00001 diff --git a/packages/psd/tests/integration/userMask.test.ts b/packages/psd/tests/integration/userMask.test.ts new file mode 100644 index 0000000..2d048fe --- /dev/null +++ b/packages/psd/tests/integration/userMask.test.ts @@ -0,0 +1,60 @@ +// @webtoon/psd +// Copyright 2021-present NAVER WEBTOON +// MIT License + +import * as fs from "fs"; +import * as path from "path"; +import {describe, expect, it} from "vitest"; + +import PSD from "../../src/index"; + +const FIXTURE_DIR = path.join(__dirname, "fixtures"); + +describe(`@webtoon/psd reads user masks`, () => { + it(`should parse the mask properties successfully`, () => { + const data = fs.readFileSync(path.resolve(FIXTURE_DIR, "mask.psd")).buffer; + const psd = PSD.parse(data); + const maskedLayer = psd.layers[0]; + expect(maskedLayer.maskData).toStrictEqual({ + backgroundColor: 0, + top: 39, + bottom: 59, + left: 579, + right: 600, + flags: { + invertMaskWhenBlending: false, + layerMaskDisabled: false, + masksHaveParametersApplied: false, + positionRelativeToLayer: false, + userMaskFromRenderingOtherData: true, + }, + parameters: undefined, + realData: { + backgroundColor: 0, + bottom: 65535, + left: 65535, + right: 65535, + top: 40, + flags: { + invertMaskWhenBlending: false, + layerMaskDisabled: false, + masksHaveParametersApplied: false, + positionRelativeToLayer: false, + userMaskFromRenderingOtherData: false, + }, + }, + }); + expect(maskedLayer.userMask).toHaveLength(0); + expect(maskedLayer.realUserMask).toHaveLength(0); + }); + + it(`should extract mask pixels`, async () => { + const data = fs.readFileSync(path.resolve(FIXTURE_DIR, "mask.psd")).buffer; + const psd = PSD.parse(data); + const maskedLayer = psd.layers[0]; + // NOTE: maybe we should introduce decoding into some grayscale format instead? + // 21 (width) * 20 (height) * 4 (since we decompress into RGBA format) + expect(await maskedLayer.userMask()).toHaveLength(1680); + expect(await maskedLayer.realUserMask()).toBeUndefined(); + }); +}); From 251cc5169e2ba34471e0e61d4ce85ff58c1be5c7 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Thu, 13 Oct 2022 18:55:19 +0200 Subject: [PATCH 2/8] bugfix: decoding mask channels turns out in certain cases given length is much more than real length of a channel - and we need to read scanline sizes in order to calulcate that. Also, in case of masks, number of scanlines is proportional to mask height, not layer height. --- .../readLayerRecordsAndChannels.ts | 55 +++++---- packages/psd/src/utils/boundingBox.ts | 16 ++- .../psd/tests/integration/userMask.test.ts | 109 ++++++++++++------ 3 files changed, 115 insertions(+), 65 deletions(-) diff --git a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts index 35cdf0a..115994c 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts @@ -15,10 +15,9 @@ import { matchBlendMode, matchChannelCompression, matchClipping, - RawDataDescriptorValue, } from "../../interfaces"; import {parseEngineData} from "../../methods"; -import {Cursor, InvalidBlendingModeSignature} from "../../utils"; +import {Cursor, height, InvalidBlendingModeSignature} from "../../utils"; import {readAdditionalLayerInfo} from "./AdditionalLayerInfo"; import { LayerChannels, @@ -46,12 +45,11 @@ export function readLayerRecordsAndChannels( // Read layer channels const result = layerRecords .map((layerRecord): [LayerRecord, LayerChannels] => { - const layerHeight = calcLayerHeight(layerRecord); // The channels for each layer are stored in the same order as the layers const channels = readLayerChannels( cursor, layerRecord.channelInformation, - layerHeight, + layerRecord, fileVersionSpec ); @@ -231,14 +229,32 @@ function readLayerFlags(cursor: Cursor): { }; } -function calcLayerHeight(layerRecord: LayerRecord): number { - return layerRecord.bottom - layerRecord.top + 1; +function realMask(layerRecord: LayerRecord): MaskData { + const maskData = layerRecord.maskData.realData; + if (!maskData) { + throw new Error("missing real mask data"); + } + return maskData; +} + +function calcLayerHeight( + layerRecord: LayerRecord, + channelId: ChannelKind +): number { + switch (channelId) { + case ChannelKind.UserSuppliedLayerMask: + return height(layerRecord.maskData); + case ChannelKind.RealUserSuppliedLayerMask: + return height(realMask(layerRecord)); + default: + return layerRecord.bottom - layerRecord.top + 1; + } } function readLayerChannels( cursor: Cursor, channelInformation: [ChannelKind, number][], - scanLines: number, + layerRecord: LayerRecord, fileVersionSpec: FileVersionSpec ): LayerChannels { const channels = new Map(); @@ -246,36 +262,27 @@ function readLayerChannels( const {length} = channelInformation; for (let i = 0; i < length; i++) { const [channelKind, channelDataLength] = channelInformation[i]; + const scanLines = calcLayerHeight(layerRecord, channelKind); // Each channel has its own compression method; a layer may contain multiple // channels with different compression methods. // This is different from the PSD Image Data section, which uses a single // compression method for all channels. const compression = matchChannelCompression(cursor.read("u16")); - const channelData = cursor.take(channelDataLength); switch (compression) { case ChannelCompression.RawData: { - channels.set(channelKind, {compression, data: channelData}); + const data = cursor.take(channelDataLength); + channels.set(channelKind, {compression, data}); break; } case ChannelCompression.RleCompressed: { - // We're skipping over the bytes that describe the length of each scanline since - // we don't currently use them. We might re-think this in the future when we implement - // serialization of a Psd back into bytes.. But not a concern at the moment. - // Compressed bytes per scanline are encoded at the beginning as 2 bytes per scanline - - const bytesPerScanline = fileVersionSpec.rleScanlineLengthFieldSize; - // Do not attempt to skip more than the length of the channel data. - // This is needed because some layers (e.g. gradient fill layers) may - // have empty channel data (channelDataLength === 0). - const skip = Math.min(channelDataLength, scanLines * bytesPerScanline); - const data = new Uint8Array( - channelData.buffer, - channelData.byteOffset + skip, - channelData.byteLength - skip + const sizes = Array.from(Array(scanLines), () => + cursor.read(fileVersionSpec.rleScanlineLengthFieldReadType) ); + const size = sizes.reduce((a, b) => a + b); + const data = cursor.take(size); channels.set(channelKind, {compression, data}); break; } @@ -291,7 +298,7 @@ function readMaskData(cursor: Cursor): MaskData { const [top, left, bottom, right] = readBounds(cursor); const backgroundColor = cursor.read("u8"); const flags = readFlags(cursor); - const realData = length >= 20 ? readRealData(cursor) : undefined; + const realData = length >= 36 ? readRealData(cursor) : undefined; const parameters = flags.masksHaveParametersApplied ? readParameters(cursor) : undefined; diff --git a/packages/psd/src/utils/boundingBox.ts b/packages/psd/src/utils/boundingBox.ts index e2f94c4..fa605dd 100644 --- a/packages/psd/src/utils/boundingBox.ts +++ b/packages/psd/src/utils/boundingBox.ts @@ -8,16 +8,22 @@ interface BoundingBox { bottom: number; right: number; } + +export function height(boundingBox: BoundingBox): number { + return boundingBox.bottom - boundingBox.top; +} + +export function width(boundingBox: BoundingBox): number { + return boundingBox.right - boundingBox.left; +} + export function dimensions(boundingBox: BoundingBox): { height: number; width: number; } { - const height = boundingBox.bottom - boundingBox.top; - const width = boundingBox.right - boundingBox.left; - return {width, height}; + return {width: width(boundingBox), height: height(boundingBox)}; } export function area(boundingBox: BoundingBox): number { - const {width, height} = dimensions(boundingBox); - return width * height; + return width(boundingBox) * height(boundingBox); } diff --git a/packages/psd/tests/integration/userMask.test.ts b/packages/psd/tests/integration/userMask.test.ts index 2d048fe..1d16c2d 100644 --- a/packages/psd/tests/integration/userMask.test.ts +++ b/packages/psd/tests/integration/userMask.test.ts @@ -4,57 +4,94 @@ import * as fs from "fs"; import * as path from "path"; -import {describe, expect, it} from "vitest"; +import {beforeAll, describe, expect, it} from "vitest"; -import PSD from "../../src/index"; +import type Psd from "../../src/index"; +import PSD, {Layer} from "../../src/index"; const FIXTURE_DIR = path.join(__dirname, "fixtures"); describe(`@webtoon/psd reads user masks`, () => { - it(`should parse the mask properties successfully`, () => { + let psd: Psd; + beforeAll(() => { const data = fs.readFileSync(path.resolve(FIXTURE_DIR, "mask.psd")).buffer; - const psd = PSD.parse(data); - const maskedLayer = psd.layers[0]; - expect(maskedLayer.maskData).toStrictEqual({ - backgroundColor: 0, - top: 39, - bottom: 59, - left: 579, - right: 600, - flags: { - invertMaskWhenBlending: false, - layerMaskDisabled: false, - masksHaveParametersApplied: false, - positionRelativeToLayer: false, - userMaskFromRenderingOtherData: true, - }, - parameters: undefined, - realData: { + psd = PSD.parse(data); + }); + + describe(`layer without user mask`, () => { + let maskedLayer: Layer; + beforeAll(() => { + maskedLayer = psd.layers[0]; + }); + + it(`should not parse the mask real flags if they are missing`, () => { + expect(maskedLayer.maskData).toStrictEqual({ backgroundColor: 0, - bottom: 65535, - left: 65535, - right: 65535, - top: 40, + top: 39, + bottom: 59, + left: 579, + right: 600, flags: { invertMaskWhenBlending: false, layerMaskDisabled: false, masksHaveParametersApplied: false, positionRelativeToLayer: false, - userMaskFromRenderingOtherData: false, + userMaskFromRenderingOtherData: true, }, - }, + parameters: undefined, + realData: undefined, + }); + }); + + it(`should extract mask pixels`, async () => { + // NOTE: maybe we should introduce decoding into some grayscale format instead? + // 21 (width) * 20 (height) * 4 (since we decompress into RGBA format) + expect(await maskedLayer.userMask()).toHaveLength(1680); + expect(await maskedLayer.realUserMask()).toBeUndefined(); }); - expect(maskedLayer.userMask).toHaveLength(0); - expect(maskedLayer.realUserMask).toHaveLength(0); }); - it(`should extract mask pixels`, async () => { - const data = fs.readFileSync(path.resolve(FIXTURE_DIR, "mask.psd")).buffer; - const psd = PSD.parse(data); - const maskedLayer = psd.layers[0]; - // NOTE: maybe we should introduce decoding into some grayscale format instead? - // 21 (width) * 20 (height) * 4 (since we decompress into RGBA format) - expect(await maskedLayer.userMask()).toHaveLength(1680); - expect(await maskedLayer.realUserMask()).toBeUndefined(); + describe(`layer with real user mask`, () => { + let maskedLayer: Layer; + beforeAll(() => { + maskedLayer = psd.layers[3]; + }); + + it(`should parse the mask properties successfully`, () => { + expect(maskedLayer.maskData).toStrictEqual({ + backgroundColor: 0, + top: -31, + bottom: 102, + left: -107, + right: 704, + flags: { + invertMaskWhenBlending: false, + layerMaskDisabled: false, + masksHaveParametersApplied: false, + positionRelativeToLayer: false, + userMaskFromRenderingOtherData: true, + }, + parameters: undefined, + realData: { + backgroundColor: 255, + bottom: 5600, + left: 0, + right: 640, + top: 0, + flags: { + invertMaskWhenBlending: false, + layerMaskDisabled: true, + masksHaveParametersApplied: false, + positionRelativeToLayer: false, + userMaskFromRenderingOtherData: false, + }, + }, + }); + }); + + it(`should extract real mask pixels`, async () => { + const mask = await maskedLayer.realUserMask(); + expect(mask).toHaveLength(14_336_000); + }); }); }); From 19d2a88c173d7246882ed7d953d880db42b2d494 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Fri, 14 Oct 2022 10:41:33 +0200 Subject: [PATCH 3/8] bugfix: group frame may be undefined --- packages/psd/src/classes/Group.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/psd/src/classes/Group.ts b/packages/psd/src/classes/Group.ts index 8e6eaa7..4259e15 100644 --- a/packages/psd/src/classes/Group.ts +++ b/packages/psd/src/classes/Group.ts @@ -16,15 +16,15 @@ export class Group implements NodeBase { /** @internal */ constructor( - private layerFrame: GroupFrame, + private layerFrame: GroupFrame | undefined, public readonly parent: NodeParent ) {} get name(): string { - return this.layerFrame.layerProperties.name; + return this.layerFrame?.layerProperties.name ?? ""; } get opacity(): number { - return this.layerFrame.layerProperties.opacity; + return this.layerFrame?.layerProperties.opacity ?? 0; } get composedOpacity(): number { return this.parent.composedOpacity * (this.opacity / 255); From 0fb0f3f688ce4fc9943c047471d763660a7e33cb Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Fri, 14 Oct 2022 10:53:08 +0200 Subject: [PATCH 4/8] bring back previous bugfix (9cd2d8f1) Originally from here: https://github.com/webtoon/psd/commit/9cd2d8f1caeeed778a9c902892f524d2d96b3f1b NOTE: I cannot reproduce it on my sample files, and there's no test file. So I can only hope to preserve compatibility :) --- .../readLayerRecordsAndChannels.ts | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts index 115994c..0d268fc 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts @@ -17,7 +17,12 @@ import { matchClipping, } from "../../interfaces"; import {parseEngineData} from "../../methods"; -import {Cursor, height, InvalidBlendingModeSignature} from "../../utils"; +import { + Cursor, + height, + InvalidBlendingModeSignature, + ReadType, +} from "../../utils"; import {readAdditionalLayerInfo} from "./AdditionalLayerInfo"; import { LayerChannels, @@ -262,14 +267,12 @@ function readLayerChannels( const {length} = channelInformation; for (let i = 0; i < length; i++) { const [channelKind, channelDataLength] = channelInformation[i]; - const scanLines = calcLayerHeight(layerRecord, channelKind); // Each channel has its own compression method; a layer may contain multiple // channels with different compression methods. // This is different from the PSD Image Data section, which uses a single // compression method for all channels. const compression = matchChannelCompression(cursor.read("u16")); - switch (compression) { case ChannelCompression.RawData: { const data = cursor.take(channelDataLength); @@ -277,12 +280,18 @@ function readLayerChannels( break; } case ChannelCompression.RleCompressed: { - const sizes = Array.from(Array(scanLines), () => - cursor.read(fileVersionSpec.rleScanlineLengthFieldReadType) + const data = cursor.take( + // Do not attempt to take more than the length of the channel data. + // This is needed because some layers (e.g. gradient fill layers) may + // have empty channel data (channelDataLength === 0). + channelDataLength > 0 + ? rleCompressedSize( + cursor, + calcLayerHeight(layerRecord, channelKind), + fileVersionSpec.rleScanlineLengthFieldReadType + ) + : channelDataLength ); - const size = sizes.reduce((a, b) => a + b); - - const data = cursor.take(size); channels.set(channelKind, {compression, data}); break; } @@ -292,6 +301,15 @@ function readLayerChannels( return channels; } +function rleCompressedSize( + cursor: Cursor, + scanLines: number, + readType: ReadType +): number { + const sizes = Array.from(Array(scanLines), () => cursor.read(readType)); + return sizes.reduce((a, b) => a + b); +} + function readMaskData(cursor: Cursor): MaskData { const length = cursor.read("u32"); const startsAt = cursor.position; From 66d38a52dd147886b91684197ed301f66dda8927 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Tue, 18 Oct 2022 12:03:04 +0200 Subject: [PATCH 5/8] Remove obsolete comment --- .../LayerAndMaskInformation/readLayerRecordsAndChannels.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts index 0d268fc..5b2d1ba 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts @@ -121,8 +121,6 @@ function readLayerRecord( const layerExtraDataSize = cursor.read("u32"); const layerExtraDataBegin = cursor.position; - // Skip the Layer Mask info segment, which we don't need for now - // Read the length of the segment and skip it const maskData = readMaskData(cursor); // Skip the Blending Range segment, which we don't need for now From 5fae0e386f89672c6a72c7a3a3ef23602e5804e5 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Tue, 18 Oct 2022 12:09:01 +0200 Subject: [PATCH 6/8] Describe what masks should look like, assert hash This is easier than having full binary dump on disk and just as useful ;) --- .../psd/tests/integration/userMask.test.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/psd/tests/integration/userMask.test.ts b/packages/psd/tests/integration/userMask.test.ts index 1d16c2d..08e8ee1 100644 --- a/packages/psd/tests/integration/userMask.test.ts +++ b/packages/psd/tests/integration/userMask.test.ts @@ -4,6 +4,7 @@ import * as fs from "fs"; import * as path from "path"; +import * as crypto from "crypto"; import {beforeAll, describe, expect, it} from "vitest"; import type Psd from "../../src/index"; @@ -44,10 +45,19 @@ describe(`@webtoon/psd reads user masks`, () => { }); it(`should extract mask pixels`, async () => { + const mask = await maskedLayer.userMask(); // NOTE: maybe we should introduce decoding into some grayscale format instead? // 21 (width) * 20 (height) * 4 (since we decompress into RGBA format) - expect(await maskedLayer.userMask()).toHaveLength(1680); + expect(mask).toHaveLength(1680); expect(await maskedLayer.realUserMask()).toBeUndefined(); + + const hash = crypto.createHash("sha256").update(mask).digest("hex"); + // NOTE: when changing the hash, please make sure the result is coherent :) + // Either using https://www.npmjs.com/package/canvas or browser build (see README) + // This is a fat arrow pointing right + expect(hash).toEqual( + "b2a77f8a194fcacbad5e5918b34086b1dc518c6d1294a4acdc411c3394ea22cc" + ); }); }); @@ -92,6 +102,14 @@ describe(`@webtoon/psd reads user masks`, () => { it(`should extract real mask pixels`, async () => { const mask = await maskedLayer.realUserMask(); expect(mask).toHaveLength(14_336_000); + + const hash = crypto.createHash("sha256").update(mask).digest("hex"); + // NOTE: when changing the hash, please make sure the result is coherent :) + // Either using https://www.npmjs.com/package/canvas or browser build (see README) + // This is a vertical black bar with diffusion on top + expect(hash).toEqual( + "2901bf3b6e114c440ecc7d371a592e028bc8538f6c3cb8b823a7a7c74b7c80cd" + ); }); }); }); From abbc9067ce73d23fa1056a470f1b60c5b5119804 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Tue, 18 Oct 2022 12:12:15 +0200 Subject: [PATCH 7/8] Use `height` helper also for layerRecord --- .../LayerAndMaskInformation/readLayerRecordsAndChannels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts index 5b2d1ba..d98e226 100644 --- a/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts +++ b/packages/psd/src/sections/LayerAndMaskInformation/readLayerRecordsAndChannels.ts @@ -250,7 +250,7 @@ function calcLayerHeight( case ChannelKind.RealUserSuppliedLayerMask: return height(realMask(layerRecord)); default: - return layerRecord.bottom - layerRecord.top + 1; + return height(layerRecord) + 1; } } From f0487e5259cf134d208525a8bc4878e0d7a0b133 Mon Sep 17 00:00:00 2001 From: Lucas Czaplinski Date: Tue, 18 Oct 2022 12:14:04 +0200 Subject: [PATCH 8/8] Fix test name --- packages/psd/tests/integration/userMask.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/psd/tests/integration/userMask.test.ts b/packages/psd/tests/integration/userMask.test.ts index 08e8ee1..d128336 100644 --- a/packages/psd/tests/integration/userMask.test.ts +++ b/packages/psd/tests/integration/userMask.test.ts @@ -19,7 +19,7 @@ describe(`@webtoon/psd reads user masks`, () => { psd = PSD.parse(data); }); - describe(`layer without user mask`, () => { + describe(`layer without real user mask`, () => { let maskedLayer: Layer; beforeAll(() => { maskedLayer = psd.layers[0];