psd 1.3.3 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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