From cf9711d54bac2634440cec0defdab150d14048a3 Mon Sep 17 00:00:00 2001 From: quark Date: Thu, 6 Feb 2025 16:56:20 +0800 Subject: [PATCH 1/2] feat: add base support for path text --- src/engineData2.ts | 66 +++++++++++++++++++++++++++++++++++++++++++--- src/psd.ts | 12 +++++++++ src/psdReader.ts | 37 ++++++++++++++++++++++++++ src/text.ts | 3 ++- 4 files changed, 113 insertions(+), 5 deletions(-) diff --git a/src/engineData2.ts b/src/engineData2.ts index 2779b8b..f8fd2a0 100644 --- a/src/engineData2.ts +++ b/src/engineData2.ts @@ -1,6 +1,8 @@ /// Engine data 2 experiments // /test/engineData2.json:1109 is character codes +import { EngineData } from './text'; + interface KeysDict { [key: string]: { name?: string; @@ -127,7 +129,7 @@ const keysRoot: KeysDict = { }, }, }, - } + }, }, }, }, @@ -217,8 +219,64 @@ const keysRoot: KeysDict = { }, }, '8': { - name: '8', - children: {}, + name: 'TextFrameSet', + children: { + '0': { + uproot: true, + children: { + '0': { + name: 'TextPath', + children: { + '0': { name: 'Name' }, + '1': { + name: 'BezierCurve', + children: { + '0': { name: 'ControlPoints' }, + }, + }, + '2': { + name: 'Data', + children: { + '0': { name: 'Type' }, + '1': { name: 'Orientation' }, + '2': { name: 'FrameMatrix' }, + '4': { name: '4' }, + '6': { name: 'TextRange' }, + '7': { name: 'RowGutter' }, + '8': { name: 'ColumnGutter' }, + '9': { name: '9' }, + '10': { + name: 'BaselineAlignment', + children: { + '0': { name: 'Flag' }, + '1': { name: 'Min' }, + }, + }, + '11': { + name: 'PathData', + children: { + '1': { name: '1' }, + '0': { name: 'Reversed' }, + '2': { name: '2' }, + '3': { name: '3' }, + '4': { name: 'Spacing' }, + '5': { name: '5' }, + '6': { name: '6' }, + '7': { name: '7' }, + '18': { name: '18' }, + }, + }, + '12': { name: '12' }, + '13': { name: '13' }, + }, + }, + '3': { name: '3' }, + '97': { name: 'UUID' }, + }, + }, + }, + }, + }, }, '9': { name: 'Predefined', @@ -336,7 +394,7 @@ const keysRoot: KeysDict = { }, }; -function decodeObj(obj: any, keys: KeysDict): any { +function decodeObj(obj: EngineData, keys: KeysDict): any { if (obj === null) return obj; if (Array.isArray(obj)) return obj.map(x => decodeObj(x, keys)); if (typeof obj !== 'object') return obj; diff --git a/src/psd.ts b/src/psd.ts index af93784..01cfab9 100644 --- a/src/psd.ts +++ b/src/psd.ts @@ -427,6 +427,18 @@ export interface LayerTextData { boxBounds?: number[]; bounds?: UnitsBounds; boundingBox?: UnitsBounds; + + textPath?: { + BezierCurve?: { + /**the length number should be multiples of 8, which is present a bezier curve */ + ControlPoints: number[]; + }, + Data: { + Type: number; + FrameMatrix: number[]; + TextRange: number[]; + } + } } export interface PatternInfo { diff --git a/src/psdReader.ts b/src/psdReader.ts index 9e37dd1..a64c04f 100644 --- a/src/psdReader.ts +++ b/src/psdReader.ts @@ -3,6 +3,10 @@ import { Psd, Layer, ColorMode, SectionDividerType, LayerAdditionalInfo, ReadOpt import { resetImageData, offsetForChannel, decodeBitmap, createImageData, toBlendMode, ChannelID, Compression, LayerMaskFlags, MaskParams, ColorSpace, RAW_IMAGE_DATA, largeAdditionalInfoKeys, imageDataToCanvas } from './helpers'; import { infoHandlersMap } from './additionalInfo'; import { resourceHandlersMap } from './imageResources'; +import { parseEngineData } from './engineData'; +import { toByteArray } from 'base64-js'; +import { decodeEngineData2 } from './engineData2'; +import type { EngineData } from './text'; interface ChannelInfo { id: ChannelID; @@ -340,6 +344,14 @@ export function readPsd(reader: PsdReader, readOptions: ReadOptions = {}) { // but add option to preserve file color mode (need to return image data instead of canvas in that case) // psd.colorMode = ColorMode.RGB; // we convert all color modes to RGB + if(psd.engineData){ + const byteArray = toByteArray(psd.engineData); + const engineData = parseEngineData(byteArray); + const parsedEngineData = decodeEngineData2(engineData); + + assignGlobalEngineData(psd.children, parsedEngineData); + } + return psd; } @@ -690,6 +702,31 @@ export function readAdditionalLayerInfo(reader: PsdReader, target: LayerAddition }, false, u64); } +/** + * There is a Global text engine data outside text element. + * So, we need to pick global engine data and set to per text element. + */ +function assignGlobalEngineData(layers: Layer[] | undefined, globalEngineData: EngineData) { + const resources = globalEngineData?.ResourceDict?.TextFrameSet; + + if (!resources || Object.keys(resources).length === 0 || !layers?.length) { + return; + } + const resourceLength = Object.keys(resources).length; + let textIndex = 0; + layers.forEach((layer) => { + const isText = !!layer.text; + if (isText) { + if (textIndex < resourceLength) { + const resource = resources[textIndex++]; + layer.text!.textPath = resource.TextPath; + } else { + console.warn('Not enough resources for all text layers'); + } + } + }); +} + function createImageDataBitDepth(width: number, height: number, bitDepth: number): PixelData { if (bitDepth === 1 || bitDepth === 8) { return createImageData(width, height); diff --git a/src/text.ts b/src/text.ts index 5da06b7..9dc0170 100644 --- a/src/text.ts +++ b/src/text.ts @@ -97,6 +97,7 @@ interface ResourceDict { SubscriptSize: number; SubscriptPosition: number; SmallCapSize: number; + TextFrameSet?: any[]; } interface ParagraphRun { @@ -120,7 +121,7 @@ interface PhotoshopNode { }; } -interface EngineData { +export interface EngineData { EngineDict: { Editor: { Text: string; }; ParagraphRun: { From 0bd81b1aaf302953545bd7e2a37c7562d53d1f20 Mon Sep 17 00:00:00 2001 From: quark Date: Tue, 25 Feb 2025 21:33:30 +0800 Subject: [PATCH 2/2] feat: support CMYK color mode --- src/psdReader.ts | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/psdReader.ts b/src/psdReader.ts index 1031804..e5fc181 100644 --- a/src/psdReader.ts +++ b/src/psdReader.ts @@ -13,7 +13,7 @@ interface ChannelInfo { length: number; } -export const supportedColorModes = [ColorMode.Bitmap, ColorMode.Grayscale, ColorMode.RGB, ColorMode.Indexed]; +export const supportedColorModes = [ColorMode.Bitmap, ColorMode.Grayscale, ColorMode.RGB, ColorMode.Indexed, ColorMode.CMYK]; const colorModes = ['bitmap', 'grayscale', 'indexed', 'RGB', 'CMYK', 'multichannel', 'duotone', 'lab']; function setupGrayscale(data: PixelData) { @@ -840,10 +840,11 @@ function readImageData(reader: PsdReader, psd: Psd) { } case ColorMode.CMYK: { if (bitsPerChannel !== 8) throw new Error('bitsPerChannel Not supproted'); - if (psd.channels !== 4) throw new Error(`Invalid channel count`); + if (psd.channels !== 4 && psd.channels !== 5) throw new Error(`Invalid channel count`); + const channelLen = psd.channels! + 1; const channels = [0, 1, 2, 3]; - if (reader.globalAlpha) channels.push(4); + if (psd.channels === 5 || reader.globalAlpha) channels.push(4); if (compression === Compression.RawData) { throw new Error(`Not implemented`); @@ -855,12 +856,12 @@ function readImageData(reader: PsdReader, psd: Psd) { const cmykImageData: PixelData = { width: imageData.width, height: imageData.height, - data: new Uint8Array(imageData.width * imageData.height * 5), + data: new Uint8Array(imageData.width * imageData.height * channelLen), }; const start = reader.offset; - readDataRLE(reader, cmykImageData, psd.width, psd.height, bitsPerChannel, 5, channels, reader.large); - cmykToRgb(cmykImageData, imageData, true); + readDataRLE(reader, cmykImageData, psd.width, psd.height, bitsPerChannel, channelLen, channels, reader.large); + cmykToRgb(cmykImageData, imageData, false, channelLen); if (RAW_IMAGE_DATA) (psd as any).imageDataRaw = new Uint8Array(reader.view.buffer, reader.view.byteOffset + start, reader.offset - start); } else { @@ -897,19 +898,19 @@ function readImageData(reader: PsdReader, psd: Psd) { } } -function cmykToRgb(cmyk: PixelData, rgb: PixelData, reverseAlpha: boolean) { +function cmykToRgb(cmyk: PixelData, rgb: PixelData, reverseAlpha: boolean, channelLen: number = 5) { const size = rgb.width * rgb.height * 4; const srcData = cmyk.data; const dstData = rgb.data; - for (let src = 0, dst = 0; dst < size; src += 5, dst += 4) { - const c = srcData[src]; - const m = srcData[src + 1]; - const y = srcData[src + 2]; - const k = srcData[src + 3]; - dstData[dst] = ((((c * k) | 0) / 255) | 0); - dstData[dst + 1] = ((((m * k) | 0) / 255) | 0); - dstData[dst + 2] = ((((y * k) | 0) / 255) | 0); + for (let src = 0, dst = 0; dst < size; src += channelLen, dst += 4) { + const c = 255 - srcData[src]; + const m = 255 - srcData[src + 1]; + const y = 255 - srcData[src + 2]; + const k = 255 - srcData[src + 3]; + dstData[dst + 0] = ((65535 - (c * (255 - k) + (k << 8))) >> 8) | 0; + dstData[dst + 1] = ((65535 - (m * (255 - k) + (k << 8))) >> 8) | 0; + dstData[dst + 2] = ((65535 - (y * (255 - k) + (k << 8))) >> 8) | 0; dstData[dst + 3] = reverseAlpha ? 255 - srcData[src + 4] : srcData[src + 4]; }