Skip to content

Commit

Permalink
feature: Implement reading mask data (#56)
Browse files Browse the repository at this point in the history
* [feature] implement reading mask data

* 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.

* bugfix: group frame may be undefined

* bring back previous bugfix (9cd2d8f)

Originally from here:
9cd2d8f

NOTE: I cannot reproduce it on my sample files, and there's no test file.
So I can only hope to preserve compatibility :)

* Remove obsolete comment

* Describe what masks should look like, assert hash

This is easier than having full binary dump on disk and just as useful ;)

* Use `height` helper also for layerRecord

* Fix test name
  • Loading branch information
scoiatael committed Oct 19, 2022
1 parent 14cf059 commit 0f99674
Show file tree
Hide file tree
Showing 9 changed files with 399 additions and 33 deletions.
6 changes: 3 additions & 3 deletions packages/psd/src/classes/Group.ts
Expand Up @@ -16,15 +16,15 @@ export class Group implements NodeBase<NodeParent, NodeChild> {

/** @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);
Expand Down
24 changes: 23 additions & 1 deletion packages/psd/src/classes/Layer.ts
Expand Up @@ -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";
Expand Down Expand Up @@ -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<Uint8Array | undefined> {
const userMask = this.layerFrame.userMask;
if (!userMask) {
return undefined;
}
return decodeGrayscale(area(this.maskData), userMask);
}

async realUserMask(): Promise<Uint8Array | undefined> {
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;
Expand Down
6 changes: 6 additions & 0 deletions packages/psd/src/sections/LayerAndMaskInformation/classes.ts
Expand Up @@ -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;
Expand Down
51 changes: 51 additions & 0 deletions packages/psd/src/sections/LayerAndMaskInformation/interfaces.ts
Expand Up @@ -33,6 +33,7 @@ export interface LayerRecord {
layerText?: string;
/** If defined, containts extra text properties */
engineData?: EngineData;
maskData: MaskData;
}

export type LayerChannels = Map<ChannelKind, ChannelBytes>;
Expand Down Expand Up @@ -60,6 +61,7 @@ export interface LayerProperties {
text?: string;
/** Text properties */
textProperties?: EngineData;
maskData: MaskData;
}

export const createLayerProperties = (
Expand All @@ -79,6 +81,7 @@ export const createLayerProperties = (
blendMode,
layerText,
engineData,
maskData,
} = layerRecord;

return {
Expand All @@ -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;
}

0 comments on commit 0f99674

Please sign in to comment.