psd
Version:
A general purpose Photoshop file parser.
151 lines (124 loc) • 5.81 kB
text/coffeescript
# The Node abstraction is one of the most important in PSD.js. It's the base for the
# tree representation of the document structure. Every layer and group is a node in
# the document tree. All common functionality is here, and both layers and groups extend
# this class with specialized functionality.
#
# While you can access the layer data directly, the Node interface provides a somewhat
# higher-level API that makes it easier and less verbose to access the wealth of
# information that's stored in each PSD.
_ = require 'lodash'
{Module} = require 'coffeescript-module'
module.exports = class Node extends Module
# We have a couple of important mixins that provide some really cool functionality.
@includes require('./nodes/ancestry.coffee')
@includes require('./nodes/search.coffee')
@includes require('./nodes/build_preview.coffee')
# There are some common properties that are shared by all Node types. We define them
# here to DRY up the code a little, especially when exporting data.
@PROPERTIES: ['name', 'left', 'right', 'top', 'bottom', 'height', 'width']
# Each Node subclass defines a type, which makes it easier to idenfity what we're
# dealing with, since `constructor.name` can get mangled during minification and
# wreak havoc.
type: 'node'
# Every node gets a reference to the layer/group and its parent, which allows us to
# traverse the tree structure. It also builds references to all of its children.
constructor: (@layer, @parent = null) ->
@layer.node = @
@_children = []
# We go ahead and copy the layer name to the node since it's such a commonly
# accessed property.
@name = @layer.name
@forceVisible = null
# We also store the coordinates on the node, especially since we'll eventually be
# able to modify them based on layer comp data.
@coords =
top: @layer.top
bottom: @layer.bottom
left: @layer.left
right: @layer.right
@topOffset = 0
@leftOffset = 0
@createProperties()
createProperties: ->
# The coordinate properties take into consideration any offsets that might be set
# by the current layer comp. This is not implemented yet, but will be in the future.
Object.defineProperty @, 'top',
get: -> @coords.top + @topOffset
set: (val) -> @coords.top = val
Object.defineProperty @, 'right',
get: -> @coords.right + @leftOffset
set: (val) -> @coords.right = val
Object.defineProperty @, 'bottom',
get: -> @coords.bottom + @topOffset
set: (val) -> @coords.bottom = val
Object.defineProperty @, 'left',
get: -> @coords.left + @leftOffset
set: (val) -> @coords.left = val
# We take the tiny overhead of recalculating this every time since the offset could
# change based on the layer comp.
Object.defineProperty @, 'width', get: -> @right - @left
Object.defineProperty @, 'height', get: -> @bottom - @top
# **All properties should be accessed through `get()`**. While many things can be
# accessed without it, using `get()` provides 2 things:
#
# * Consistency
# * Access to both data on the Node and the Layer through the same interface.
#
# This makes it much cleaner to access stuff like layer info blocks, since you just
# give the name of the block you want to access. For example:
#
# ``` coffeescript
# node.get('typeTool').export()
#
# # vs
#
# node.layer.typeTool().export()
# ```
get: (prop) ->
value = if @[prop]? then @[prop] else @layer[prop]
if typeof value is 'function' then value() else value
# Is this layer/group visible? This checks all possible places that could define
# whether or not this is true, e.g. clipping masks. It also checks the current
# layer comp visibility override (not implemented yet).
visible: ->
return false if @layer.clipped and not @clippingMask().visible()
if @forceVisible? then @forceVisible else @layer.visible
hidden: -> not @visible()
isLayer: -> @type is 'layer'
isGroup: -> @type is 'group'
isRoot: -> @type is 'root'
# Retrieves the clipping mask for this node. Because a clipping mask can be applied
# to multiple layers, we have to traverse the tree until we find the first node that
# does not have the `clipped` flag. We can do it this way because all layers that
# the clipping node affects must be siblings and in sequence.
clippingMask: ->
return null unless @layer.clipped
@clippingMaskCached or= (
maskNode = @nextSibling()
maskNode = maskNode.nextSibling() while maskNode.clipped
maskNode
)
clippedBy: -> @clippingMask()
# We can export the most important information about this node as a plain object.
# If we're exporting a group, it will recursively export itself and all of it's descendants as well.
export: ->
hash =
type: null
visible: @visible()
opacity: @layer.opacity / 255.0
blendingMode: @layer.blendingMode()
hash[prop] = @[prop] for prop in Node.PROPERTIES
hash
# While the PSD document does not define explicit dimensions for groups, we can generate
# them based on the bounding boxes of their layer children. When we build the tree structure,
# we update the dimensions of the group whenever a layer is added so that we finish with
# the actual bounding box of the group's contents.
updateDimensions: ->
return if @isLayer()
child.updateDimensions() for child in @_children
return if @isRoot()
nonEmptyChildren = @_children.filter((c) -> not c.isEmpty())
@left = _.min(nonEmptyChildren.map((c) -> c.left)) or 0
@top = _.min(nonEmptyChildren.map((c) -> c.top)) or 0
@bottom = _.max(nonEmptyChildren.map((c) -> c.bottom)) or 0
@right = _.max(nonEmptyChildren.map((c) -> c.right)) or 0