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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 448518141d3b745904f809280d0e826e08bf8dc0
4
- data.tar.gz: 4c0725157152e2f924b95cf023603a9b86bd1f54
3
+ metadata.gz: 9cbcd0dd708df678b81c59484b465c4e83153dfd
4
+ data.tar.gz: 3f8a84f7b4a8dd36c1567dd11e8e3c751a22a1a7
5
5
  SHA512:
6
- metadata.gz: e559032f4b160eb26451c38aa4bc3fa7869aa6984f60c5d062596087cf32a38fdc181e3c703a56b22ba88fc965abe727fa158eb427f76042e46c55f694593ab3
7
- data.tar.gz: 4e31eb123c902d3381e3895d262589f8d269aca4e30c8a74f541d03ef6a9e527aefc9c895df5e358f62e5e8f5b584e4a6a5070b4cf83a10892c629ba46550d96
6
+ metadata.gz: e66d305e6d0157a0e4c52d2e84ea8a71ace66dbc7cbaea88469a5e1b8dc91e6e87f75e795a0eb3c612c642150b5b2ed3213e6cb45803643707d8ddb270e7d799
7
+ data.tar.gz: c61813c1eeee66eb8fbe2236462b93fc988f68f864d1f60d88f3135905b0b8ba5bd5a112cdf8855d485e32615c6f1a307658c75843641ac9e160ca7d986c77be
data/README.md CHANGED
@@ -43,7 +43,7 @@ Or install it yourself as:
43
43
 
44
44
  The [full source code documentation](http://rubydoc.info/gems/psd/frames) is available, but here are some common ways to use and access the PSD data:
45
45
 
46
- **Loading a PSD**
46
+ ### Loading a PSD
47
47
 
48
48
  ``` ruby
49
49
  require 'psd'
@@ -74,11 +74,12 @@ PSD.open('path/to/file.psd') do
74
74
  end
75
75
  ```
76
76
 
77
- **Traversing the Document**
77
+ ### Traversing the Document
78
78
 
79
79
  To access the document as a tree structure, use `psd.tree` to get the root node. From there, you can traverse the tree using any of these methods:
80
80
 
81
81
  * `root`: get the root node from anywhere in the tree
82
+ * `children`: get all immediate children of the node
82
83
  * `ancestors`: get all ancestors in the path of this node (excluding the root)
83
84
  * `siblings`: get all sibling tree nodes including the current one (e.g. all layers in a folder)
84
85
  * `descendants`: get all descendant nodes not including the current one
@@ -97,7 +98,7 @@ If you know the path to a group or layer within the tree, you can search by that
97
98
  psd.tree.children_at_path("Version A/Matte")
98
99
  ```
99
100
 
100
- **Layer Comps**
101
+ ### Layer Comps
101
102
 
102
103
  You can also filter nodes based on a layer comp. To generate a new tree consisting only of the layers that are enabled in a certain layer comp:
103
104
 
@@ -112,7 +113,7 @@ puts tree.children.map(&:name)
112
113
 
113
114
  This returns a new node tree and does not alter the original so you won't lose any data.
114
115
 
115
- **Accessing Layer Data**
116
+ ### Accessing Layer Data
116
117
 
117
118
  To get data such as the name or dimensions of a layer:
118
119
 
@@ -134,7 +135,7 @@ psd.tree.descendant_layers.first.text[:font]
134
135
  "font-family: \"HelveticaNeue-Light\", \"AdobeInvisFont\", \"MyriadPro-Regular\";\nfont-size: 33.0pt;\ncolor: rgba(19, 120, 98, 255);"}
135
136
  ```
136
137
 
137
- **Exporting Data**
138
+ ### Exporting Data
138
139
 
139
140
  When working with the tree structure, you can recursively export any node to a Hash.
140
141
 
@@ -196,7 +197,7 @@ png = psd.image.to_png # reference to PNG data
196
197
  psd.image.save_as_png 'path/to/output.png' # writes PNG to disk
197
198
  ```
198
199
 
199
- **Debugging**
200
+ ### Debugging
200
201
 
201
202
  If you run into any problems parsing a PSD, you can enable debug logging via the `PSD_DEBUG` environment variable. For example:
202
203
 
@@ -210,6 +211,20 @@ You can also give a path to a file instead. If you need to enable debugging prog
210
211
  PSD.debug = true
211
212
  ```
212
213
 
214
+ ### Preview Building
215
+
216
+ **This is currently an experimental feature. It works "well enough" but is not perfect yet.**
217
+
218
+ You can build previews of any subset or version of the PSD document. This is useful for generating previews of layer comps or exporting individual layer groups as images.
219
+
220
+ ``` ruby
221
+ # Save a layer comp
222
+ psd.tree.filter_by_comp("Version A").save_as_png('./Version A.png')
223
+
224
+ # Generate PNG of individual layer group
225
+ psd.tree.children_at_path("Group 1").first.to_png
226
+ ```
227
+
213
228
  ## To-do
214
229
 
215
230
  There are a few features that are currently missing from PSD.rb.
data/lib/psd.rb CHANGED
@@ -103,8 +103,6 @@ class PSD
103
103
  @resources = Resources.new(@file)
104
104
  @resources.parse
105
105
 
106
- PSD.logger.debug @resources.data.inspect
107
-
108
106
  return @resources
109
107
  end
110
108
 
@@ -35,17 +35,18 @@ class PSD
35
35
  vLit: 'vivid light',
36
36
  lLit: 'linear light',
37
37
  pLit: 'pin light',
38
- hMix: 'hard mix'
38
+ hMix: 'hard mix',
39
+ pass: 'passthru'
39
40
  }
40
41
 
41
42
  # Get the readable name for this blend mode.
42
43
  def mode
43
- BLEND_MODES[blend_key.to_sym]
44
+ BLEND_MODES[blend_key.strip.to_sym]
44
45
  end
45
46
 
46
47
  # Set the blend mode with the readable name.
47
48
  def mode=(val)
48
- blend_key = BLEND_MODES.invert[val.downcase]
49
+ blend_key = BLEND_MODES.invert[val.strip.downcase]
49
50
  end
50
51
 
51
52
  # Opacity is stored as an integer between 0-255. This converts the opacity
@@ -7,7 +7,8 @@ class PSD
7
7
  include ImageFormat::LayerRLE
8
8
  include ImageFormat::LayerRAW
9
9
 
10
- attr_reader :width, :height
10
+ attr_reader :width, :height, :mask_data
11
+
11
12
 
12
13
  def initialize(file, header, layer)
13
14
  @layer = layer
@@ -18,6 +19,9 @@ class PSD
18
19
  super(file, header)
19
20
 
20
21
  @channels_info = @layer.channels_info
22
+ @has_mask = @layer.mask.width * @layer.mask.height > 0
23
+ @opacity = @layer.opacity / 255.0
24
+ @mask_data = []
21
25
  end
22
26
 
23
27
  def skip
@@ -33,6 +37,7 @@ class PSD
33
37
 
34
38
  def parse
35
39
  PSD.logger.debug "Layer = #{@layer.name}, Size = #{width}x#{height}"
40
+ PSD.logger.debug @channels_info
36
41
 
37
42
  @chan_pos = 0
38
43
 
@@ -65,10 +70,14 @@ class PSD
65
70
  end
66
71
  end
67
72
 
73
+ @width = @layer.width
74
+ @height = @layer.height
75
+
68
76
  if @channel_data.length != @length
69
77
  PSD.logger.error "#{@channel_data.length} read; expected #{@length}"
70
78
  end
71
79
 
80
+ parse_user_mask
72
81
  process_image_data
73
82
  end
74
83
 
@@ -80,9 +89,27 @@ class PSD
80
89
  when 1 then parse_rle!
81
90
  when 2, 3 then parse_zip!
82
91
  else
83
- PSD.logger.error "Unknown image compression. Attempting to skip."
92
+ PSD.logger.error "Unknown image compression: #{@compression}. Attempting to skip."
84
93
  @file.seek(@end_pos)
85
94
  end
86
95
  end
96
+
97
+ def parse_user_mask
98
+ return unless has_mask?
99
+
100
+ channel = @channels_info.select { |c| c[:id] == -2 }.first
101
+ index = @channels_info.index { |c| c[:id] == -2 }
102
+ return if channel.nil?
103
+
104
+ start = @channel_length * index
105
+ length = @layer.mask.width * @layer.mask.height
106
+ PSD.logger.debug "Copying user mask: #{length} bytes at #{start}"
107
+
108
+ @mask_data = @channel_data[start, length]
109
+
110
+ if @mask_data.length != length
111
+ PSD.logger.error "Mask length is incorrect. Expected = #{length}, Actual = #{@mask_data.length}"
112
+ end
113
+ end
87
114
  end
88
115
  end
@@ -0,0 +1,40 @@
1
+ class PSD
2
+ class ClippingMask
3
+ def initialize(layer, png=nil)
4
+ @layer = layer
5
+ @png = png
6
+ end
7
+
8
+ def apply
9
+ return @png unless @layer.clipped?
10
+
11
+ mask = @layer.next_sibling
12
+
13
+ PSD.logger.debug "Applying clipping mask #{mask.name} to #{@layer.name}"
14
+
15
+ width, height = @layer.document_dimensions
16
+ full_png = ChunkyPNG::Canvas.new(width.to_i, height.to_i, ChunkyPNG::Color::TRANSPARENT)
17
+ full_png.compose!(@png, @layer.left, @layer.top)
18
+
19
+ height.times do |y|
20
+ width.times do |x|
21
+ if y < mask.top || y > mask.bottom || x < mask.left || x > mask.right
22
+ alpha = 0
23
+ else
24
+ mask_x = x - mask.left
25
+ mask_y = y - mask.top
26
+
27
+ pixel = mask.image.pixel_data[mask_y * mask.width + mask_x]
28
+ alpha = pixel.nil? ? 0 : ChunkyPNG::Color.a(pixel)
29
+ end
30
+
31
+ color = ChunkyPNG::Color.to_truecolor_alpha_bytes(full_png[x, y])
32
+ color[3] = color[3] * alpha / 255
33
+ full_png[x, y] = ChunkyPNG::Color.rgba(*color)
34
+ end
35
+ end
36
+
37
+ full_png.crop!(@layer.left, @layer.top, @layer.width, @layer.height)
38
+ end
39
+ end
40
+ end
@@ -3,10 +3,12 @@ class PSD
3
3
  # document in the color space as defined by the user instead of a normalized
4
4
  # value of some kind. This means that we have to do all the conversion ourselves
5
5
  # for each color space.
6
- class Color
6
+ module Color
7
+ extend self
8
+
7
9
  # This is a relic of libpsd that will likely go away in a future version. It
8
10
  # stored the entire color value in a 32-bit address space for speed.
9
- def self.color_space_to_argb(color_space, color_component)
11
+ def color_space_to_argb(color_space, color_component)
10
12
  color = case color_space
11
13
  when 0
12
14
  rgb_to_color *color_component
@@ -26,7 +28,7 @@ class PSD
26
28
  color_to_argb(color)
27
29
  end
28
30
 
29
- def self.color_to_argb(color)
31
+ def color_to_argb(color)
30
32
  [
31
33
  (color) >> 24,
32
34
  ((color) & 0x00FF0000) >> 16,
@@ -35,19 +37,19 @@ class PSD
35
37
  ]
36
38
  end
37
39
 
38
- def self.rgb_to_color(*args)
40
+ def rgb_to_color(*args)
39
41
  argb_to_color(255, *args)
40
42
  end
41
43
 
42
- def self.argb_to_color(a, r, g, b)
44
+ def argb_to_color(a, r, g, b)
43
45
  (a << 24) | (r << 16) | (g << 8) | b
44
46
  end
45
47
 
46
- def self.hsb_to_color(*args)
48
+ def hsb_to_color(*args)
47
49
  ahsb_to_color(255, *args)
48
50
  end
49
51
 
50
- def self.ahsb_to_color(alpha, hue, saturation, brightness)
52
+ def ahsb_to_color(alpha, hue, saturation, brightness)
51
53
  if saturation == 0.0
52
54
  b = g = r = (255 * brightness).to_i
53
55
  else
@@ -66,7 +68,7 @@ class PSD
66
68
  argb_to_color alpha, r, g, b
67
69
  end
68
70
 
69
- def self.hue_to_color(hue, m1, m2)
71
+ def hue_to_color(hue, m1, m2)
70
72
  hue = (hue % 360).to_i
71
73
  if hue < 60
72
74
  v = m1 + (m2 - m1) * hue / 60
@@ -81,7 +83,7 @@ class PSD
81
83
  (v * 255).to_i
82
84
  end
83
85
 
84
- def self.cmyk_to_color(c, m, y, k)
86
+ def cmyk_to_color(c, m, y, k)
85
87
  r = 1 - (c * (1 - k) + k) * 255
86
88
  g = 1 - (m * (1 - k) + k) * 255
87
89
  b = 1 - (y * (1 - k) + k) * 255
@@ -93,16 +95,16 @@ class PSD
93
95
  rgb_to_color r, g, b
94
96
  end
95
97
 
96
- def self.lab_to_color(*args)
98
+ def lab_to_color(*args)
97
99
  alab_to_color(255, *args)
98
100
  end
99
101
 
100
- def self.alab_to_color(alpha, l, a, b)
102
+ def alab_to_color(alpha, l, a, b)
101
103
  xyz = lab_to_xyz(l, a, b)
102
104
  axyz_to_color alpha, xyz[:x], xyz[:y], xyz[:z]
103
105
  end
104
106
 
105
- def self.lab_to_xyz(l, a, b)
107
+ def lab_to_xyz(l, a, b)
106
108
  y = (l + 16) / 116
107
109
  x = y + (a / 500)
108
110
  z = y - (b / 200)
@@ -112,7 +114,7 @@ class PSD
112
114
  end
113
115
  end
114
116
 
115
- def self.cmyk_to_rgb(c, m, y, k)
117
+ def cmyk_to_rgb(c, m, y, k)
116
118
  Hash[{
117
119
  r: (65535 - (c * (255 - k) + (k << 8))) >> 8,
118
120
  g: (65535 - (m * (255 - k) + (k << 8))) >> 8,
@@ -0,0 +1,357 @@
1
+ class PSD
2
+ # Collection of methods that composes two RGBA pixels together
3
+ # in various ways based on Photoshop blend modes.
4
+ #
5
+ # Mostly based on similar code from libpsd.
6
+ module Compose
7
+ extend self
8
+
9
+ DEFAULT_OPTS = {
10
+ opacity: 255,
11
+ fill_opacity: 255
12
+ }
13
+
14
+ #
15
+ # Normal blend modes
16
+ #
17
+
18
+ # Normal composition, delegate to ChunkyPNG
19
+ def normal(fg, bg, opts={})
20
+ return fg if opaque?(fg) || fully_transparent?(bg)
21
+ return bg if fully_transparent?(fg)
22
+
23
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
24
+ new_r = blend_channel(r(bg), r(fg), mix_alpha)
25
+ new_g = blend_channel(g(bg), g(fg), mix_alpha)
26
+ new_b = blend_channel(b(bg), b(fg), mix_alpha)
27
+
28
+ rgba(new_r, new_g, new_b, dst_alpha)
29
+ end
30
+
31
+ #
32
+ # Subtractive blend modes
33
+ #
34
+
35
+ def darken(fg, bg, opts={})
36
+ return fg if fully_transparent?(bg)
37
+ return bg if fully_transparent?(fg)
38
+
39
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
40
+ new_r = r(fg) <= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
41
+ new_g = g(fg) <= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
42
+ new_b = b(fg) <= b(bg) ? blend_channel(b(bg), b(fg), mix_alpha) : b(bg)
43
+
44
+ rgba(new_r, new_g, new_b, dst_alpha)
45
+ end
46
+
47
+ def multiply(fg, bg, opts={})
48
+ return fg if fully_transparent?(bg)
49
+ return bg if fully_transparent?(fg)
50
+
51
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
52
+ new_r = blend_channel(r(bg), r(fg) * r(bg) >> 8, mix_alpha)
53
+ new_g = blend_channel(g(bg), g(fg) * g(bg) >> 8, mix_alpha)
54
+ new_b = blend_channel(b(bg), b(fg) * b(bg) >> 8, mix_alpha)
55
+
56
+ rgba(new_r, new_g, new_b, dst_alpha)
57
+ end
58
+
59
+ def color_burn(fg, bg, opts={})
60
+ return fg if fully_transparent?(bg)
61
+ return bg if fully_transparent?(fg)
62
+
63
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
64
+
65
+ calculate_foreground = Proc.new do |b, f|
66
+ if f > 0
67
+ f = ((255 - b) << 8) / f
68
+ f > 255 ? 0 : (255 - f)
69
+ else
70
+ b
71
+ end
72
+ end
73
+
74
+ new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
75
+ new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
76
+ new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)
77
+
78
+ rgba(new_r, new_g, new_b, dst_alpha)
79
+ end
80
+
81
+ def linear_burn(fg, bg, opts={})
82
+ return fg if fully_transparent?(bg)
83
+ return bg if fully_transparent?(fg)
84
+
85
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
86
+
87
+ new_r = blend_channel(r(bg), r(fg) < (255 - r(bg)) ? 0 : r(fg) - 255 - r(bg), mix_alpha)
88
+ new_g = blend_channel(g(bg), g(fg) < (255 - g(bg)) ? 0 : g(fg) - 255 - g(bg), mix_alpha)
89
+ new_b = blend_channel(b(bg), b(fg) < (255 - b(bg)) ? 0 : b(fg) - 255 - b(bg), mix_alpha)
90
+
91
+ rgba(new_r, new_g, new_b, dst_alpha)
92
+ end
93
+
94
+ #
95
+ # Additive blend modes
96
+ #
97
+
98
+ def lighten(fg, bg, opts={})
99
+ return fg if fully_transparent?(bg)
100
+ return bg if fully_transparent?(fg)
101
+
102
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
103
+
104
+ new_r = r(fg) >= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
105
+ new_g = g(fg) >= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
106
+ new_b = b(fg) >= b(bg) ? blend_channel(b(bg), b(fg), mix_alpha) : b(bg)
107
+
108
+ rgba(new_r, new_g, new_b, dst_alpha)
109
+ end
110
+
111
+ def screen(fg, bg, opts={})
112
+ return fg if fully_transparent?(bg)
113
+ return bg if fully_transparent?(fg)
114
+
115
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
116
+
117
+ new_r = blend_channel(r(bg), 255 - ((255 - r(bg)) * (255 - r(fg)) >> 8), mix_alpha)
118
+ new_g = blend_channel(g(bg), 255 - ((255 - g(bg)) * (255 - g(fg)) >> 8), mix_alpha)
119
+ new_b = blend_channel(b(bg), 255 - ((255 - b(bg)) * (255 - b(fg)) >> 8), mix_alpha)
120
+
121
+ rgba(new_r, new_g, new_b, dst_alpha)
122
+ end
123
+
124
+ def color_dodge(fg, bg, opts={})
125
+ return fg if fully_transparent?(bg)
126
+ return bg if fully_transparent?(fg)
127
+
128
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
129
+
130
+ calculate_foreground = Proc.new do |b, f|
131
+ f < 255 ? [(b << 8) / (255 - f), 255].min : b
132
+ end
133
+
134
+ new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
135
+ new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
136
+ new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)
137
+
138
+ rgba(new_r, new_g, new_b, dst_alpha)
139
+ end
140
+
141
+ def linear_dodge(fg, bg, opts={})
142
+ return fg if fully_transparent?(bg)
143
+ return bg if fully_transparent?(fg)
144
+
145
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
146
+
147
+ new_r = blend_channel(r(bg), (r(bg) + r(fg)) > 255 ? 255 : r(bg) + r(fg), mix_alpha)
148
+ new_g = blend_channel(g(bg), (g(bg) + g(fg)) > 255 ? 255 : g(bg) + g(fg), mix_alpha)
149
+ new_b = blend_channel(b(bg), (b(bg) + b(fg)) > 255 ? 255 : b(bg) + b(fg), mix_alpha)
150
+
151
+ rgba(new_r, new_g, new_b, dst_alpha)
152
+ end
153
+
154
+
155
+ #
156
+ # Contrasting blend modes
157
+ #
158
+
159
+ def overlay(fg, bg, opts={})
160
+ return fg if fully_transparent?(bg)
161
+ return bg if fully_transparent?(fg)
162
+
163
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
164
+
165
+ calculate_foreground = Proc.new do |b, f|
166
+ if b < 128
167
+ b * f >> 7
168
+ else
169
+ 255 - ((255 - b) * (255 - f) >> 7)
170
+ end
171
+ end
172
+
173
+ new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
174
+ new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
175
+ new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)
176
+
177
+ rgba(new_r, new_g, new_b, dst_alpha)
178
+ end
179
+
180
+ def soft_light(fg, bg, opts={})
181
+ return fg if fully_transparent?(bg)
182
+ return bg if fully_transparent?(fg)
183
+
184
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
185
+
186
+ calculate_foreground = Proc.new do |b, f|
187
+ c1 = b * f >> 8
188
+ c2 = 255 - ((255 - b) * (255 - f) >> 8)
189
+ ((255 - b) * c1 >> 8) + (b * c2 >> 8)
190
+ end
191
+
192
+ new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
193
+ new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
194
+ new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)
195
+
196
+ rgba(new_r, new_g, new_b, dst_alpha)
197
+ end
198
+
199
+ def hard_light(fg, bg, opts={})
200
+ return fg if fully_transparent?(bg)
201
+ return bg if fully_transparent?(fg)
202
+
203
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
204
+
205
+ calculate_foreground = Proc.new do |b, f|
206
+ if f < 128
207
+ b * f >> 7
208
+ else
209
+ 255 - ((255 - f) * (255 - b) >> 7)
210
+ end
211
+ end
212
+
213
+ new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
214
+ new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
215
+ new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)
216
+
217
+ rgba(new_r, new_g, new_b, dst_alpha)
218
+ end
219
+
220
+ def vivid_light(fg, bg, opts={})
221
+ return fg if fully_transparent?(bg)
222
+ return bg if fully_transparent?(fg)
223
+
224
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
225
+
226
+ calculate_foreground = Proc.new do |b, f|
227
+ if f < 255
228
+ [(b * b / (255 - f) + f * f / (255 - b)) >> 1, 255].min
229
+ else
230
+ b
231
+ end
232
+ end
233
+
234
+ new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
235
+ new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
236
+ new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)
237
+
238
+ rgba(new_r, new_g, new_b, dst_alpha)
239
+ end
240
+
241
+ def linear_light(fg, bg, opts={})
242
+ return fg if fully_transparent?(bg)
243
+ return bg if fully_transparent?(fg)
244
+
245
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
246
+
247
+ calculate_foreground = Proc.new do |b, f|
248
+ if b < 255
249
+ [f * f / (255 - b), 255].min
250
+ else
251
+ 255
252
+ end
253
+ end
254
+
255
+ new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
256
+ new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
257
+ new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)
258
+
259
+ rgba(new_r, new_g, new_b, dst_alpha)
260
+ end
261
+
262
+ def pin_light(fg, bg, opts={})
263
+ return fg if fully_transparent?(bg)
264
+ return bg if fully_transparent?(fg)
265
+
266
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
267
+
268
+ calculate_foreground = Proc.new do |b, f|
269
+ if f >= 128
270
+ [b, (f - 128) * 2].max
271
+ else
272
+ [b, f * 2].min
273
+ end
274
+ end
275
+
276
+ new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
277
+ new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
278
+ new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)
279
+
280
+ rgba(new_r, new_g, new_b, dst_alpha)
281
+ end
282
+
283
+ def hard_mix(fg, bg, opts={})
284
+ return fg if fully_transparent?(bg)
285
+ return bg if fully_transparent?(fg)
286
+
287
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
288
+
289
+ new_r = blend_channel(r(bg), (r(bg) + r(fg) <= 255) ? 0 : 255, mix_alpha)
290
+ new_g = blend_channel(g(bg), (g(bg) + g(fg) <= 255) ? 0 : 255, mix_alpha)
291
+ new_b = blend_channel(b(bg), (b(bg) + b(fg) <= 255) ? 0 : 255, mix_alpha)
292
+
293
+ rgba(new_r, new_g, new_b, dst_alpha)
294
+ end
295
+
296
+ #
297
+ # Inversion blend modes
298
+ #
299
+
300
+ def difference(fg, bg, opts={})
301
+ return fg if fully_transparent?(bg)
302
+ return bg if fully_transparent?(fg)
303
+
304
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
305
+
306
+ new_r = blend_channel(r(bg), (r(bg) - r(fg)).abs, mix_alpha)
307
+ new_g = blend_channel(g(bg), (g(bg) - g(fg)).abs, mix_alpha)
308
+ new_b = blend_channel(b(bg), (b(bg) - b(fg)).abs, mix_alpha)
309
+
310
+ rgba(new_r, new_g, new_b, dst_alpha)
311
+ end
312
+
313
+ def exclusion(fg, bg, opts={})
314
+ return fg if fully_transparent?(bg)
315
+ return bg if fully_transparent?(fg)
316
+
317
+ mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
318
+
319
+ new_r = blend_channel(r(bg), r(bg) + r(fg) - (r(bg) * r(fg) >> 7), mix_alpha)
320
+ new_g = blend_channel(g(bg), g(bg) + g(fg) - (g(bg) * g(fg) >> 7), mix_alpha)
321
+ new_b = blend_channel(b(bg), b(bg) + b(fg) - (b(bg) * b(fg) >> 7), mix_alpha)
322
+
323
+ rgba(new_r, new_g, new_b, dst_alpha)
324
+ end
325
+
326
+ # If the blend mode is missing, we fall back to normal composition.
327
+ def method_missing(method, *args, &block)
328
+ return ChunkyPNG::Color.send(method, *args) if ChunkyPNG::Color.respond_to?(method)
329
+ normal(*args)
330
+ end
331
+
332
+ private
333
+
334
+ def calculate_alphas(fg, bg, opts)
335
+ opacity = calculate_opacity(opts)
336
+ src_alpha = a(fg) * opacity >> 8
337
+ dst_alpha = a(bg)
338
+
339
+ mix_alpha = (src_alpha << 8) / (src_alpha + ((256 - src_alpha) * dst_alpha >> 8))
340
+ dst_alpha = dst_alpha + ((256 - dst_alpha) * src_alpha >> 8)
341
+
342
+ return mix_alpha, dst_alpha
343
+ end
344
+
345
+ def calculate_opacity(opts)
346
+ opts[:opacity] * opts[:fill_opacity] / 255
347
+ end
348
+
349
+ def blend_channel(bg, fg, alpha)
350
+ ((bg << 8) + (fg - bg) * alpha) >> 8
351
+ end
352
+
353
+ def blend_alpha(bg, fg)
354
+ bg + ((255 - bg) * fg >> 8)
355
+ end
356
+ end
357
+ end