psd 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,6 +75,10 @@ class PSD
75
75
  read(length).encode('UTF-8', 'MacRoman').delete("\000")
76
76
  end
77
77
 
78
+ def read_byte
79
+ read(1).bytes.to_a[0]
80
+ end
81
+
78
82
  # Reads a unicode string, which is double the length of a normal string and encoded as UTF-16.
79
83
  def read_unicode_string(length=nil)
80
84
  length ||= read_int if length.nil?
@@ -83,7 +87,7 @@ class PSD
83
87
 
84
88
  # Reads a boolean value.
85
89
  def read_boolean
86
- read(1).bytes.to_a[0] != 0
90
+ read_byte != 0
87
91
  end
88
92
 
89
93
  # Reads a 32-bit color space value.
@@ -8,6 +8,9 @@ class PSD
8
8
  include ImageMode::RGB
9
9
  include Export::PNG
10
10
 
11
+ attr_reader :pixel_data, :opacity, :has_mask
12
+ alias :has_mask? :has_mask
13
+
11
14
  # All of the possible compression formats Photoshop uses.
12
15
  COMPRESSIONS = [
13
16
  'Raw',
@@ -28,6 +31,8 @@ class PSD
28
31
  calculate_length
29
32
  @channel_data = []
30
33
  @pixel_data = []
34
+ @opacity = 1.0
35
+ @has_mask = false
31
36
 
32
37
  @start_pos = @file.tell
33
38
  @end_pos = @start_pos + @length
@@ -110,5 +115,9 @@ class PSD
110
115
  def pixel_step
111
116
  depth == 8 ? 1 : 2
112
117
  end
118
+
119
+ def pixel(i)
120
+ @pixel_data[i]
121
+ end
113
122
  end
114
123
  end
@@ -8,7 +8,7 @@ class PSD::Image
8
8
  # data.
9
9
  def to_png
10
10
  PSD.logger.debug "Beginning PNG export"
11
- png = ChunkyPNG::Image.new(width.to_i, height.to_i, ChunkyPNG::Color::TRANSPARENT)
11
+ png = ChunkyPNG::Canvas.new(width.to_i, height.to_i, ChunkyPNG::Color::TRANSPARENT)
12
12
 
13
13
  i = 0
14
14
  height.times do |y|
@@ -22,6 +22,59 @@ class PSD::Image
22
22
  end
23
23
  alias :export :to_png
24
24
 
25
+ def to_png_with_mask
26
+ return to_png unless has_mask?
27
+
28
+ PSD.logger.debug "Beginning PNG export with mask"
29
+
30
+ # We generate the preview at the document size instead to make applying the mask
31
+ # significantly easier.
32
+ width = @layer.header.width.to_i
33
+ height = @layer.header.height.to_i
34
+ png = ChunkyPNG::Canvas.new(width, height, ChunkyPNG::Color::TRANSPARENT)
35
+
36
+ i = 0
37
+ @layer.height.times do |y|
38
+ @layer.width.times do |x|
39
+ png[x + @layer.left, y + @layer.top] = @pixel_data[i]
40
+ i += 1
41
+ end
42
+ end
43
+
44
+ # Now we apply the mask
45
+ i = 0
46
+ @layer.mask.height.times do |y|
47
+ @layer.mask.width.times do |x|
48
+ offset_x = @layer.mask.left + x
49
+ offset_y = @layer.mask.top + y
50
+
51
+ color = ChunkyPNG::Color.to_truecolor_alpha_bytes(png.get_pixel(offset_x, offset_y))
52
+ color[3] = color[3] * @mask_data[i] / 255
53
+
54
+ png.set_pixel(offset_x, offset_y, ChunkyPNG::Color.rgba(*color))
55
+ i += 1
56
+ end
57
+ end
58
+
59
+ png.crop!(@layer.left, @layer.top, @layer.width.to_i, @layer.height.to_i)
60
+ end
61
+
62
+ def mask_to_png
63
+ return unless has_mask?
64
+
65
+ png = ChunkyPNG::Canvas.new(@layer.mask.width.to_i, @layer.mask.height.to_i, ChunkyPNG::Color::TRANSPARENT)
66
+
67
+ i = 0
68
+ @layer.mask.height.times do |y|
69
+ @layer.mask.width.times do |x|
70
+ png[x, y] = ChunkyPNG::Color.grayscale(@mask_data[i])
71
+ i += 1
72
+ end
73
+ end
74
+
75
+ png
76
+ end
77
+
25
78
  # Saves the PNG data to disk.
26
79
  def save_as_png(file)
27
80
  to_png.save(file, :fast_rgba)
@@ -5,7 +5,7 @@ class PSD
5
5
  private
6
6
 
7
7
  def parse_raw!(length = @length)
8
- @length.times do |i|
8
+ length.times do |i|
9
9
  @channel_data[i] = @file.read(1).bytes.to_a[0]
10
10
  end
11
11
  end
@@ -10,12 +10,15 @@ class PSD
10
10
  (0...@num_pixels).step(pixel_step) do |i|
11
11
  r = g = b = 0
12
12
  a = 255
13
+ mask = nil
13
14
 
14
15
  @channels_info.each_with_index do |chan, index|
16
+ next if chan[:id] == -2
17
+
15
18
  val = @channel_data[i + (@channel_length * index)]
16
19
 
17
20
  case chan[:id]
18
- when -1 then a = val
21
+ when -1 then a = (val * opacity).to_i
19
22
  when 0 then r = val
20
23
  when 1 then g = val
21
24
  when 2 then b = val
@@ -14,12 +14,13 @@ class PSD
14
14
  include PathComponents
15
15
  include PositionAndChannels
16
16
 
17
- attr_reader :id, :info_keys
17
+ attr_reader :id, :info_keys, :header
18
18
  attr_accessor :group_layer, :node, :file
19
19
 
20
20
  # Initializes all of the defaults for the layer.
21
- def initialize(file)
21
+ def initialize(file, header)
22
22
  @file = file
23
+ @header = header
23
24
 
24
25
  @mask = {}
25
26
  @blending_ranges = {}
@@ -1,7 +1,20 @@
1
1
  class PSD
2
2
  class Layer
3
3
  module BlendModes
4
- attr_reader :blend_mode, :blending_mode, :opacity
4
+ attr_reader :blend_mode, :opacity
5
+
6
+ def blending_mode
7
+ if !info[:section_divider].nil? && info[:section_divider].blend_mode
8
+ BlendMode::BLEND_MODES[info[:section_divider].blend_mode.strip.to_sym]
9
+ else
10
+ @blending_mode
11
+ end
12
+ end
13
+
14
+ # Is the layer below this one a clipping mask?
15
+ def clipped?
16
+ @blend_mode.clipping == 1
17
+ end
5
18
 
6
19
  private
7
20
 
@@ -8,14 +8,16 @@ class PSD
8
8
  def parse_blending_ranges
9
9
  length = @file.read_int
10
10
 
11
+ # Composite gray blend. Contains 2 black values followed by 2 white values.
12
+ # Present but irrelevant for Lab & Grayscale.
11
13
  @blending_ranges[:grey] = {
12
14
  source: {
13
- black: @file.read_short,
14
- white: @file.read_short
15
+ black: [@file.read_byte, @file.read_byte],
16
+ white: [@file.read_byte, @file.read_byte]
15
17
  },
16
18
  dest: {
17
- black: @file.read_short,
18
- white: @file.read_short
19
+ black: [@file.read_byte, @file.read_byte],
20
+ white: [@file.read_byte, @file.read_byte]
19
21
  }
20
22
  }
21
23
 
@@ -25,12 +27,12 @@ class PSD
25
27
  @blending_ranges[:num_channels].times do
26
28
  @blending_ranges[:channels] << {
27
29
  source: {
28
- black: @file.read_short,
29
- white: @file.read_short
30
+ black: [@file.read_byte, @file.read_byte],
31
+ white: [@file.read_byte, @file.read_byte]
30
32
  },
31
33
  dest: {
32
- black: @file.read_short,
33
- white: @file.read_short
34
+ black: [@file.read_byte, @file.read_byte],
35
+ white: [@file.read_byte, @file.read_byte]
34
36
  }
35
37
  }
36
38
  end
@@ -41,16 +43,16 @@ class PSD
41
43
  length += @blending_ranges[:num_channels] * 8
42
44
  outfile.write_int length
43
45
 
44
- outfile.write_short @blending_ranges[:grey][:source][:black]
45
- outfile.write_short @blending_ranges[:grey][:source][:white]
46
- outfile.write_short @blending_ranges[:grey][:dest][:black]
47
- outfile.write_short @blending_ranges[:grey][:dest][:white]
46
+ outfile.write @blending_ranges[:grey][:source][:black].pack('CC')
47
+ outfile.write @blending_ranges[:grey][:source][:white].pack('CC')
48
+ outfile.write @blending_ranges[:grey][:dest][:black].pack('CC')
49
+ outfile.write @blending_ranges[:grey][:dest][:white].pack('CC')
48
50
 
49
51
  @blending_ranges[:num_channels].times do |i|
50
- outfile.write_short @blending_ranges[:channels][i][:source][:black]
51
- outfile.write_short @blending_ranges[:channels][i][:source][:white]
52
- outfile.write_short @blending_ranges[:channels][i][:dest][:black]
53
- outfile.write_short @blending_ranges[:channels][i][:dest][:white]
52
+ outfile.write @blending_ranges[:channels][i][:source][:black].pack('CC')
53
+ outfile.write @blending_ranges[:channels][i][:source][:white].pack('CC')
54
+ outfile.write @blending_ranges[:channels][i][:dest][:black].pack('CC')
55
+ outfile.write @blending_ranges[:channels][i][:dest][:white].pack('CC')
54
56
  end
55
57
 
56
58
  @file.seek length + 4, IO::SEEK_CUR
@@ -45,7 +45,7 @@ class PSD
45
45
  end
46
46
 
47
47
  def fill_opacity
48
- return nil unless info.has_key?(:fill_opacity)
48
+ return 255 unless info.has_key?(:fill_opacity)
49
49
  info[:fill_opacity].enabled
50
50
  end
51
51
  end
@@ -3,6 +3,8 @@ class PSD
3
3
  module Info
4
4
  # All of the extra layer info sections that we know how to parse.
5
5
  LAYER_INFO = {
6
+ blend_clipping_elements: BlendClippingElements,
7
+ blend_interior_elements: BlendInteriorElements,
6
8
  type: TypeTool,
7
9
  legacy_type: LegacyTypeTool,
8
10
  metadata: MetadataSetting,
@@ -15,7 +17,10 @@ class PSD
15
17
  layer_id: LayerID,
16
18
  fill_opacity: FillOpacity,
17
19
  placed_layer: PlacedLayer,
18
- vector_mask: VectorMask
20
+ vector_mask: VectorMask,
21
+ vector_mask_2: VectorMask2,
22
+ vector_stroke: VectorStroke,
23
+ vector_stroke_content: VectorStrokeContent
19
24
  }
20
25
 
21
26
  attr_reader :adjustments
@@ -46,7 +51,7 @@ class PSD
46
51
  PSD.logger.debug "Layer Info: key = #{key}, start = #{pos}, length = #{length}"
47
52
 
48
53
  begin
49
- i = info.new(@file, length)
54
+ i = info.new(self, length)
50
55
  i.parse
51
56
 
52
57
  @adjustments[name] = i
@@ -60,7 +65,7 @@ class PSD
60
65
  end
61
66
 
62
67
  if !info_parsed
63
- PSD.logger.debug "Skipping: key = #{key}, pos = #{@file.tell}, length = #{length}"
68
+ PSD.logger.debug "Skipping: layer = #{name}, key = #{key}, pos = #{@file.tell}, length = #{length}"
64
69
  @file.seek pos + length
65
70
  end
66
71
 
@@ -73,6 +78,10 @@ class PSD
73
78
  @extra_data_end = @file.tell
74
79
  end
75
80
 
81
+ def vector_mask
82
+ info[:vector_mask_2] || info[:vector_mask]
83
+ end
84
+
76
85
  def export_extra_data(outfile)
77
86
  outfile.write @file.read(@extra_data_end - @extra_data_begin)
78
87
  if @path_components && !@path_components.empty?
@@ -7,8 +7,9 @@ class PSD
7
7
  class << self; attr_accessor :key; end
8
8
  @key = ""
9
9
 
10
- def initialize(file, length)
11
- @file = file
10
+ def initialize(layer, length)
11
+ @layer = layer
12
+ @file = layer.file
12
13
  @length = length
13
14
  @section_end = @file.tell + @length
14
15
  @data = {}
@@ -0,0 +1,13 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class BlendClippingElements < LayerInfo
5
+ @key = 'clbl'
6
+
7
+ attr_reader :enabled
8
+ def parse
9
+ @enabled = @file.read_boolean
10
+ @file.seek 3, IO::SEEK_CUR
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class BlendInteriorElements < LayerInfo
5
+ @key = 'infx'
6
+
7
+ attr_reader :enabled
8
+ def parse
9
+ @enabled = @file.read_boolean
10
+ @file.seek 3, IO::SEEK_CUR
11
+ end
12
+ end
13
+ end
@@ -4,8 +4,10 @@ class PSD
4
4
  class LayerNameSource < LayerInfo
5
5
  @key = 'lnsr'
6
6
 
7
+ attr_reader :id
8
+
7
9
  def parse
8
- @data = @file.read_int
10
+ @id = @file.read_string(4)
9
11
  return self
10
12
  end
11
13
  end
@@ -0,0 +1,10 @@
1
+ require_relative 'vector_mask'
2
+
3
+ class PSD
4
+ # Identical to VectorMask, except with a different key. This
5
+ # exists in Photoshop >= CS6. If this key exists, then there
6
+ # is also a vscg key.
7
+ class VectorMask2 < VectorMask
8
+ @key = 'vsms'
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class VectorStroke < LayerInfo
5
+ @key = 'vstk'
6
+
7
+ def parse
8
+ version = @file.read_int
9
+ @data = Descriptor.new(@file).parse
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '../layer_info'
2
+
3
+ class PSD
4
+ class VectorStrokeContent < LayerInfo
5
+ @key = 'vscg'
6
+
7
+ attr_reader :key
8
+
9
+ def parse
10
+ key = @file.read_string(4)
11
+ version = @file.read_int
12
+ @data = Descriptor.new(@file).parse
13
+ end
14
+ end
15
+ end
@@ -51,7 +51,7 @@ class PSD
51
51
 
52
52
  @layer_section_start = @file.tell
53
53
  layer_count.times do
54
- @layers << PSD::Layer.new(@file).parse
54
+ @layers << PSD::Layer.new(@file, @header).parse
55
55
  end
56
56
 
57
57
  layers.each do |layer|
@@ -0,0 +1,84 @@
1
+ class PSD
2
+ class LayerStyles
3
+ # Blend modes in layer effects use different keys
4
+ # than normal layer blend modes. Thanks Adobe.
5
+ BLEND_TRANSLATION = {
6
+ 'Nrml' => 'norm',
7
+ 'Dslv' => 'diss',
8
+ 'Drkn' => 'dark',
9
+ 'Mltp' => 'mul',
10
+ 'CBrn' => 'idiv',
11
+ 'linearBurn' => 'lbrn',
12
+ 'Lghn' => 'lite',
13
+ 'Scrn' => 'scrn',
14
+ 'CDdg' => 'div',
15
+ 'linearDodge' => 'lddg',
16
+ 'Ovrl' => 'over',
17
+ 'SftL' => 'sLit',
18
+ 'HrdL' => 'hLit',
19
+ 'vividLight' => 'vLit',
20
+ 'linearLight' => 'lLit',
21
+ 'pinLight' => 'pLit',
22
+ 'hardMix' => 'hMix',
23
+ 'Dfrn' => 'diff',
24
+ 'Xclu' => 'smud',
25
+ 'H ' => 'hue',
26
+ 'Strt' => 'sat',
27
+ 'Clr ' => 'colr',
28
+ 'Lmns' => 'lum'
29
+ }
30
+
31
+ attr_reader :layer, :data, :png
32
+
33
+ def initialize(layer, png=nil)
34
+ @layer = layer
35
+ @data = layer.info[:object_effects]
36
+ @png = png || layer.image.to_png
37
+
38
+ if @data.nil?
39
+ @applied = true
40
+ else
41
+ @data = @data.data
42
+ @applied = false
43
+ end
44
+ end
45
+
46
+ def apply!
47
+ return png if @applied || data.nil?
48
+
49
+ apply_color_overlay if data.has_key?('SoFi')
50
+ png
51
+ end
52
+
53
+ private
54
+
55
+ def apply_color_overlay
56
+ overlay_data = data['SoFi']
57
+ color_data = overlay_data['Clr ']
58
+ blending_mode = BlendMode::BLEND_MODES[BLEND_TRANSLATION[overlay_data['Md ']].to_sym]
59
+
60
+ width = layer.width.to_i
61
+ height = layer.height.to_i
62
+
63
+ PSD.logger.debug("Layer style: layer = #{layer.name}, type = color overlay, blend mode = #{blending_mode}")
64
+
65
+ for y in 0...height do
66
+ for x in 0...width do
67
+ pixel = png.get_pixel(x, y)
68
+ alpha = ChunkyPNG::Color.a(pixel)
69
+ next if alpha == 0
70
+
71
+ overlay_color = ChunkyPNG::Color.rgba(
72
+ color_data['Rd '].round,
73
+ color_data['Grn '].round,
74
+ color_data['Bl '].round,
75
+ alpha
76
+ )
77
+
78
+ color = Compose.send(blending_mode, overlay_color, pixel)
79
+ png.set_pixel(x, y, color)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end