import { Psd , Layer , LayerAdditionalInfo , ColorMode , SectionDividerType , WriteOptions , Color , GlobalLayerMaskInfo } from './psd' ; import { hasAlpha , createCanvas , writeDataRLE , PixelData , LayerChannelData , ChannelData , offsetForChannel , createImageData , fromBlendMode , ChannelID , Compression , clamp , LayerMaskFlags , MaskParams , ColorSpace , Bounds , largeAdditionalInfoKeys , RAW_IMAGE_DATA } from './helpers' ; import { ExtendedWriteOptions , hasMultiEffects , infoHandlers } from './additionalInfo' ; import { resourceHandlers } from './imageResources' ; export interface PsdWriter { offset : number ; buffer : ArrayBuffer ; view : DataView ; } export function createWriter ( size = 4096 ) { const buffer = new ArrayBuffer ( size ) ; const view = new DataView ( buffer ) ; const offset = 0 ; return { buffer , view , offset } ; } export function getWriterBuffer ( writer ) { return writer . buffer . slice ( 0 , writer . offset ) ; } export function getWriterBufferNoCopy ( writer ) { return new Uint8Array ( writer . buffer , 0 , writer . offset ) ; } export function writeUint8 ( writer , value ) { const offset = addSize ( writer , 1 ) ; writer . view . setUint8 ( offset , value ) ; } export function writeInt16 ( writer , value ) { const offset = addSize ( writer , 2 ) ; writer . view . setInt16 ( offset , value , false ) ; } export function writeUint16 ( writer , value ) { const offset = addSize ( writer , 2 ) ; writer . view . setUint16 ( offset , value , false ) ; } export function writeInt32 ( writer , value ) { const offset = addSize ( writer , 4 ) ; writer . view . setInt32 ( offset , value , false ) ; } export function writeUint32 ( writer , value ) { const offset = addSize ( writer , 4 ) ; writer . view . setUint32 ( offset , value , false ) ; } export function writeFloat32 ( writer , value ) { const offset = addSize ( writer , 4 ) ; writer . view . setFloat32 ( offset , value , false ) ; } export function writeFloat64 ( writer , value ) { const offset = addSize ( writer , 8 ) ; writer . view . setFloat64 ( offset , value , false ) ; } export function writeFixedPoint32 ( writer , value ) { writeInt32 ( writer , value * ( 1 << 16 ) ) ; } export function writeFixedPointPath32 ( writer , value ) { writeInt32 ( writer , value * ( 1 << 24 ) ) ; } export function writeBytes ( writer , buffer ) { if ( buffer ) { ensureSize ( writer , writer . offset + buffer . length ) ; const bytes = new Uint8Array ( writer . buffer ) ; bytes . set ( buffer , writer . offset ) ; writer . offset += buffer . length ; } } export function writeZeros ( writer , count ) { for ( let i = 0 ; i < count ; i ++ ) { writeUint8 ( writer , 0 ) ; } } export function writeSignature ( writer , signature ) { if ( signature . length !== 4 ) throw new Error ( `Invalid signature: '${ signature }'` ) ; for ( let i = 0 ; i < 4 ; i ++ ) { writeUint8 ( writer , signature . charCodeAt ( i ) ) ; } } export function writePascalString ( writer , text , padTo ) { let length = text . length ; writeUint8 ( writer , length ) ; for ( let i = 0 ; i < length ; i ++ ) { const code = text . charCodeAt ( i ) ; writeUint8 ( writer , code < 128 ? code : '?' . charCodeAt ( 0 ) ) ; } while ( ++ length % padTo ) { writeUint8 ( writer , 0 ) ; } } export function writeUnicodeString ( writer , text ) { writeUint32 ( writer , text . length ) ; for ( let i = 0 ; i < text . length ; i ++ ) { writeUint16 ( writer , text . charCodeAt ( i ) ) ; } } export function writeUnicodeStringWithPadding ( writer , text ) { writeUint32 ( writer , text . length + 1 ) ; for ( let i = 0 ; i < text . length ; i ++ ) { writeUint16 ( writer , text . charCodeAt ( i ) ) ; } writeUint16 ( writer , 0 ) ; } function getLargestLayerSize ( layers = [ ] ) { let max = 0 ; for ( const layer of layers ) { if ( layer . canvas || layer . imageData ) { const { width , height } = getLayerDimentions ( layer ) ; max = Math . max ( max , 2 * height + 2 * width * height ) ; } if ( layer . children ) { max = Math . max ( max , getLargestLayerSize ( layer . children ) ) ; } } return max ; } export function writeSection ( writer , round , func , writeTotalLength = false , large = false ) { if ( large ) writeUint32 ( writer , 0 ) ; const offset = writer . offset ; writeUint32 ( writer , 0 ) ; func ( ) ; let length = writer . offset - offset - 4 ; let len = length ; while ( ( len % round ) !== 0 ) { writeUint8 ( writer , 0 ) ; len ++ ; } if ( writeTotalLength ) { length = len ; } writer . view . setUint32 ( offset , length , false ) ; } export function writePsd ( writer , psd , options = { } ) { if ( ! ( + psd . width > 0 && + psd . height > 0 ) ) throw new Error ( 'Invalid document size' ) ; if ( ( psd . width > 30000 || psd . height > 30000 ) && ! options . psb ) throw new Error ( 'Document size is too large (max is 30000x30000, use PSB format instead)' ) ; let imageResources = psd . imageResources || { } ; const opt = { ... options , layerIds : [ ] } ; if ( opt . generateThumbnail ) { imageResources = { ... imageResources , thumbnail : createThumbnail ( psd ) } ; } let imageData = psd . imageData ; if ( ! imageData && psd . canvas ) { imageData = psd . canvas . getContext ( '2d' ) ! . getImageData ( 0 , 0 , psd . canvas . width , psd . canvas . height ) ; } if ( imageData && ( psd . width !== imageData . width || psd . height !== imageData . height ) ) throw new Error ( 'Document canvas must have the same size as document' ) ; const globalAlpha = ! ! imageData && hasAlpha ( imageData ) ; const maxBufferSize = Math . max ( getLargestLayerSize ( psd . children ) , 4 * 2 * psd . width * psd . height + 2 * psd . height ) ; const tempBuffer = new Uint8Array ( maxBufferSize ) ; writeSignature ( writer , '8BPS' ) ; writeUint16 ( writer , options . psb ? 2 : 1 ) ; writeZeros ( writer , 6 ) ; writeUint16 ( writer , globalAlpha ? 4 : 3 ) ; writeUint32 ( writer , psd . height ) ; writeUint32 ( writer , psd . width ) ; writeUint16 ( writer , 8 ) ; writeUint16 ( writer , ColorMode . RGB ) ; writeSection ( writer , 1 , ( ) => { } ) ; writeSection ( writer , 1 , ( ) => { for ( const handler of resourceHandlers ) { if ( handler . has ( imageResources ) ) { writeSignature ( writer , '8BIM' ) ; writeUint16 ( writer , handler . key ) ; writePascalString ( writer , '' , 2 ) ; writeSection ( writer , 2 , ( ) => handler . write ( writer , imageResources ) ) ; } } } ) ; writeSection ( writer , 2 , ( ) => { writeLayerInfo ( tempBuffer , writer , psd , globalAlpha , opt ) ; writeGlobalLayerMaskInfo ( writer , psd . globalLayerMaskInfo ) ; writeAdditionalLayerInfo ( writer , psd , psd , opt ) ; } , undefined , ! ! opt . psb ) ; const channels = globalAlpha ? [ 0 , 1 , 2 , 3 ] : [ 0 , 1 , 2 ] ; const data = imageData || { data : new Uint8Array ( 4 * psd . width * psd . height ) , width : psd . width , height : psd . height , } ; writeUint16 ( writer , Compression . RleCompressed ) ; if ( RAW_IMAGE_DATA && ( psd as any ) . imageDataRaw ) { console . log ( 'writing raw image data' ) ; writeBytes ( writer , ( psd as any ) . imageDataRaw ) ; } else { writeBytes ( writer , writeDataRLE ( tempBuffer , data , psd . width , psd . height , channels , ! ! options . psb ) ) ; } } function writeLayerInfo ( tempBuffer , writer , psd , globalAlpha , options ) { writeSection ( writer , 4 , ( ) => { const layers = [ ] ; addChildren ( layers , psd . children ) ; if ( ! layers . length ) layers . push ( { } ) ; writeInt16 ( writer , globalAlpha ? - layers . length : layers . length ) ; const layersData = layers . map ( ( l , i ) => getChannels ( tempBuffer , l , i === 0 , options ) ) ; for ( const layerData of layersData ) { const { layer , top , left , bottom , right , channels } = layerData ; writeInt32 ( writer , top ) ; writeInt32 ( writer , left ) ; writeInt32 ( writer , bottom ) ; writeInt32 ( writer , right ) ; writeUint16 ( writer , channels . length ) ; for ( const c of channels ) { writeInt16 ( writer , c . channelId ) ; if ( options . psb ) writeUint32 ( writer , 0 ) ; writeUint32 ( writer , c . length ) ; } writeSignature ( writer , '8BIM' ) ; writeSignature ( writer , fromBlendMode [ layer . blendMode ! ] || 'norm' ) ; writeUint8 ( writer , Math . round ( clamp ( layer . opacity ?? 1 , 0 , 1 ) * 255 ) ) ; writeUint8 ( writer , layer . clipping ? 1 : 0 ) ; let flags = 0x08 ; if ( layer . transparencyProtected ) flags |= 0x01 ; if ( layer . hidden ) flags |= 0x02 ; if ( layer . vectorMask || ( layer . sectionDivider && layer . sectionDivider . type !== SectionDividerType . Other ) ) { flags |= 0x10 ; } if ( layer . effects && hasMultiEffects ( layer . effects ) ) { flags |= 0x20 ; } writeUint8 ( writer , flags ) ; writeUint8 ( writer , 0 ) ; writeSection ( writer , 1 , ( ) => { writeLayerMaskData ( writer , layer , layerData ) ; writeLayerBlendingRanges ( writer , psd ) ; writePascalString ( writer , layer . name || '' , 4 ) ; writeAdditionalLayerInfo ( writer , layer , psd , options ) ; } ) ; } for ( const layerData of layersData ) { for ( const channel of layerData . channels ) { writeUint16 ( writer , channel . compression ) ; if ( channel . buffer ) { writeBytes ( writer , channel . buffer ) ; } } } } , true , options . psb ) ; } function writeLayerMaskData ( writer , { mask } , layerData ) { writeSection ( writer , 1 , ( ) => { if ( ! mask ) return ; const m = layerData . mask || { } as Partial < Bounds > ; writeInt32 ( writer , m . top ! ) ; writeInt32 ( writer , m . left ! ) ; writeInt32 ( writer , m . bottom ! ) ; writeInt32 ( writer , m . right ! ) ; writeUint8 ( writer , mask . defaultColor ! ) ; let params = 0 ; if ( mask . userMaskDensity !== undefined ) params |= MaskParams . UserMaskDensity ; if ( mask . userMaskFeather !== undefined ) params |= MaskParams . UserMaskFeather ; if ( mask . vectorMaskDensity !== undefined ) params |= MaskParams . VectorMaskDensity ; if ( mask . vectorMaskFeather !== undefined ) params |= MaskParams . VectorMaskFeather ; let flags = 0 ; if ( mask . disabled ) flags |= LayerMaskFlags . LayerMaskDisabled ; if ( mask . positionRelativeToLayer ) flags |= LayerMaskFlags . PositionRelativeToLayer ; if ( mask . fromVectorData ) flags |= LayerMaskFlags . LayerMaskFromRenderingOtherData ; if ( params ) flags |= LayerMaskFlags . MaskHasParametersAppliedToIt ; writeUint8 ( writer , flags ) ; if ( params ) { writeUint8 ( writer , params ) ; if ( mask . userMaskDensity !== undefined ) writeUint8 ( writer , Math . round ( mask . userMaskDensity * 0xff ) ) ; if ( mask . userMaskFeather !== undefined ) writeFloat64 ( writer , mask . userMaskFeather ) ; if ( mask . vectorMaskDensity !== undefined ) writeUint8 ( writer , Math . round ( mask . vectorMaskDensity * 0xff ) ) ; if ( mask . vectorMaskFeather !== undefined ) writeFloat64 ( writer , mask . vectorMaskFeather ) ; } writeZeros ( writer , 2 ) ; } ) ; } function writeLayerBlendingRanges ( writer , psd ) { writeSection ( writer , 1 , ( ) => { writeUint32 ( writer , 65535 ) ; writeUint32 ( writer , 65535 ) ; let channels = psd . channels || 0 ; for ( let i = 0 ; i < channels ; i ++ ) { writeUint32 ( writer , 65535 ) ; writeUint32 ( writer , 65535 ) ; } } ) ; } function writeGlobalLayerMaskInfo ( writer , info ) { writeSection ( writer , 1 , ( ) => { if ( info ) { writeUint16 ( writer , info . overlayColorSpace ) ; writeUint16 ( writer , info . colorSpace1 ) ; writeUint16 ( writer , info . colorSpace2 ) ; writeUint16 ( writer , info . colorSpace3 ) ; writeUint16 ( writer , info . colorSpace4 ) ; writeUint16 ( writer , info . opacity * 0xff ) ; writeUint8 ( writer , info . kind ) ; writeZeros ( writer , 3 ) ; } } ) ; } function writeAdditionalLayerInfo ( writer , target , psd , options ) { for ( const handler of infoHandlers ) { let key = handler . key ; if ( key === 'Txt2' && options . invalidateTextLayers ) continue ; if ( key === 'vmsk' && options . psb ) key = 'vsms' ; if ( handler . has ( target ) ) { const large = options . psb && largeAdditionalInfoKeys . indexOf ( key ) !== - 1 ; writeSignature ( writer , large ? '8B64' : '8BIM' ) ; writeSignature ( writer , key ) ; const fourBytes = key === 'Txt2' || key === 'luni' || key === 'vmsk' || key === 'artb' || key === 'artd' || key === 'vogk' || key === 'SoLd' || key === 'lnk2' || key === 'vscg' || key === 'vsms' || key === 'GdFl' || key === 'lmfx' || key === 'lrFX' || key === 'cinf' || key === 'PlLd' || key === 'Anno' ; writeSection ( writer , fourBytes ? 4 : 2 , ( ) => { handler . write ( writer , target , psd , options ) ; } , key !== 'Txt2' && key !== 'cinf' && key !== 'extn' , large ) ; } } } function addChildren ( layers , children ) { if ( ! children ) return ; for ( const c of children ) { if ( c . children && c . canvas ) throw new Error ( `Invalid layer, cannot have both 'canvas' and 'children' properties` ) ; if ( c . children && c . imageData ) throw new Error ( `Invalid layer, cannot have both 'imageData' and 'children' properties` ) ; if ( c . children ) { layers . push ( { name : '</Layer group>' , sectionDivider : { type : SectionDividerType . BoundingSectionDivider , } , } ) ; addChildren ( layers , c . children ) ; layers . push ( { sectionDivider : { type : c . opened === false ? SectionDividerType . ClosedFolder : SectionDividerType . OpenFolder , key : fromBlendMode [ c . blendMode ! ] || 'pass' , subType : 0 , } , ... c , } ) ; } else { layers . push ( { ... c } ) ; } } } function resizeBuffer ( writer , size ) { let newLength = writer . buffer . byteLength ; do { newLength *= 2 ; } while ( size > newLength ) ; const newBuffer = new ArrayBuffer ( newLength ) ; const newBytes = new Uint8Array ( newBuffer ) ; const oldBytes = new Uint8Array ( writer . buffer ) ; newBytes . set ( oldBytes ) ; writer . buffer = newBuffer ; writer . view = new DataView ( writer . buffer ) ; } function ensureSize ( writer , size ) { if ( size > writer . buffer . byteLength ) { resizeBuffer ( writer , size ) ; } } function addSize ( writer , size ) { const offset = writer . offset ; ensureSize ( writer , writer . offset += size ) ; return offset ; } function createThumbnail ( psd ) { const canvas = createCanvas ( 10 , 10 ) ; let scale = 1 ; if ( psd . width > psd . height ) { canvas . width = 160 ; canvas . height = Math . floor ( psd . height * ( canvas . width / psd . width ) ) ; scale = canvas . width / psd . width ; } else { canvas . height = 160 ; canvas . width = Math . floor ( psd . width * ( canvas . height / psd . height ) ) ; scale = canvas . height / psd . height ; } const context = canvas . getContext ( '2d' ) ! ; context . scale ( scale , scale ) ; if ( psd . imageData ) { const temp = createCanvas ( psd . imageData . width , psd . imageData . height ) ; temp . getContext ( '2d' ) ! . putImageData ( psd . imageData , 0 , 0 ) ; context . drawImage ( temp , 0 , 0 ) ; } else if ( psd . canvas ) { context . drawImage ( psd . canvas , 0 , 0 ) ; } return canvas ; } function getChannels ( tempBuffer , layer , background , options ) { const layerData = getLayerChannels ( tempBuffer , layer , background , options ) ; const mask = layer . mask ; if ( mask ) { let { top = 0 , left = 0 , right = 0 , bottom = 0 } = mask ; let { width , height } = getLayerDimentions ( mask ) ; let imageData = mask . imageData ; if ( ! imageData && mask . canvas && width && height ) { imageData = mask . canvas . getContext ( '2d' ) ! . getImageData ( 0 , 0 , width , height ) ; } if ( width && height && imageData ) { right = left + width ; bottom = top + height ; let buffer = writeDataRLE ( tempBuffer , imageData , width , height , [ 0 ] , ! ! options . psb ) ! ; if ( RAW_IMAGE_DATA && ( layer as any ) . maskDataRaw ) { buffer = ( layer as any ) . maskDataRaw ; } layerData . mask = { top , left , right , bottom } ; layerData . channels . push ( { channelId : ChannelID . UserMask , compression : Compression . RleCompressed , buffer : buffer , length : 2 + buffer . length , } ) ; } else { layerData . mask = { top : 0 , left : 0 , right : 0 , bottom : 0 } ; layerData . channels . push ( { channelId : ChannelID . UserMask , compression : Compression . RawData , buffer : new Uint8Array ( 0 ) , length : 0 , } ) ; } } return layerData ; } function getLayerDimentions ( { canvas , imageData } ) { return imageData || canvas || { width : 0 , height : 0 } ; } function cropImageData ( data , left , top , width , height ) { const croppedData = createImageData ( width , height ) ; const srcData = data . data ; const dstData = croppedData . data ; for ( let y = 0 ; y < height ; y ++ ) { for ( let x = 0 ; x < width ; x ++ ) { let src = ( ( x + left ) + ( y + top ) * width ) * 4 ; let dst = ( x + y * width ) * 4 ; dstData [ dst ] = srcData [ src ] ; dstData [ dst + 1 ] = srcData [ src + 1 ] ; dstData [ dst + 2 ] = srcData [ src + 2 ] ; dstData [ dst + 3 ] = srcData [ src + 3 ] ; } } return croppedData ; } function getLayerChannels ( tempBuffer , layer , background , options ) { let { top = 0 , left = 0 , right = 0 , bottom = 0 } = layer ; let channels = [ { channelId : ChannelID . Transparency , compression : Compression . RawData , buffer : undefined , length : 2 } , { channelId : ChannelID . Color0 , compression : Compression . RawData , buffer : undefined , length : 2 } , { channelId : ChannelID . Color1 , compression : Compression . RawData , buffer : undefined , length : 2 } , { channelId : ChannelID . Color2 , compression : Compression . RawData , buffer : undefined , length : 2 } , ] ; let { width , height } = getLayerDimentions ( layer ) ; if ( ! ( layer . canvas || layer . imageData ) || ! width || ! height ) { right = left ; bottom = top ; return { layer , top , left , right , bottom , channels } ; } right = left + width ; bottom = top + height ; let data = layer . imageData || layer . canvas ! . getContext ( '2d' ) ! . getImageData ( 0 , 0 , width , height ) ; if ( options . trimImageData ) { const trimmed = trimData ( data ) ; if ( trimmed . left !== 0 || trimmed . top !== 0 || trimmed . right !== data . width || trimmed . bottom !== data . height ) { left += trimmed . left ; top += trimmed . top ; right -= ( data . width - trimmed . right ) ; bottom -= ( data . height - trimmed . bottom ) ; width = right - left ; height = bottom - top ; if ( ! width || ! height ) { return { layer , top , left , right , bottom , channels } ; } if ( layer . imageData ) { data = cropImageData ( data , trimmed . left , trimmed . top , width , height ) ; } else { data = layer . canvas ! . getContext ( '2d' ) ! . getImageData ( trimmed . left , trimmed . top , width , height ) ; } } } const channelIds = [ ChannelID . Color0 , ChannelID . Color1 , ChannelID . Color2 , ] ; if ( ! background || options . noBackground || layer . mask || hasAlpha ( data ) || ( RAW_IMAGE_DATA && ( layer as any ) . imageDataRaw ?. [ '-1' ] ) ) { channelIds . unshift ( ChannelID . Transparency ) ; } channels = channelIds . map ( channel => { const offset = offsetForChannel ( channel , false ) ; let buffer = writeDataRLE ( tempBuffer , data , width , height , [ offset ] , ! ! options . psb ) ! ; if ( RAW_IMAGE_DATA && ( layer as any ) . imageDataRaw ) { buffer = ( layer as any ) . imageDataRaw [ channel ] ; } return { channelId : channel , compression : Compression . RleCompressed , buffer : buffer , length : 2 + buffer . length , } ; } ) ; return { layer , top , left , right , bottom , channels } ; } function isRowEmpty ( { data , width } , y , left , right ) { const start = ( ( y * width + left ) * 4 + 3 ) | 0 ; const end = ( start + ( right - left ) * 4 ) | 0 ; for ( let i = start ; i < end ; i = ( i + 4 ) | 0 ) { if ( data [ i ] !== 0 ) { return false ; } } return true ; } function isColEmpty ( { data , width } , x , top , bottom ) { const stride = ( width * 4 ) | 0 ; const start = ( top * stride + x * 4 + 3 ) | 0 ; for ( let y = top , i = start ; y < bottom ; y ++ , i = ( i + stride ) | 0 ) { if ( data [ i ] !== 0 ) { return false ; } } return true ; } function trimData ( data ) { let top = 0 ; let left = 0 ; let right = data . width ; let bottom = data . height ; while ( top < bottom && isRowEmpty ( data , top , left , right ) ) top ++ ; while ( bottom > top && isRowEmpty ( data , bottom - 1 , left , right ) ) bottom -- ; while ( left < right && isColEmpty ( data , left , top , bottom ) ) left ++ ; while ( right > left && isColEmpty ( data , right - 1 , top , bottom ) ) right -- ; return { top , left , right , bottom } ; } export function writeColor ( writer , color ) { if ( ! color ) { writeUint16 ( writer , ColorSpace . RGB ) ; writeZeros ( writer , 8 ) ; } else if ( 'r' in color ) { writeUint16 ( writer , ColorSpace . RGB ) ; writeUint16 ( writer , Math . round ( color . r * 257 ) ) ; writeUint16 ( writer , Math . round ( color . g * 257 ) ) ; writeUint16 ( writer , Math . round ( color . b * 257 ) ) ; writeUint16 ( writer , 0 ) ; } else if ( 'l' in color ) { writeUint16 ( writer , ColorSpace . Lab ) ; writeInt16 ( writer , Math . round ( color . l * 10000 ) ) ; writeInt16 ( writer , Math . round ( color . a < 0 ? ( color . a * 12800 ) : ( color . a * 12700 ) ) ) ; writeInt16 ( writer , Math . round ( color . b < 0 ? ( color . b * 12800 ) : ( color . b * 12700 ) ) ) ; writeUint16 ( writer , 0 ) ; } else if ( 'h' in color ) { writeUint16 ( writer , ColorSpace . HSB ) ; writeUint16 ( writer , Math . round ( color . h * 0xffff ) ) ; writeUint16 ( writer , Math . round ( color . s * 0xffff ) ) ; writeUint16 ( writer , Math . round ( color . b * 0xffff ) ) ; writeUint16 ( writer , 0 ) ; } else if ( 'c' in color ) { writeUint16 ( writer , ColorSpace . CMYK ) ; writeUint16 ( writer , Math . round ( color . c * 257 ) ) ; writeUint16 ( writer , Math . round ( color . m * 257 ) ) ; writeUint16 ( writer , Math . round ( color . y * 257 ) ) ; writeUint16 ( writer , Math . round ( color . k * 257 ) ) ; } else { writeUint16 ( writer , ColorSpace . Grayscale ) ; writeUint16 ( writer , Math . round ( color . k * 10000 / 255 ) ) ; writeZeros ( writer , 6 ) ; } }