Jump To …

psdchannelimage.coffee

PSD files also store merged image data for each individual layer. Unfortunately, parsing this image data is a bit different than parsing the overall merged image data at the end of the file. The main difference is that each image channel has independent compression, and the channel order is defined individually for each layer. Also, the layer size is often not the same size as the full image.

class PSDChannelImage extends PSDImage
  constructor: (file, header, @layer) ->
    @width = @layer.cols
    @height = @layer.rows
    @channelsInfo = @layer.channelsInfo

    super file, header

  skip: ->
    Log.debug "Skipping channel image data. Layer = #{@layer.name}"
    for channel in @channelsInfo
      @file.seek channel.length

  getImageWidth: -> @width
  getImageHeight: -> @height
  getImageChannels: -> @layer.channels

Since we're working on a per-channel basis now, we only read the byte counts for the current channel only.

  getByteCounts: ->
    byteCounts = []
    for i in [0...@getImageHeight()]
      byteCounts.push @file.readShortInt()

    byteCounts

  parse: ->
    Log.debug "\nLayer: #{@layer.name}, image size: #{@length} (#{@getImageWidth()}x#{@getImageHeight()})"
    

We must keep track of the current channel data position global to this object now, since we parse a single channel at a time.

    @chanPos = 0

Loop through each image channel and parse each one like a full image.

    for i in [0...@getImageChannels()]
      @chInfo = @layer.channelsInfo[i]

      if @chInfo.length <= 0
        @parseCompression()
        continue

If the ID of this current channel is -2, then we assume the dimensions of the layer mask.

      if @chInfo.id is -2
        @width = @layer.mask.width
        @height = @layer.mask.height
      else
        @width = @layer.cols
        @height = @layer.rows

      start = @file.tell()

      Log.debug "Channel ##{@chInfo.id}: length=#{@chInfo.length}"
      @parseImageData()

      end = @file.tell()

Sanity check

      if end isnt start + @chInfo.length
        Log.debug "ERROR: read incorrect number of bytes for channel ##{@chInfo.id}. Layer=#{@layer.name}, Expected = #{start + @chInfo.length}, Actual: #{end}"
        @file.seek start + @chInfo.length, false

Futher sanity checks

    if @channelData.length isnt @length
      Log.debug "ERROR: #{@channelData.length} read; expected #{@length}"

    @processImageData()

@parseUserMask()

    if exports?
      memusage = process.memoryUsage()
      used = Math.round memusage.heapUsed / 1024 / 1024
      total = Math.round memusage.heapTotal / 1024 / 1024
      Log.debug "\nMemory usage: #{used}MB / #{total}MB"

Since we're parsing on a per-channel basis, we need to modify the behavior of the RAW encoding parser a bit. This version is aware of the current channel data position, since layers that have RAW encoding often use RLE encoded alpha channels.

  parseRaw: ->
    Log.debug "Attempting to parse RAW encoded channel..."
    data = @file.read(@chInfo.length - 2)
    dataIndex = 0
    for i in [@chanPos...@chanPos+@chInfo.length - 2]
      @channelData[i] = data[dataIndex++]

    @chanPos += @chInfo.length - 2

Compression is stored on a per-channel basis, not a per-image basis for layers

  parseImageData: ->
    @compression = @parseCompression()

    switch @compression
      when 0 then @parseRaw()
      when 1 then @parseRLE()
      when 2, 3 then @parseZip()
      else
        Log.debug "Unknown image compression. Attempting to skip."
        return @file.seek @endPos, false

Parse a single channel instead of every image channel

  parseChannelData: ->
    lineIndex = 0
    
    Log.debug "Parsing layer channel ##{@chInfo.id}, Start = #{@file.tell()}"
    [@chanPos, lineIndex] = @decodeRLEChannel(@chanPos, lineIndex)

parseUserMask: -> if @getImageDepth() is 8 @parseUserMask8()