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 +4 -4
- data/README.md +21 -6
- data/lib/psd.rb +0 -2
- data/lib/psd/blend_mode.rb +4 -3
- data/lib/psd/channel_image.rb +29 -2
- data/lib/psd/clipping_mask.rb +40 -0
- data/lib/psd/color.rb +15 -13
- data/lib/psd/compose.rb +357 -0
- data/lib/psd/file.rb +5 -1
- data/lib/psd/image.rb +9 -0
- data/lib/psd/image_exports/png.rb +54 -1
- data/lib/psd/image_formats/raw.rb +1 -1
- data/lib/psd/image_modes/rgb.rb +4 -1
- data/lib/psd/layer.rb +3 -2
- data/lib/psd/layer/blend_modes.rb +14 -1
- data/lib/psd/layer/blending_ranges.rb +18 -16
- data/lib/psd/layer/helpers.rb +1 -1
- data/lib/psd/layer/info.rb +12 -3
- data/lib/psd/layer_info.rb +3 -2
- data/lib/psd/layer_info/blend_clipping_elements.rb +13 -0
- data/lib/psd/layer_info/blend_interior_elements.rb +13 -0
- data/lib/psd/layer_info/layer_name_source.rb +3 -1
- data/lib/psd/layer_info/vector_mask_2.rb +10 -0
- data/lib/psd/layer_info/vector_stroke.rb +12 -0
- data/lib/psd/layer_info/vector_stroke_content.rb +15 -0
- data/lib/psd/layer_mask.rb +1 -1
- data/lib/psd/layer_styles.rb +84 -0
- data/lib/psd/node.rb +2 -1
- data/lib/psd/node_layer.rb +7 -1
- data/lib/psd/nodes/ancestry.rb +12 -0
- data/lib/psd/nodes/build_preview.rb +69 -0
- data/lib/psd/nodes/search.rb +1 -0
- data/lib/psd/resources/slices.rb +27 -0
- data/lib/psd/version.rb +1 -1
- data/spec/files/slices.psd +0 -0
- data/spec/image_spec.rb +2 -2
- data/spec/slices_spec.rb +52 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9cbcd0dd708df678b81c59484b465c4e83153dfd
|
4
|
+
data.tar.gz: 3f8a84f7b4a8dd36c1567dd11e8e3c751a22a1a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
data/lib/psd/blend_mode.rb
CHANGED
@@ -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
|
data/lib/psd/channel_image.rb
CHANGED
@@ -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
|
data/lib/psd/color.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
40
|
+
def rgb_to_color(*args)
|
39
41
|
argb_to_color(255, *args)
|
40
42
|
end
|
41
43
|
|
42
|
-
def
|
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
|
48
|
+
def hsb_to_color(*args)
|
47
49
|
ahsb_to_color(255, *args)
|
48
50
|
end
|
49
51
|
|
50
|
-
def
|
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
|
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
|
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
|
98
|
+
def lab_to_color(*args)
|
97
99
|
alab_to_color(255, *args)
|
98
100
|
end
|
99
101
|
|
100
|
-
def
|
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
|
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
|
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,
|
data/lib/psd/compose.rb
ADDED
@@ -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
|