Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
dist
doc
examples
shims
test
5,827 changes: 1,889 additions & 3,938 deletions dist/psd.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/psd.js.map

Large diffs are not rendered by default.

21 changes: 10 additions & 11 deletions dist/psd.min.js

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion lib/psd/file.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ module.exports = class File
# Helper that reads a single byte.
readByte: -> @read(1)[0]

# Helper that reads a single signed byte.
readChar: -> new Int8Array(@read(1))[0]

# Helper that reads a single byte and interprets it as a boolean.
readBoolean: -> @readByte() isnt 0

Expand All @@ -77,7 +80,7 @@ module.exports = class File
# Adobe's lovely signed 32-bit fixed-point number with 8bits.24bits
# http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/PhotoshopFileFormats.htm#50577409_17587
readPathNumber: ->
a = @readByte()
a = @readChar()

arr = @read(3)
b1 = arr[0] << 16
Expand Down
1 change: 1 addition & 0 deletions lib/psd/layer/info.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ LAYER_INFO = {
objectEffects: require('../layer_info/object_effects.coffee')
sectionDivider: require('../layer_info/section_divider.coffee')
solidColor: require('../layer_info/solid_color.coffee')
gradientMap: require('../layer_info/gradient_map.coffee')
typeTool: require('../layer_info/typetool.coffee')
vectorMask: require('../layer_info/vector_mask.coffee')
vectorOrigination: require('../layer_info/vector_origination.coffee')
Expand Down
8 changes: 7 additions & 1 deletion lib/psd/layer_info/fill_opacity.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@ LayerInfo = require '../layer_info.coffee'
module.exports = class FillOpacity extends LayerInfo
@shouldParse: (key) -> key is 'iOpa'

constructor: (layer, length) ->
super(layer, length)
@value = 255

parse: ->
@value = @file.readByte()
@value = @file.readByte()

opacity: -> @value
15 changes: 14 additions & 1 deletion lib/psd/layer_info/gradient_fill.coffee
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
LayerInfo = require '../layer_info.coffee'
Descriptor = require '../descriptor.coffee'
Color = require '../color.coffee'

module.exports = class GradientFill extends LayerInfo
@shouldParse: (key) -> key is 'GdFl'

parse: ->
@file.seek 4, true # Skip sig
@data = new Descriptor(@file).parse()
@data = new Descriptor(@file).parse()

colors: ->
clrs = [];
i = @data.Grad.Clrs.length
while (i > 0)
c = @data.Grad.Clrs[i-1]['Clr ']
if c.class.id == 'RGBC'
clrs.unshift([c['Rd '], c['Grn '], c['Bl ']])
else
clrs.unshift(Color.cmykToRgb(2.55 * c['Cyn '], 2.55 * c['Mgnt'], 2.55 * c['Ylw '], 2.55 * c['Blck']))
i--
clrs
57 changes: 57 additions & 0 deletions lib/psd/layer_info/gradient_map.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
LayerInfo = require '../layer_info.coffee'
Descriptor = require '../descriptor.coffee'

module.exports = class GradientMap extends LayerInfo
@shouldParse: (key) -> key is 'grdm'

constructor: (layer, length) ->
super(layer, length)

@gradient = {}
@gradient.inverted = false
@gradient.dithered = false

parse: ->
@file.seek 2, true
@gradient.inverted = @file.readBoolean()
@gradient.dithered = @file.readBoolean()
@gradient.name = @file.readUnicodeString()
@gradient.stops = []
c = @file.readUShort()
while (c > 0)
grad = {}
grad.location = @file.readUInt()
grad.midPoint = @file.readUInt()
grad.mode = @file.readUShort()
grad.color = []
cs = 0
while (cs < @layer.channels)
grad.color.push(@file.readUShort() / 256)
cs++
@gradient.stops.push(grad)
c--

@gradient.transparencies = []
c = @file.readUShort()
while (c > 0)
transp = {}
transp.location = @file.readUInt()
transp.midPoint = @file.readUInt()
transp.opacity = @file.readUShort()
@gradient.transparencies.push(transp)
c--
@gradient.expansionCount = @file.readUShort()
@gradient.interpolation = @file.readUShort()
@gradient.length = @file.readUShort()
@gradient.mode = @file.readUShort()
@gradient.seed = @file.readInt()
@gradient.showTransparency = @file.readUShort()
@gradient.useColor = @file.readUShort()
@gradient.roughness = @file.readInt()
@gradient.colorModel = @file.readUShort()
@gradient.minColor = [@file.readUShort() / 256, @file.readUShort() / 256, @file.readUShort() / 256, @file.readUShort() / 256]
@gradient.maxColor = [@file.readUShort() / 256, @file.readUShort() / 256, @file.readUShort() / 256, @file.readUShort() / 256]
@file.readUShort()

gradientMap: -> @gradient

15 changes: 9 additions & 6 deletions lib/psd/layer_info/solid_color.coffee
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
LayerInfo = require '../layer_info.coffee'
Descriptor = require '../descriptor.coffee'
Color = require '../color.coffee'

module.exports = class SolidColor extends LayerInfo
@shouldParse: (key) -> key is 'SoCo'

constructor: (layer, length) ->
super(layer, length)

@r = @g = @b = 0
@clr = [0, 0, 0]

parse: ->
@file.seek 4, true
@data = new Descriptor(@file).parse()

@r = Math.round @colorData()['Rd ']
@g = Math.round @colorData()['Grn ']
@b = Math.round @colorData()['Bl ']

c = @colorData()
if @data['Clr '].class.id == 'RGBC'
@clr = [Math.round(c['Rd ']), Math.round(c['Grn ']), Math.round(c['Bl '])]
else
@clr = Color.cmykToRgb(2.55 * c['Cyn '], 2.55 * c['Mgnt'], 2.55 * c['Ylw '], 2.55 * c['Blck'])

colorData: -> @data['Clr ']
color: -> [@r, @g, @b]
color: -> @clr
3 changes: 3 additions & 0 deletions lib/psd/layer_info/typetool.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ _ = require 'lodash'
parseEngineData = require 'parse-engine-data'
LayerInfo = require '../layer_info.coffee'
Descriptor = require '../descriptor.coffee'
Color = require '../color.coffee'

module.exports = class TextElements extends LayerInfo
@shouldParse: (key) -> key is 'TySh'
Expand Down Expand Up @@ -118,6 +119,8 @@ module.exports = class TextElements extends LayerInfo
@styles().FillColor.map (s) ->
values = s.Values.map (v) -> Math.round(v * 255)
values.push values.shift() # Change ARGB -> RGBA for consistency
if s.Type == 2
values = Color.cmykToRgb(values[0], values[1], values[2], values[3]).concat(values[4])
values

styles: ->
Expand Down
157 changes: 155 additions & 2 deletions lib/psd/layer_mask.coffee
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
_ = require 'lodash'
iconv = require 'iconv-lite'
Util = require './util.coffee'
Layer = require './layer.coffee'

Expand All @@ -17,7 +18,9 @@ module.exports = class LayerMask
@layers = []
@mergedAlpha = false
@globalMask = null

@patterns = []
@textInfo = []

skip: -> @file.seek @file.readInt(), true

parse: ->
Expand All @@ -33,6 +36,20 @@ module.exports = class LayerMask
# words, they're stored bottom to top and we want them top to bottom.
@layers.reverse()

while @file.pos < finish and !@file.data[@file.pos]
@file.seek 1, true

while @file.pos < finish and @file.readString(4) == '8BIM'
str = @file.readString(4)
sectionLen = @file.readInt()
endSection = ((sectionLen + 3) & ~3) + @file.tell()
if sectionLen > 0
@file.seek -4, true
switch str
when 'Patt', 'Pat2', 'Pat3' then @parsePatterns()
when 'Txt2' then @parseTextInfo()
@file.seek endSection

@file.seek finish

parseLayers: ->
Expand All @@ -54,7 +71,7 @@ module.exports = class LayerMask
length = @file.readInt()
return if length <= 0

maskEnd = @file.tell() + length
maskEnd = @file.tell() + length + 3

@globalMask = _({}).tap (mask) =>
mask.overlayColorSpace = @file.readShort()
Expand All @@ -71,3 +88,139 @@ module.exports = class LayerMask
mask.kind = @file.readByte()

@file.seek maskEnd

getPatternAsPNG: (pattern) ->
canvas = document.createElement('canvas')
canvas.width = pattern.width
canvas.height = pattern.height
ctx = canvas.getContext('2d')
imageData = ctx.createImageData(pattern.width, pattern.height)
pixelData = imageData.data
numPixels = pattern.width * pattern.height
nbChannels = pattern.data.slice(0,24).length
for i in [0...numPixels]
r = g = b = 0
a = 255

for chan in [0...nbChannels]
channelData = pattern.data[chan]
val = channelData[i]

switch chan
when 0 then r = val
when 1 then g = val
when 2 then b = val
when 3 then a = val
pixelData.set([r, g, b, a], i*4)

if pattern.data[24]
channelData = pattern.data[24]
for i in [0...numPixels]
val = channelData[i]
pixelData[i*4 + 3] = val
ctx.putImageData(imageData, 0, 0)
canvas.toDataURL("image/png")

parsePatterns: ->
file = @file
patterns = @patterns
getPatternAsPNG = @getPatternAsPNG
readVirtualMemoryArrayList = ->
file.seek 4, true # version
VMALEnd = file.readInt() + file.tell()
pattern = {top: file.readInt(), left: file.readInt(), bottom: file.readInt(), right : file.readInt(), channels: file.readInt(), data: []}
pattern.width = pattern.right - pattern.left
pattern.height = pattern.bottom - pattern.top
pattern.toURL = () ->
getPatternAsPNG(@)
for i in [0...pattern.channels+2]
lineIndex = 0
chanPos = 0
if !file.readInt()
continue
l = file.readInt()
endChannel = l + file.tell()
depth = file.readInt()
file.readInt()
file.readInt()
file.readInt()
file.readInt()
file.readShort()
compressed = file.readByte()
if compressed
byteCounts = []
pattern.data[i] = new Uint8Array(pattern.width*pattern.height)
for j in [0...pattern.height]
byteCounts.push(file.readShort());
for j in [0...pattern.height]
finish = file.tell() + byteCounts[lineIndex + j]
while file.tell() < finish
len = file.read(1)[0]
if len < 128
len += 1
data = file.read(len)
pattern.data[i].set data, chanPos
chanPos += len
else if len > 128
len ^= 0xff
len += 2
val = file.read(1)[0]
pattern.data[i].fill(val, chanPos, chanPos+len)
chanPos += len
lineIndex += pattern.height
else
pattern.data[i] = new Uint8Array(file.read(l-23))
file.seek endChannel
file.seek VMALEnd
pattern

readPattern = ->
patternEnd = ((file.readInt() + 3) & ~3) + file.tell()
file.seek 4, true # version
mode = file.readInt()
point = [file.readShort(), file.readShort()]
pattern = {name: file.readUnicodeString(), id: file.readString(file.readByte()), palette: []}
if mode == 2
pattern.palette = file.read(256*3)
pattern.data = readVirtualMemoryArrayList()
patterns.push(pattern)
file.seek patternEnd

patternsEnd = file.readInt() + file.tell()
readPattern() while file.tell() < patternsEnd

parseTextInfo: ->
textInfoLen = @file.readInt()
if !textInfoLen
return
endTextInfo = ((textInfoLen + 3) & ~3) + @file.tell()
rawText = ""
c = ''
pc
while 1
if @file.pos >= endTextInfo
break
pc = c
c = @file.readString(1)
if c == '(' and pc == ' '
d = []
l = 0;
while @file.pos+l < endTextInfo and (@file.data[@file.pos+l] != ')'.charCodeAt(0) or @file.data[@file.pos+l+1] != 32 or @file.data[@file.pos+l-1] == '\\'.charCodeAt(0))
if !['('.charCodeAt(0), ')'.charCodeAt(0)].includes(@file.data[@file.pos+l+1]) || @file.data[@file.pos+l] != '\\'.charCodeAt(0)
d.push(@file.data[@file.pos+l])
l++
@file.seek l+2, true
rawText += ' "' + iconv.decode(new Buffer(d), 'utf16').replace(/\u0000/g, "").replace(/\t/gm, "\\t").replace(/\r/gm, "\\r").replace(/\n/gm, "\\n") + '",'
else if c == "<" and @file.data[@file.pos] == "<".charCodeAt(0)
rawText += '{'
@file.seek 1, true
else if c == ">" and @file.data[@file.pos] == ">".charCodeAt(0)
rawText += '},'
@file.seek 1, true
else if c == "]"
rawText += '],'
else if c == "." and [' ', '-'].includes(pc)
rawText += '0.'
else
rawText += c
@textInfo = JSON.parse(('{'+rawText+'}').replace(/ ?\/([\d]+)/g, "\"$1\":").replace(/\/([\w]+) ?/g,"\"$1\",").replace(/(false ?|true ?)/g, '$1,').replace(/"nil ?"/g, 'null,').replace(/([-\.\d]+) ?/g, "$1, ").replace(/, ?(,|}|\])/gm,"$1").replace(/\u0003/gm,"\\u0003").replace(/\u2029/gm,"\\u2029").replace(/"([\d]+), "/g,'"$1"'))
3 changes: 2 additions & 1 deletion lib/psd/resource_section.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ module.exports = class ResourceSection
require('./resources/layer_comps.coffee')
require('./resources/layer_links.coffee')
require('./resources/resolution_info.coffee')
require('./resources/angle.coffee')
]

@factory: (resource) ->
for Section in RESOURCES
continue unless Section::id is resource.id
return _.tap new Section(resource), (s) -> s.parse()

null
null
19 changes: 19 additions & 0 deletions lib/psd/resources/angle.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = class Angle
id: 1037
name: 'angle'

constructor: (@resource) ->
@file = @resource.file

parse: ->
# 32-bit fixed-point number (16.16)
@angle = @file.readUInt()

@resource.data = @

export: ->
data = {}
for key in ['angle']
data[key] = @[key]

data