psd 2.1.2 → 3.9.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 +5 -5
- data/.travis.yml +2 -0
- data/README.md +60 -20
- data/lib/psd/blend_mode.rb +46 -38
- data/lib/psd/channel_image.rb +11 -5
- data/lib/psd/color.rb +14 -5
- data/lib/psd/descriptor.rb +39 -16
- data/lib/psd/file.rb +9 -6
- data/lib/psd/header.rb +38 -33
- data/lib/psd/helpers.rb +2 -2
- data/lib/psd/image.rb +21 -16
- data/lib/psd/image_formats/layer_rle.rb +2 -2
- data/lib/psd/image_formats/rle.rb +4 -10
- data/lib/psd/image_modes/cmyk.rb +18 -5
- data/lib/psd/image_modes/greyscale.rb +6 -1
- data/lib/psd/image_modes/rgb.rb +17 -5
- data/lib/psd/layer/blend_modes.rb +15 -13
- data/lib/psd/layer/helpers.rb +8 -10
- data/lib/psd/layer/info/black_white.rb +33 -0
- data/lib/psd/{layer_info → layer/info}/blend_clipping_elements.rb +4 -2
- data/lib/psd/{layer_info → layer/info}/blend_interior_elements.rb +4 -2
- data/lib/psd/layer/info/brightness_contrast.rb +21 -0
- data/lib/psd/layer/info/channel_blending_restrictions.rb +27 -0
- data/lib/psd/layer/info/channel_mixer.rb +27 -0
- data/lib/psd/layer/info/color_balance.rb +23 -0
- data/lib/psd/layer/info/color_lookup.rb +14 -0
- data/lib/psd/layer/info/content_generator.rb +42 -0
- data/lib/psd/layer/info/curves.rb +78 -0
- data/lib/psd/layer/info/effects_layer.rb +174 -0
- data/lib/psd/layer/info/exposure.rb +20 -0
- data/lib/psd/{layer_info → layer/info}/fill_opacity.rb +4 -2
- data/lib/psd/layer/info/gradient_fill.rb +14 -0
- data/lib/psd/layer/info/gradient_map.rb +69 -0
- data/lib/psd/layer/info/hue_saturation.rb +41 -0
- data/lib/psd/layer/info/invert.rb +17 -0
- data/lib/psd/layer/info/knockout.rb +22 -0
- data/lib/psd/layer/info/layer_effects.rb +16 -0
- data/lib/psd/{layer_info → layer/info}/layer_group.rb +4 -2
- data/lib/psd/{layer_info → layer/info}/layer_id.rb +4 -2
- data/lib/psd/layer/info/layer_mask_as_global_mask.rb +16 -0
- data/lib/psd/{layer_info → layer/info}/layer_name_source.rb +4 -2
- data/lib/psd/{layer_info → layer/info}/layer_section_divider.rb +4 -2
- data/lib/psd/{layer_info → layer/info}/legacy_typetool.rb +6 -4
- data/lib/psd/layer/info/levels.rb +48 -0
- data/lib/psd/{layer_info → layer/info}/locked.rb +4 -2
- data/lib/psd/{layer_info → layer/info}/metadata_setting.rb +5 -3
- data/lib/psd/{layer_info → layer/info}/object_effects.rb +5 -5
- data/lib/psd/layer/info/pattern.rb +14 -0
- data/lib/psd/layer/info/pattern_fill.rb +15 -0
- data/lib/psd/layer/info/photo_filter.rb +44 -0
- data/lib/psd/{layer_info → layer/info}/placed_layer.rb +4 -2
- data/lib/psd/layer/info/posterize.rb +16 -0
- data/lib/psd/{layer_info → layer/info}/reference_point.rb +4 -2
- data/lib/psd/layer/info/selective_color.rb +32 -0
- data/lib/psd/layer/info/sheet_color.rb +36 -0
- data/lib/psd/layer/info/solid_color.rb +36 -0
- data/lib/psd/layer/info/threshold.rb +16 -0
- data/lib/psd/layer/info/transparency_shapes_layer.rb +16 -0
- data/lib/psd/{layer_info → layer/info}/typetool.rb +27 -13
- data/lib/psd/{layer_info → layer/info}/unicode_name.rb +4 -2
- data/lib/psd/{layer_info → layer/info}/vector_mask.rb +7 -5
- data/lib/psd/layer/info/vector_mask_as_global_mask.rb +16 -0
- data/lib/psd/layer/info/vector_origination.rb +14 -0
- data/lib/psd/{layer_info → layer/info}/vector_stroke.rb +4 -2
- data/lib/psd/{layer_info → layer/info}/vector_stroke_content.rb +4 -2
- data/lib/psd/layer/info/vibrance.rb +22 -0
- data/lib/psd/layer/info.rb +106 -19
- data/lib/psd/layer/position_and_channels.rb +2 -6
- data/lib/psd/layer.rb +13 -16
- data/lib/psd/layer_info.rb +0 -4
- data/lib/psd/layer_mask.rb +61 -50
- data/lib/psd/lazy_execute.rb +5 -1
- data/lib/psd/mask.rb +7 -1
- data/lib/psd/node.rb +142 -49
- data/lib/psd/nodes/ancestry.rb +7 -6
- data/lib/psd/nodes/build_preview.rb +4 -4
- data/lib/psd/nodes/group.rb +36 -0
- data/lib/psd/nodes/layer.rb +45 -0
- data/lib/psd/nodes/layer_comps.rb +93 -0
- data/lib/psd/nodes/locking.rb +36 -0
- data/lib/psd/nodes/root.rb +87 -0
- data/lib/psd/nodes/search.rb +8 -65
- data/lib/psd/path_record.rb +5 -70
- data/lib/psd/renderer/blender.rb +10 -5
- data/lib/psd/renderer/cairo_helpers.rb +46 -0
- data/lib/psd/renderer/canvas.rb +41 -20
- data/lib/psd/renderer/canvas_management.rb +2 -2
- data/lib/psd/renderer/clipping_mask.rb +5 -4
- data/lib/psd/renderer/compose.rb +59 -69
- data/lib/psd/renderer/layer_styles/color_overlay.rb +45 -27
- data/lib/psd/renderer/layer_styles.rb +15 -5
- data/lib/psd/renderer/mask.rb +26 -22
- data/lib/psd/renderer/mask_canvas.rb +12 -0
- data/lib/psd/renderer/vector_shape.rb +239 -0
- data/lib/psd/renderer.rb +18 -7
- data/lib/psd/resource_section.rb +13 -6
- data/lib/psd/resources/base.rb +33 -0
- data/lib/psd/resources/guides.rb +6 -4
- data/lib/psd/resources/layer_comps.rb +6 -4
- data/lib/psd/resources/resolution_info.rb +48 -0
- data/lib/psd/resources/saved_path.rb +19 -0
- data/lib/psd/resources/slices.rb +7 -5
- data/lib/psd/resources/work_path.rb +22 -0
- data/lib/psd/resources/xmp_metadata.rb +46 -0
- data/lib/psd/resources.rb +17 -21
- data/lib/psd/slice.rb +44 -0
- data/lib/psd/slices.rb +13 -0
- data/lib/psd/util.rb +4 -2
- data/lib/psd/version.rb +1 -1
- data/lib/psd.rb +31 -45
- data/psd.gemspec +2 -3
- data/spec/files/alignment_modes.psd +0 -0
- data/spec/files/blendmodes.psd +0 -0
- data/spec/files/example.psb +0 -0
- data/spec/hierarchy_spec.rb +5 -0
- data/spec/locked_spec.rb +8 -8
- data/spec/psb_parsing_spec.rb +57 -0
- data/spec/text_spec.rb +13 -1
- metadata +115 -75
- data/circle.yml +0 -6
- data/lib/psd/layer_info/vector_mask_2.rb +0 -10
- data/lib/psd/node_exporting.rb +0 -20
- data/lib/psd/node_group.rb +0 -86
- data/lib/psd/node_layer.rb +0 -81
- data/lib/psd/node_root.rb +0 -93
- data/lib/psd/nodes/has_children.rb +0 -13
- data/lib/psd/nodes/lock_to_origin.rb +0 -7
- data/lib/psd/nodes/parse_layers.rb +0 -18
- data/lib/psd/renderer/layer_styles/drop_shadow.rb +0 -75
- data/lib/psd/section.rb +0 -26
data/lib/psd/path_record.rb
CHANGED
@@ -19,28 +19,13 @@ class PSD
|
|
19
19
|
case @record_type
|
20
20
|
when 0, 3 then read_path_record
|
21
21
|
when 1, 2, 4, 5 then read_bezier_point
|
22
|
+
when 6 then read_path_fill_rule_record
|
22
23
|
when 7 then read_clipboard_record
|
23
24
|
when 8 then read_initial_fill
|
24
25
|
else @file.seek(24, IO::SEEK_CUR)
|
25
26
|
end
|
26
27
|
end
|
27
28
|
|
28
|
-
# Writes out the path to file.
|
29
|
-
def write(outfile)
|
30
|
-
outfile.write_short @record_type
|
31
|
-
case @record_type
|
32
|
-
when 0 then write_path_record(outfile)
|
33
|
-
when 3 then write_path_record(outfile)
|
34
|
-
when 1 then write_bezier_point(outfile)
|
35
|
-
when 2 then write_bezier_point(outfile)
|
36
|
-
when 4 then write_bezier_point(outfile)
|
37
|
-
when 5 then write_bezier_point(outfile)
|
38
|
-
when 7 then write_clipboard_record(outfile)
|
39
|
-
when 8 then write_initial_fill(outfile)
|
40
|
-
else outfile.seek(24, IO::SEEK_CUR)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
29
|
# Exports the path record to an easier to work with hash.
|
45
30
|
def to_hash
|
46
31
|
case @record_type
|
@@ -51,6 +36,7 @@ class PSD
|
|
51
36
|
when 1, 2, 4, 5
|
52
37
|
{
|
53
38
|
linked: @linked,
|
39
|
+
closed: [1, 2].include?(@record_type),
|
54
40
|
preceding: {
|
55
41
|
vert: @preceding_vert,
|
56
42
|
horiz: @preceding_horiz
|
@@ -83,34 +69,6 @@ class PSD
|
|
83
69
|
end.merge({ record_type: @record_type })
|
84
70
|
end
|
85
71
|
|
86
|
-
# Attempts to translate the path
|
87
|
-
def translate(x=0, y=0)
|
88
|
-
return unless is_bezier_point?
|
89
|
-
|
90
|
-
document_width, document_height = @layer.document_dimensions
|
91
|
-
translate_x_ratio = x.to_f / document_width.to_f
|
92
|
-
translate_y_ratio = y.to_f / document_height.to_f
|
93
|
-
|
94
|
-
@preceding_vert += translate_y_ratio
|
95
|
-
@preceding_horiz += translate_x_ratio
|
96
|
-
@anchor_vert += translate_y_ratio
|
97
|
-
@anchor_horiz += translate_x_ratio
|
98
|
-
@leaving_vert += translate_y_ratio
|
99
|
-
@leaving_horiz += translate_x_ratio
|
100
|
-
end
|
101
|
-
|
102
|
-
# Attempts to scale the path
|
103
|
-
def scale(xr, yr)
|
104
|
-
return unless is_bezier_point?
|
105
|
-
|
106
|
-
@preceding_vert *= yr
|
107
|
-
@preceding_horiz *= xr
|
108
|
-
@anchor_vert *= yr
|
109
|
-
@anchor_horiz *= xr
|
110
|
-
@leaving_vert *= yr
|
111
|
-
@leaving_horiz *= xr
|
112
|
-
end
|
113
|
-
|
114
72
|
# Is this record a bezier point?
|
115
73
|
def is_bezier_point?
|
116
74
|
[1,2,4,5].include? @record_type
|
@@ -123,11 +81,6 @@ class PSD
|
|
123
81
|
@file.seek(22, IO::SEEK_CUR)
|
124
82
|
end
|
125
83
|
|
126
|
-
def write_path_record(file)
|
127
|
-
file.write_short @num_points
|
128
|
-
file.seek(22, IO::SEEK_CUR)
|
129
|
-
end
|
130
|
-
|
131
84
|
def read_bezier_point
|
132
85
|
@linked = [1,4].include? @record_type
|
133
86
|
|
@@ -141,13 +94,8 @@ class PSD
|
|
141
94
|
@leaving_horiz = @file.read_path_number
|
142
95
|
end
|
143
96
|
|
144
|
-
def
|
145
|
-
|
146
|
-
outfile.write_path_number @preceding_horiz
|
147
|
-
outfile.write_path_number @anchor_vert
|
148
|
-
outfile.write_path_number @anchor_horiz
|
149
|
-
outfile.write_path_number @leaving_vert
|
150
|
-
outfile.write_path_number @leaving_horiz
|
97
|
+
def read_path_fill_rule_record
|
98
|
+
@file.seek(24, IO::SEEK_CUR)
|
151
99
|
end
|
152
100
|
|
153
101
|
def read_clipboard_record
|
@@ -159,22 +107,9 @@ class PSD
|
|
159
107
|
@file.seek(4, IO::SEEK_CUR)
|
160
108
|
end
|
161
109
|
|
162
|
-
def write_clipboard_record(file)
|
163
|
-
[@clipboard_top, @clipboard_left, @clipboard_bottom,
|
164
|
-
@clipboard_right, @clipboard_resolution].each do |point|
|
165
|
-
file.write_path_number point
|
166
|
-
end
|
167
|
-
file.seek(4, IO::SEEK_CUR)
|
168
|
-
end
|
169
|
-
|
170
110
|
def read_initial_fill
|
171
111
|
@initial_fill = @file.read_short
|
172
112
|
@file.seek(22, IO::SEEK_CUR)
|
173
113
|
end
|
174
|
-
|
175
|
-
def write_initial_fill(file)
|
176
|
-
file.write_short @initial_fill
|
177
|
-
file.seek(22, IO::SEEK_CUR)
|
178
|
-
end
|
179
114
|
end
|
180
|
-
end
|
115
|
+
end
|
data/lib/psd/renderer/blender.rb
CHANGED
@@ -16,7 +16,8 @@ class PSD
|
|
16
16
|
# Composes the foreground Canvas onto the background Canvas using the
|
17
17
|
# blending mode specified by the foreground.
|
18
18
|
def compose!
|
19
|
-
PSD.logger.debug "
|
19
|
+
PSD.logger.debug "#{fg.node.debug_name} -> #{bg.node.debug_name}: #{fg.node.blending_mode} blending"
|
20
|
+
PSD.logger.debug "fg: (#{fg.left}, #{fg.top}) #{fg.width}x#{fg.height}; bg: (#{bg.left}, #{bg.top}) #{bg.width}x#{bg.height}"
|
20
21
|
|
21
22
|
offset_x = fg.left - bg.left
|
22
23
|
offset_y = fg.top - bg.top
|
@@ -30,12 +31,12 @@ class PSD
|
|
30
31
|
|
31
32
|
color = Compose.send(
|
32
33
|
fg.node.blending_mode,
|
33
|
-
fg.
|
34
|
-
bg.
|
35
|
-
|
34
|
+
fg.get_pixel(x, y),
|
35
|
+
bg.get_pixel(base_x, base_y),
|
36
|
+
calculated_opacity
|
36
37
|
)
|
37
38
|
|
38
|
-
bg.
|
39
|
+
bg.set_pixel base_x, base_y, color
|
39
40
|
end
|
40
41
|
end
|
41
42
|
end
|
@@ -48,6 +49,10 @@ class PSD
|
|
48
49
|
fill_opacity: @fill_opacity
|
49
50
|
}
|
50
51
|
end
|
52
|
+
|
53
|
+
def calculated_opacity
|
54
|
+
@calculated_opacity ||= compose_options[:opacity] * compose_options[:fill_opacity] / 255
|
55
|
+
end
|
51
56
|
end
|
52
57
|
end
|
53
58
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class PSD
|
2
|
+
class Renderer
|
3
|
+
# Adapted from
|
4
|
+
# http://www.hokstad.com/simple-drawing-in-ruby-with-cairo
|
5
|
+
module CairoHelpers
|
6
|
+
def cairo_image_surface(w, h, bg=nil)
|
7
|
+
surface = Cairo::ImageSurface.new(w, h)
|
8
|
+
cr = Cairo::Context.new(surface)
|
9
|
+
|
10
|
+
if bg
|
11
|
+
cr.set_source_rgba(*bg)
|
12
|
+
cr.paint
|
13
|
+
end
|
14
|
+
|
15
|
+
yield cr
|
16
|
+
|
17
|
+
surface.finish
|
18
|
+
|
19
|
+
data = cr.target.data.to_s[0, 4 * w * h]
|
20
|
+
|
21
|
+
# Cairo data is stored as BGRA, ugh.
|
22
|
+
data = data.unpack("N*").map do |color|
|
23
|
+
color = ChunkyPNG::Color.to_truecolor_alpha_bytes(color)
|
24
|
+
ChunkyPNG::Color.rgba(color[2], color[1], color[0], color[3])
|
25
|
+
end
|
26
|
+
|
27
|
+
ChunkyPNG::Canvas.new(w, h, data)
|
28
|
+
end
|
29
|
+
|
30
|
+
def cairo_path(cr, *pairs)
|
31
|
+
first = true
|
32
|
+
pairs.each do |cmd|
|
33
|
+
if cmd == :c
|
34
|
+
cr.close_path
|
35
|
+
first = true
|
36
|
+
elsif first
|
37
|
+
cr.move_to(*cmd)
|
38
|
+
first = false
|
39
|
+
else
|
40
|
+
cr.curve_to(*cmd)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/psd/renderer/canvas.rb
CHANGED
@@ -1,34 +1,35 @@
|
|
1
1
|
class PSD
|
2
2
|
class Renderer
|
3
3
|
class Canvas
|
4
|
-
|
4
|
+
extend Forwardable
|
5
5
|
|
6
|
-
|
6
|
+
attr_reader :canvas, :node, :opts, :width, :height, :opacity, :fill_opacity
|
7
|
+
|
8
|
+
def_delegators :node, :top, :right, :bottom, :left
|
9
|
+
def_delegators :canvas, :[], :[]=, :get_pixel, :set_pixel
|
10
|
+
|
11
|
+
def initialize(node, width = nil, height = nil, opts = {})
|
7
12
|
@node = node
|
13
|
+
@opts = opts
|
8
14
|
@pixel_data = @node.group? ? [] : @node.image.pixel_data
|
9
15
|
|
10
16
|
@width = (width || @node.width).to_i
|
11
17
|
@height = (height || @node.height).to_i
|
12
|
-
@left = @node.left.to_i
|
13
|
-
@right = @node.right.to_i
|
14
|
-
@top = @node.top.to_i
|
15
|
-
@bottom = @node.bottom.to_i
|
16
18
|
|
17
19
|
@opacity = @node.opacity.to_f
|
18
20
|
@fill_opacity = @node.fill_opacity.to_f
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
initialize_canvas unless @node.group?
|
22
|
+
initialize_canvas
|
23
23
|
end
|
24
24
|
|
25
25
|
def paint_to(base)
|
26
26
|
PSD.logger.debug "Painting #{node.name} to #{base.node.debug_name}"
|
27
27
|
|
28
|
-
|
28
|
+
apply_masks
|
29
29
|
apply_clipping_mask
|
30
30
|
apply_layer_styles
|
31
31
|
apply_layer_opacity
|
32
|
+
|
32
33
|
compose_pixels(base)
|
33
34
|
end
|
34
35
|
|
@@ -38,9 +39,6 @@ class PSD
|
|
38
39
|
@height = @canvas.height
|
39
40
|
end
|
40
41
|
|
41
|
-
def [](x, y); @canvas[x, y]; end
|
42
|
-
def []=(x, y, value); @canvas[x, y] = value; end
|
43
|
-
|
44
42
|
def method_missing(method, *args, &block)
|
45
43
|
@canvas.send(method, *args, &block)
|
46
44
|
end
|
@@ -48,20 +46,43 @@ class PSD
|
|
48
46
|
private
|
49
47
|
|
50
48
|
def initialize_canvas
|
51
|
-
PSD.logger.debug "Initializing canvas for #{node.debug_name}"
|
49
|
+
PSD.logger.debug "Initializing canvas for #{node.debug_name}; color = #{ChunkyPNG::Color.to_truecolor_alpha_bytes(fill_color)}"
|
50
|
+
|
51
|
+
@canvas = ChunkyPNG::Canvas.new(@width, @height, fill_color)
|
52
|
+
return if @node.group? || has_fill? || @opts[:base]
|
52
53
|
|
53
54
|
# Sorry, ChunkyPNG.
|
54
55
|
@canvas.send(:replace_canvas!, width, height, @pixel_data)
|
55
|
-
|
56
|
+
ensure
|
56
57
|
# This can now be referenced by @canvas.pixels
|
57
58
|
@pixel_data = nil
|
58
59
|
end
|
59
60
|
|
60
|
-
def
|
61
|
-
|
61
|
+
def has_fill?
|
62
|
+
!@opts[:base] && @node.layer? && @node.solid_color
|
63
|
+
end
|
64
|
+
|
65
|
+
def fill_color
|
66
|
+
if has_fill?
|
67
|
+
@node.solid_color.color
|
68
|
+
else
|
69
|
+
ChunkyPNG::Color::TRANSPARENT
|
70
|
+
end
|
71
|
+
end
|
62
72
|
|
63
|
-
|
64
|
-
|
73
|
+
def apply_masks
|
74
|
+
([@node] + @node.ancestors).each do |n|
|
75
|
+
next unless n.raster_mask?
|
76
|
+
break if n.group? && !n.passthru_blending?
|
77
|
+
|
78
|
+
if n.layer?
|
79
|
+
PSD.logger.debug "Applying raster mask to #{@node.name}"
|
80
|
+
Mask.new(self).apply!
|
81
|
+
else
|
82
|
+
PSD.logger.debug "Applying raster mask to #{@node.name} from #{n.name}"
|
83
|
+
Mask.new(self, n).apply!
|
84
|
+
end
|
85
|
+
end
|
65
86
|
end
|
66
87
|
|
67
88
|
def apply_clipping_mask
|
@@ -92,4 +113,4 @@ class PSD
|
|
92
113
|
end
|
93
114
|
end
|
94
115
|
end
|
95
|
-
end
|
116
|
+
end
|
@@ -5,9 +5,9 @@ class PSD
|
|
5
5
|
@canvas_stack.last
|
6
6
|
end
|
7
7
|
|
8
|
-
def create_group_canvas(node, width
|
8
|
+
def create_group_canvas(node, width = @width, height = @height, opts = {})
|
9
9
|
PSD.logger.debug "Group canvas created. Node = #{node.name || ":root:"}, width = #{width}, height = #{height}"
|
10
|
-
push_canvas Canvas.new(node, width, height)
|
10
|
+
push_canvas Canvas.new(node, width, height, @opts.merge(opts))
|
11
11
|
end
|
12
12
|
|
13
13
|
def push_canvas(canvas)
|
@@ -7,8 +7,9 @@ class PSD
|
|
7
7
|
@canvas = canvas
|
8
8
|
@node = @canvas.node
|
9
9
|
|
10
|
-
mask_node = @canvas.node.
|
11
|
-
|
10
|
+
mask_node = @canvas.node.clipping_mask
|
11
|
+
|
12
|
+
@mask = MaskCanvas.new(mask_node)
|
12
13
|
end
|
13
14
|
|
14
15
|
def apply!
|
@@ -31,8 +32,8 @@ class PSD
|
|
31
32
|
alpha = pixel.nil? ? 0 : ChunkyPNG::Color.a(pixel)
|
32
33
|
end
|
33
34
|
|
34
|
-
color = @canvas
|
35
|
-
@canvas
|
35
|
+
color = @canvas.get_pixel(x, y)
|
36
|
+
@canvas.set_pixel x, y, (color & 0xffffff00) | (ChunkyPNG::Color.a(color) * alpha / 255)
|
36
37
|
end
|
37
38
|
end
|
38
39
|
end
|
data/lib/psd/renderer/compose.rb
CHANGED
@@ -5,21 +5,17 @@ class PSD
|
|
5
5
|
# Mostly based on similar code from libpsd.
|
6
6
|
module Compose
|
7
7
|
extend self
|
8
|
-
|
9
|
-
DEFAULT_OPTS = {
|
10
|
-
opacity: 255,
|
11
|
-
fill_opacity: 255
|
12
|
-
}
|
8
|
+
extend ChunkyPNG::Color
|
13
9
|
|
14
10
|
#
|
15
11
|
# Normal blend modes
|
16
12
|
#
|
17
13
|
|
18
|
-
def normal(fg, bg,
|
19
|
-
return apply_opacity(fg,
|
14
|
+
def normal(fg, bg, opacity)
|
15
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
20
16
|
return bg if fully_transparent?(fg)
|
21
17
|
|
22
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
18
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
23
19
|
new_r = blend_channel(r(bg), r(fg), mix_alpha)
|
24
20
|
new_g = blend_channel(g(bg), g(fg), mix_alpha)
|
25
21
|
new_b = blend_channel(b(bg), b(fg), mix_alpha)
|
@@ -32,11 +28,11 @@ class PSD
|
|
32
28
|
# Subtractive blend modes
|
33
29
|
#
|
34
30
|
|
35
|
-
def darken(fg, bg,
|
36
|
-
return apply_opacity(fg,
|
31
|
+
def darken(fg, bg, opacity)
|
32
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
37
33
|
return bg if fully_transparent?(fg)
|
38
34
|
|
39
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
35
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
40
36
|
new_r = r(fg) <= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
|
41
37
|
new_g = g(fg) <= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
|
42
38
|
new_b = b(fg) <= b(bg) ? blend_channel(b(bg), b(fg), mix_alpha) : b(bg)
|
@@ -44,11 +40,11 @@ class PSD
|
|
44
40
|
rgba(new_r, new_g, new_b, dst_alpha)
|
45
41
|
end
|
46
42
|
|
47
|
-
def multiply(fg, bg,
|
48
|
-
return apply_opacity(fg,
|
43
|
+
def multiply(fg, bg, opacity)
|
44
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
49
45
|
return bg if fully_transparent?(fg)
|
50
46
|
|
51
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
47
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
52
48
|
new_r = blend_channel(r(bg), r(fg) * r(bg) >> 8, mix_alpha)
|
53
49
|
new_g = blend_channel(g(bg), g(fg) * g(bg) >> 8, mix_alpha)
|
54
50
|
new_b = blend_channel(b(bg), b(fg) * b(bg) >> 8, mix_alpha)
|
@@ -56,11 +52,11 @@ class PSD
|
|
56
52
|
rgba(new_r, new_g, new_b, dst_alpha)
|
57
53
|
end
|
58
54
|
|
59
|
-
def color_burn(fg, bg,
|
60
|
-
return apply_opacity(fg,
|
55
|
+
def color_burn(fg, bg, opacity)
|
56
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
61
57
|
return bg if fully_transparent?(fg)
|
62
58
|
|
63
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
59
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
64
60
|
|
65
61
|
calculate_foreground = Proc.new do |b, f|
|
66
62
|
if f > 0
|
@@ -78,11 +74,11 @@ class PSD
|
|
78
74
|
rgba(new_r, new_g, new_b, dst_alpha)
|
79
75
|
end
|
80
76
|
|
81
|
-
def linear_burn(fg, bg,
|
82
|
-
return apply_opacity(fg,
|
77
|
+
def linear_burn(fg, bg, opacity)
|
78
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
83
79
|
return bg if fully_transparent?(fg)
|
84
80
|
|
85
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
81
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
86
82
|
|
87
83
|
new_r = blend_channel(r(bg), (r(fg) < (255 - r(bg))) ? 0 : r(fg) - (255 - r(bg)), mix_alpha)
|
88
84
|
new_g = blend_channel(g(bg), (g(fg) < (255 - g(bg))) ? 0 : g(fg) - (255 - g(bg)), mix_alpha)
|
@@ -95,11 +91,11 @@ class PSD
|
|
95
91
|
# Additive blend modes
|
96
92
|
#
|
97
93
|
|
98
|
-
def lighten(fg, bg,
|
99
|
-
return apply_opacity(fg,
|
94
|
+
def lighten(fg, bg, opacity)
|
95
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
100
96
|
return bg if fully_transparent?(fg)
|
101
97
|
|
102
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
98
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
103
99
|
|
104
100
|
new_r = r(fg) >= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
|
105
101
|
new_g = g(fg) >= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
|
@@ -108,11 +104,11 @@ class PSD
|
|
108
104
|
rgba(new_r, new_g, new_b, dst_alpha)
|
109
105
|
end
|
110
106
|
|
111
|
-
def screen(fg, bg,
|
112
|
-
return apply_opacity(fg,
|
107
|
+
def screen(fg, bg, opacity)
|
108
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
113
109
|
return bg if fully_transparent?(fg)
|
114
110
|
|
115
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
111
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
116
112
|
|
117
113
|
new_r = blend_channel(r(bg), 255 - ((255 - r(bg)) * (255 - r(fg)) >> 8), mix_alpha)
|
118
114
|
new_g = blend_channel(g(bg), 255 - ((255 - g(bg)) * (255 - g(fg)) >> 8), mix_alpha)
|
@@ -121,11 +117,11 @@ class PSD
|
|
121
117
|
rgba(new_r, new_g, new_b, dst_alpha)
|
122
118
|
end
|
123
119
|
|
124
|
-
def color_dodge(fg, bg,
|
125
|
-
return apply_opacity(fg,
|
120
|
+
def color_dodge(fg, bg, opacity)
|
121
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
126
122
|
return bg if fully_transparent?(fg)
|
127
123
|
|
128
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
124
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
129
125
|
|
130
126
|
calculate_foreground = Proc.new do |b, f|
|
131
127
|
f < 255 ? [(b << 8) / (255 - f), 255].min : b
|
@@ -138,11 +134,11 @@ class PSD
|
|
138
134
|
rgba(new_r, new_g, new_b, dst_alpha)
|
139
135
|
end
|
140
136
|
|
141
|
-
def linear_dodge(fg, bg,
|
142
|
-
return apply_opacity(fg,
|
137
|
+
def linear_dodge(fg, bg, opacity)
|
138
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
143
139
|
return bg if fully_transparent?(fg)
|
144
140
|
|
145
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
141
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
146
142
|
|
147
143
|
new_r = blend_channel(r(bg), (r(bg) + r(fg)) > 255 ? 255 : r(bg) + r(fg), mix_alpha)
|
148
144
|
new_g = blend_channel(g(bg), (g(bg) + g(fg)) > 255 ? 255 : g(bg) + g(fg), mix_alpha)
|
@@ -156,11 +152,11 @@ class PSD
|
|
156
152
|
# Contrasting blend modes
|
157
153
|
#
|
158
154
|
|
159
|
-
def overlay(fg, bg,
|
160
|
-
return apply_opacity(fg,
|
155
|
+
def overlay(fg, bg, opacity)
|
156
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
161
157
|
return bg if fully_transparent?(fg)
|
162
158
|
|
163
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
159
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
164
160
|
|
165
161
|
calculate_foreground = Proc.new do |b, f|
|
166
162
|
if b < 128
|
@@ -177,11 +173,11 @@ class PSD
|
|
177
173
|
rgba(new_r, new_g, new_b, dst_alpha)
|
178
174
|
end
|
179
175
|
|
180
|
-
def soft_light(fg, bg,
|
181
|
-
return apply_opacity(fg,
|
176
|
+
def soft_light(fg, bg, opacity)
|
177
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
182
178
|
return bg if fully_transparent?(fg)
|
183
179
|
|
184
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
180
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
185
181
|
|
186
182
|
calculate_foreground = Proc.new do |b, f|
|
187
183
|
c1 = b * f >> 8
|
@@ -196,11 +192,11 @@ class PSD
|
|
196
192
|
rgba(new_r, new_g, new_b, dst_alpha)
|
197
193
|
end
|
198
194
|
|
199
|
-
def hard_light(fg, bg,
|
200
|
-
return apply_opacity(fg,
|
195
|
+
def hard_light(fg, bg, opacity)
|
196
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
201
197
|
return bg if fully_transparent?(fg)
|
202
198
|
|
203
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
199
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
204
200
|
|
205
201
|
calculate_foreground = Proc.new do |b, f|
|
206
202
|
if f < 128
|
@@ -217,11 +213,11 @@ class PSD
|
|
217
213
|
rgba(new_r, new_g, new_b, dst_alpha)
|
218
214
|
end
|
219
215
|
|
220
|
-
def vivid_light(fg, bg,
|
221
|
-
return apply_opacity(fg,
|
216
|
+
def vivid_light(fg, bg, opacity)
|
217
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
222
218
|
return bg if fully_transparent?(fg)
|
223
219
|
|
224
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
220
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
225
221
|
|
226
222
|
calculate_foreground = Proc.new do |b, f|
|
227
223
|
if f < 255
|
@@ -238,11 +234,11 @@ class PSD
|
|
238
234
|
rgba(new_r, new_g, new_b, dst_alpha)
|
239
235
|
end
|
240
236
|
|
241
|
-
def linear_light(fg, bg,
|
242
|
-
return apply_opacity(fg,
|
237
|
+
def linear_light(fg, bg, opacity)
|
238
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
243
239
|
return bg if fully_transparent?(fg)
|
244
240
|
|
245
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
241
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
246
242
|
|
247
243
|
calculate_foreground = Proc.new do |b, f|
|
248
244
|
if b < 255
|
@@ -259,11 +255,11 @@ class PSD
|
|
259
255
|
rgba(new_r, new_g, new_b, dst_alpha)
|
260
256
|
end
|
261
257
|
|
262
|
-
def pin_light(fg, bg,
|
263
|
-
return apply_opacity(fg,
|
258
|
+
def pin_light(fg, bg, opacity)
|
259
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
264
260
|
return bg if fully_transparent?(fg)
|
265
261
|
|
266
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
262
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
267
263
|
|
268
264
|
calculate_foreground = Proc.new do |b, f|
|
269
265
|
if f >= 128
|
@@ -280,11 +276,11 @@ class PSD
|
|
280
276
|
rgba(new_r, new_g, new_b, dst_alpha)
|
281
277
|
end
|
282
278
|
|
283
|
-
def hard_mix(fg, bg,
|
284
|
-
return apply_opacity(fg,
|
279
|
+
def hard_mix(fg, bg, opacity)
|
280
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
285
281
|
return bg if fully_transparent?(fg)
|
286
282
|
|
287
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
283
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
288
284
|
|
289
285
|
new_r = blend_channel(r(bg), (r(bg) + r(fg) <= 255) ? 0 : 255, mix_alpha)
|
290
286
|
new_g = blend_channel(g(bg), (g(bg) + g(fg) <= 255) ? 0 : 255, mix_alpha)
|
@@ -297,11 +293,11 @@ class PSD
|
|
297
293
|
# Inversion blend modes
|
298
294
|
#
|
299
295
|
|
300
|
-
def difference(fg, bg,
|
301
|
-
return apply_opacity(fg,
|
296
|
+
def difference(fg, bg, opacity)
|
297
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
302
298
|
return bg if fully_transparent?(fg)
|
303
299
|
|
304
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
300
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
305
301
|
|
306
302
|
new_r = blend_channel(r(bg), (r(bg) - r(fg)).abs, mix_alpha)
|
307
303
|
new_g = blend_channel(g(bg), (g(bg) - g(fg)).abs, mix_alpha)
|
@@ -310,11 +306,11 @@ class PSD
|
|
310
306
|
rgba(new_r, new_g, new_b, dst_alpha)
|
311
307
|
end
|
312
308
|
|
313
|
-
def exclusion(fg, bg,
|
314
|
-
return apply_opacity(fg,
|
309
|
+
def exclusion(fg, bg, opacity)
|
310
|
+
return apply_opacity(fg, opacity) if fully_transparent?(bg)
|
315
311
|
return bg if fully_transparent?(fg)
|
316
312
|
|
317
|
-
mix_alpha, dst_alpha = calculate_alphas(fg, bg,
|
313
|
+
mix_alpha, dst_alpha = calculate_alphas(fg, bg, opacity)
|
318
314
|
|
319
315
|
new_r = blend_channel(r(bg), r(bg) + r(fg) - (r(bg) * r(fg) >> 7), mix_alpha)
|
320
316
|
new_g = blend_channel(g(bg), g(bg) + g(fg) - (g(bg) * g(fg) >> 7), mix_alpha)
|
@@ -325,14 +321,12 @@ class PSD
|
|
325
321
|
|
326
322
|
# If the blend mode is missing, we fall back to normal composition.
|
327
323
|
def method_missing(method, *args, &block)
|
328
|
-
return ChunkyPNG::Color.send(method, *args) if ChunkyPNG::Color.respond_to?(method)
|
329
324
|
normal(*args)
|
330
325
|
end
|
331
326
|
|
332
327
|
private
|
333
328
|
|
334
|
-
def calculate_alphas(fg, bg,
|
335
|
-
opacity = calculate_opacity(opts)
|
329
|
+
def calculate_alphas(fg, bg, opacity)
|
336
330
|
src_alpha = a(fg) * opacity >> 8
|
337
331
|
dst_alpha = a(bg)
|
338
332
|
|
@@ -342,12 +336,8 @@ class PSD
|
|
342
336
|
return mix_alpha, dst_alpha
|
343
337
|
end
|
344
338
|
|
345
|
-
def
|
346
|
-
|
347
|
-
end
|
348
|
-
|
349
|
-
def apply_opacity(color, opts)
|
350
|
-
(color & 0xffffff00) | ((color & 0x000000ff) * calculate_opacity(opts) / 255)
|
339
|
+
def apply_opacity(color, opacity)
|
340
|
+
(color & 0xffffff00) | ((color & 0x000000ff) * opacity / 255)
|
351
341
|
end
|
352
342
|
|
353
343
|
def blend_channel(bg, fg, alpha)
|
@@ -358,4 +348,4 @@ class PSD
|
|
358
348
|
bg + ((255 - bg) * fg >> 8)
|
359
349
|
end
|
360
350
|
end
|
361
|
-
end
|
351
|
+
end
|