aboutsummaryrefslogtreecommitdiffstats
path: root/src/psd.coffee
blob: 2ed7f58e95a30856341444186cf647fd490afa63 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# NodeJS or browser?
if exports?
  Root = exports
  fs = require 'fs'
else
  Root = window

# Create our class and add to global scope
Root.PSD = class PSD
  # Version number
  @VERSION = "0.4.4"

  # Enable/disable debugging console logs
  @DEBUG = false

  # Loads a PSD from a file. If we're in node, then this loads the
  # file from the filesystem. If we're in the browser, then this assumes
  # it has been passed a File object (either from a file input element,
  # or from HTML5 drag & drop).
  @fromFile: (file, cb = ->) ->
    if exports?
      # We're in node. Load via fs module.
      # Callback function isn't needed.
      data = fs.readFileSync file
      new PSD data
    else
      # We're in the browser. Assume we have a File object.
      reader = new FileReader()
      reader.onload = (f) ->
        # In order to convert the file data to a useful format, we need
        # to conver the buffer into a byte array.
        bytes = new Uint8Array(f.target.result)

        psd = new PSD(bytes)
        cb(psd)

      reader.readAsArrayBuffer(file)

  # Load a PSD from a URL via ajax
  @fromURL: (url, cb = ->) ->
    xhr = new XMLHttpRequest
    xhr.open "GET", url, true
    xhr.responseType = "arraybuffer"
    xhr.onload = ->
      data = new Uint8Array(xhr.response or xhr.mozResponseArrayBuffer)
      psd = new PSD(data)
      cb(psd)

    xhr.send null

  options:
    layerImages: false # Should we parse layer image data?
    onlyVisibleLayers: false # Should we skip invisible layer image parsing?
  
  constructor: (data) ->
    # Store the main reference to our PSD file
    @file = new PSDFile data

    @header = null
    @resources = null
    @layerMask = null
    @layers = null
    @images = null
    @image = null

  setOptions: (options) ->
    @options[key] = val for own key, val of options

  # Attempt to parse all sections of the PSD file
  parse: ->
    Log.debug "Beginning parsing"
    @startTime = (new Date()).getTime()

    # It's important to parse all of the file sections in the correct order,
    # which is used here.
    @parseHeader()
    @parseImageResources()
    @parseLayersMasks()
    @parseImageData()

    @endTime = (new Date()).getTime()
    Log.debug "Parsing finished in #{@endTime - @startTime}ms"

  # Parse the first section: the header.
  # This section cannot be skipped, since it contains important parsing information
  # for the rest of the PSD file (and is relatively small anyways).
  parseHeader: ->
    Log.debug "\n### Header ###"

    # Store a reference to the file header
    @header = new PSDHeader @file
    @header.parse()

    Log.debug @header

  parseImageResources: (skip = false) ->
    Log.debug "\n### Resources ###"

    # Every PSD file has a number of resources, so we simply store them in an
    # array for now. In the future, it might make more sense to store resources
    # in an object indexed by the resource ID.
    @resources = []

    # Find the size of the resources section
    n = @file.readInt()
    length = n

    if skip
      Log.debug "Skipped!"
      return @file.seek n

    start = @file.tell()

    # Continue parsing resources until we've reached the end of the section.
    while n > 0
      pos = @file.tell()

      resource = new PSDResource @file
      resource.parse()

      n -= @file.tell() - pos
      @resources.push resource

      Log.debug "Resource: ", resource

    # This shouldn't happen. If it does, then likely something is being parsed
    # incorrectly in one of the resources, or the file is corrupt.
    if n isnt 0
      Log.debug "Image resources overran expected size by #{-n} bytes"
      @file.seek start + length

  parseLayersMasks: (skip = false) ->
    @parseHeader() unless @header
    @parseImageResources(true) unless @resources

    Log.debug "\n### Layers & Masks ###"

    @layerMask = new PSDLayerMask @file, @header, @options
    @layers = @layerMask.layers

    if skip
      Log.debug "Skipped!"
      @layerMask.skip()
    else
      @layerMask.parse()

  parseImageData: ->
    @parseHeader() unless @header
    @parseImageResources(true) unless @resources
    @parseLayersMasks(true) unless @layerMask

    @image = new PSDImage @file, @header
    @image.parse()

  # Folder layers are denoted by a flag, isFolder. This marks the beginning
  # of the folder. The end of the folder is marked by the isHidden flag.
  getLayerStructure: ->
    @parseLayersMasks() unless @layerMask

    result = {layers: []}
    parseStack = []
    for layer in @layers
      if layer.isFolder
        parseStack.push result
        result = {name: layer.name, layers: []}
      else if layer.isHidden
        temp = result
        result = parseStack.pop()
        result.layers.push temp
      else
        result.layers.push layer

    result

  # Exports a flattened version to a file. For use in NodeJS.
  toFile: (filename, cb = ->) -> 
    @parseImageData() unless @image
    @image.toFile filename, cb

  toFileSync: (filename) ->
    @parseImageData() unless @image
    @image.toFileSync filename

  # Given a canvas element
  toCanvas: (canvas, width = null, height = null) ->
    @parseImageData() unless @image
    @image.toCanvas canvas, width, height

  toImage: ->
    @parseImageData() unless @image
    @image.toImage()

  # Extracts all parsed data from this PSD in a clean JSON
  # format excluding file and image data.
  toJSON: ->
    @parseLayersMasks() unless @layerMask

    sections = [
      'header'
      'layerMask'
    ]

    data = resources: []

    for resource in @resources
      data.resources.push resource.toJSON()

    for section in sections
      data[section] = @[section].toJSON()

    data