import { LayerEffectsInfo, BevelStyle, LayerEffectShadow } from './psd'; import { toBlendMode, fromBlendMode } from './helpers'; import { PsdReader, checkSignature, readSignature, skipBytes, readUint16, readUint8, readUint32, readFixedPoint32, readColor } from './psdReader'; import { PsdWriter, writeSignature, writeUint16, writeZeros, writeFixedPoint32, writeUint8, writeUint32, writeColor } from './psdWriter'; const bevelStyles: BevelStyle[] = [ undefined as any, 'outer bevel', 'inner bevel', 'emboss', 'pillow emboss', 'stroke emboss' ]; function readBlendMode(reader: PsdReader) { checkSignature(reader, '8BIM'); return toBlendMode[readSignature(reader)] || 'normal'; } function writeBlendMode(writer: PsdWriter, mode: string | undefined) { writeSignature(writer, '8BIM'); writeSignature(writer, fromBlendMode[mode!] || 'norm'); } function readFixedPoint8(reader: PsdReader) { return readUint8(reader) / 0xff; } function writeFixedPoint8(writer: PsdWriter, value: number) { writeUint8(writer, Math.round(value * 0xff) | 0); } export function readEffects(reader: PsdReader) { const version = readUint16(reader); if (version !== 0) throw new Error(`Invalid effects layer version: ${version}`); const effectsCount = readUint16(reader); const effects: LayerEffectsInfo = {}; for (let i = 0; i < effectsCount; i++) { checkSignature(reader, '8BIM'); const type = readSignature(reader); switch (type) { case 'cmnS': { // common state (see See Effects layer, common state info) const size = readUint32(reader); const version = readUint32(reader); const visible = !!readUint8(reader); skipBytes(reader, 2); if (size !== 7 || version !== 0 || !visible) throw new Error(`Invalid effects common state`); break; } case 'dsdw': // drop shadow (see See Effects layer, drop shadow and inner shadow info) case 'isdw': { // inner shadow (see See Effects layer, drop shadow and inner shadow info) const blockSize = readUint32(reader); const version = readUint32(reader); if (blockSize !== 41 && blockSize !== 51) throw new Error(`Invalid shadow size: ${blockSize}`); if (version !== 0 && version !== 2) throw new Error(`Invalid shadow version: ${version}`); const size = readFixedPoint32(reader); readFixedPoint32(reader); // intensity const angle = readFixedPoint32(reader); const distance = readFixedPoint32(reader); const color = readColor(reader); const blendMode = readBlendMode(reader); const enabled = !!readUint8(reader); const useGlobalLight = !!readUint8(reader); const opacity = readFixedPoint8(reader); if (blockSize >= 51) readColor(reader); // native color const shadowInfo: LayerEffectShadow = { size: { units: 'Pixels', value: size }, distance: { units: 'Pixels', value: distance }, angle, color, blendMode, enabled, useGlobalLight, opacity }; if (type === 'dsdw') { effects.dropShadow = [shadowInfo]; } else { effects.innerShadow = [shadowInfo]; } break; } case 'oglw': { // outer glow (see See Effects layer, outer glow info) const blockSize = readUint32(reader); const version = readUint32(reader); if (blockSize !== 32 && blockSize !== 42) throw new Error(`Invalid outer glow size: ${blockSize}`); if (version !== 0 && version !== 2) throw new Error(`Invalid outer glow version: ${version}`); const size = readFixedPoint32(reader); readFixedPoint32(reader); // intensity const color = readColor(reader); const blendMode = readBlendMode(reader); const enabled = !!readUint8(reader); const opacity = readFixedPoint8(reader); if (blockSize >= 42) readColor(reader); // native color effects.outerGlow = { size: { units: 'Pixels', value: size }, color, blendMode, enabled, opacity }; break; } case 'iglw': { // inner glow (see See Effects layer, inner glow info) const blockSize = readUint32(reader); const version = readUint32(reader); if (blockSize !== 32 && blockSize !== 43) throw new Error(`Invalid inner glow size: ${blockSize}`); if (version !== 0 && version !== 2) throw new Error(`Invalid inner glow version: ${version}`); const size = readFixedPoint32(reader); readFixedPoint32(reader); // intensity const color = readColor(reader); const blendMode = readBlendMode(reader); const enabled = !!readUint8(reader); const opacity = readFixedPoint8(reader); if (blockSize >= 43) { readUint8(reader); // inverted readColor(reader); // native color } effects.innerGlow = { size: { units: 'Pixels', value: size }, color, blendMode, enabled, opacity }; break; } case 'bevl': { // bevel (see See Effects layer, bevel info) const blockSize = readUint32(reader); const version = readUint32(reader); if (blockSize !== 58 && blockSize !== 78) throw new Error(`Invalid bevel size: ${blockSize}`); if (version !== 0 && version !== 2) throw new Error(`Invalid bevel version: ${version}`); const angle = readFixedPoint32(reader); const strength = readFixedPoint32(reader); const size = readFixedPoint32(reader); const highlightBlendMode = readBlendMode(reader); const shadowBlendMode = readBlendMode(reader); const highlightColor = readColor(reader); const shadowColor = readColor(reader); const style = bevelStyles[readUint8(reader)] || 'inner bevel'; const highlightOpacity = readFixedPoint8(reader); const shadowOpacity = readFixedPoint8(reader); const enabled = !!readUint8(reader); const useGlobalLight = !!readUint8(reader); const direction = readUint8(reader) ? 'down' : 'up'; if (blockSize >= 78) { readColor(reader); // real highlight color readColor(reader); // real shadow color } effects.bevel = { size: { units: 'Pixels', value: size }, angle, strength, highlightBlendMode, shadowBlendMode, highlightColor, shadowColor, style, highlightOpacity, shadowOpacity, enabled, useGlobalLight, direction, }; break; } case 'sofi': { // solid fill (Photoshop 7.0) (see See Effects layer, solid fill (added in Photoshop 7.0)) const size = readUint32(reader); const version = readUint32(reader); if (size !== 34) throw new Error(`Invalid effects solid fill info size: ${size}`); if (version !== 2) throw new Error(`Invalid effects solid fill info version: ${version}`); const blendMode = readBlendMode(reader); const color = readColor(reader); const opacity = readFixedPoint8(reader); const enabled = !!readUint8(reader); readColor(reader); // native color effects.solidFill = [{ blendMode, color, opacity, enabled }]; break; } default: throw new Error(`Invalid effect type: '${type}'`); } } return effects; } function writeShadowInfo(writer: PsdWriter, shadow: LayerEffectShadow) { writeUint32(writer, 51); writeUint32(writer, 2); writeFixedPoint32(writer, shadow.size && shadow.size.value || 0); writeFixedPoint32(writer, 0); // intensity writeFixedPoint32(writer, shadow.angle || 0); writeFixedPoint32(writer, shadow.distance && shadow.distance.value || 0); writeColor(writer, shadow.color); writeBlendMode(writer, shadow.blendMode); writeUint8(writer, shadow.enabled ? 1 : 0); writeUint8(writer, shadow.useGlobalLight ? 1 : 0); writeFixedPoint8(writer, shadow.opacity ?? 1); writeColor(writer, shadow.color); // native color } export function writeEffects(writer: PsdWriter, effects: LayerEffectsInfo) { const dropShadow = effects.dropShadow?.[0]; const innerShadow = effects.innerShadow?.[0]; const outerGlow = effects.outerGlow; const innerGlow = effects.innerGlow; const bevel = effects.bevel; const solidFill = effects.solidFill?.[0]; let count = 1; if (dropShadow) count++; if (innerShadow) count++; if (outerGlow) count++; if (innerGlow) count++; if (bevel) count++; if (solidFill) count++; writeUint16(writer, 0); writeUint16(writer, count); writeSignature(writer, '8BIM'); writeSignature(writer, 'cmnS'); writeUint32(writer, 7); // size writeUint32(writer, 0); // version writeUint8(writer, 1); // visible writeZeros(writer, 2); if (dropShadow) { writeSignature(writer, '8BIM'); writeSignature(writer, 'dsdw'); writeShadowInfo(writer, dropShadow); } if (innerShadow) { writeSignature(writer, '8BIM'); writeSignature(writer, 'isdw'); writeShadowInfo(writer, innerShadow); } if (outerGlow) { writeSignature(writer, '8BIM'); writeSignature(writer, 'oglw'); writeUint32(writer, 42); writeUint32(writer, 2); writeFixedPoint32(writer, outerGlow.size?.value || 0); writeFixedPoint32(writer, 0); // intensity writeColor(writer, outerGlow.color); writeBlendMode(writer, outerGlow.blendMode); writeUint8(writer, outerGlow.enabled ? 1 : 0); writeFixedPoint8(writer, outerGlow.opacity || 0); writeColor(writer, outerGlow.color); } if (innerGlow) { writeSignature(writer, '8BIM'); writeSignature(writer, 'iglw'); writeUint32(writer, 43); writeUint32(writer, 2); writeFixedPoint32(writer, innerGlow.size?.value || 0); writeFixedPoint32(writer, 0); // intensity writeColor(writer, innerGlow.color); writeBlendMode(writer, innerGlow.blendMode); writeUint8(writer, innerGlow.enabled ? 1 : 0); writeFixedPoint8(writer, innerGlow.opacity || 0); writeUint8(writer, 0); // inverted writeColor(writer, innerGlow.color); } if (bevel) { writeSignature(writer, '8BIM'); writeSignature(writer, 'bevl'); writeUint32(writer, 78); writeUint32(writer, 2); writeFixedPoint32(writer, bevel.angle || 0); writeFixedPoint32(writer, bevel.strength || 0); writeFixedPoint32(writer, bevel.size?.value || 0); writeBlendMode(writer, bevel.highlightBlendMode); writeBlendMode(writer, bevel.shadowBlendMode); writeColor(writer, bevel.highlightColor); writeColor(writer, bevel.shadowColor); const style = bevelStyles.indexOf(bevel.style!); writeUint8(writer, style <= 0 ? 1 : style); writeFixedPoint8(writer, bevel.highlightOpacity || 0); writeFixedPoint8(writer, bevel.shadowOpacity || 0); writeUint8(writer, bevel.enabled ? 1 : 0); writeUint8(writer, bevel.useGlobalLight ? 1 : 0); writeUint8(writer, bevel.direction === 'down' ? 1 : 0); writeColor(writer, bevel.highlightColor); writeColor(writer, bevel.shadowColor); } if (solidFill) { writeSignature(writer, '8BIM'); writeSignature(writer, 'sofi'); writeUint32(writer, 34); writeUint32(writer, 2); writeBlendMode(writer, solidFill.blendMode); writeColor(writer, solidFill.color); writeFixedPoint8(writer, solidFill.opacity || 0); writeUint8(writer, solidFill.enabled ? 1 : 0); writeColor(writer, solidFill.color); } }