psd 2.1.2 → 3.1.2
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 +1 -1
- data/lib/psd.rb +8 -6
- data/lib/psd/blend_mode.rb +46 -38
- data/lib/psd/channel_image.rb +9 -5
- data/lib/psd/descriptor.rb +39 -16
- data/lib/psd/header.rb +33 -32
- data/lib/psd/image_formats/rle.rb +4 -10
- data/lib/psd/image_modes/rgb.rb +4 -4
- data/lib/psd/layer.rb +1 -15
- data/lib/psd/layer/blend_modes.rb +12 -12
- data/lib/psd/layer/helpers.rb +8 -10
- data/lib/psd/layer/info.rb +9 -7
- data/lib/psd/layer/position_and_channels.rb +0 -4
- data/lib/psd/layer_info.rb +0 -4
- data/lib/psd/layer_info/blend_clipping_elements.rb +4 -2
- data/lib/psd/layer_info/blend_interior_elements.rb +4 -2
- data/lib/psd/layer_info/fill_opacity.rb +4 -2
- data/lib/psd/layer_info/layer_group.rb +4 -2
- data/lib/psd/layer_info/layer_id.rb +4 -2
- data/lib/psd/layer_info/layer_name_source.rb +4 -2
- data/lib/psd/layer_info/layer_section_divider.rb +4 -2
- data/lib/psd/layer_info/legacy_typetool.rb +5 -3
- data/lib/psd/layer_info/locked.rb +4 -2
- data/lib/psd/layer_info/metadata_setting.rb +4 -2
- data/lib/psd/layer_info/object_effects.rb +4 -2
- data/lib/psd/layer_info/pattern.rb +14 -0
- data/lib/psd/layer_info/placed_layer.rb +4 -2
- data/lib/psd/layer_info/reference_point.rb +4 -2
- data/lib/psd/layer_info/sheet_color.rb +18 -0
- data/lib/psd/layer_info/solid_color.rb +36 -0
- data/lib/psd/layer_info/typetool.rb +4 -2
- data/lib/psd/layer_info/unicode_name.rb +4 -2
- data/lib/psd/layer_info/vector_mask.rb +4 -2
- data/lib/psd/layer_info/vector_origination.rb +14 -0
- data/lib/psd/layer_info/vector_stroke.rb +4 -2
- data/lib/psd/layer_info/vector_stroke_content.rb +4 -2
- data/lib/psd/layer_mask.rb +2 -8
- data/lib/psd/lazy_execute.rb +5 -1
- data/lib/psd/node.rb +112 -48
- data/lib/psd/nodes/ancestry.rb +80 -75
- data/lib/psd/nodes/build_preview.rb +4 -4
- data/lib/psd/nodes/group.rb +35 -0
- data/lib/psd/nodes/layer.rb +40 -0
- data/lib/psd/nodes/root.rb +90 -0
- data/lib/psd/nodes/search.rb +19 -19
- data/lib/psd/path_record.rb +1 -71
- data/lib/psd/renderer.rb +6 -5
- data/lib/psd/renderer/blender.rb +10 -5
- data/lib/psd/renderer/cairo_helpers.rb +46 -0
- data/lib/psd/renderer/canvas.rb +39 -19
- data/lib/psd/renderer/canvas_management.rb +2 -2
- data/lib/psd/renderer/clipping_mask.rb +5 -4
- data/lib/psd/renderer/compose.rb +61 -68
- data/lib/psd/renderer/layer_styles.rb +15 -5
- data/lib/psd/renderer/layer_styles/color_overlay.rb +46 -27
- 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/resource_section.rb +4 -7
- data/lib/psd/resources.rb +4 -19
- data/lib/psd/resources/base.rb +27 -0
- data/lib/psd/resources/guides.rb +6 -4
- data/lib/psd/resources/layer_comps.rb +6 -4
- data/lib/psd/resources/slices.rb +7 -5
- data/lib/psd/version.rb +1 -1
- data/psd.gemspec +1 -2
- data/spec/files/blendmodes.psd +0 -0
- data/spec/hierarchy_spec.rb +5 -0
- metadata +27 -26
- 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
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
require_relative 'layer_styles/drop_shadow'
|
1
|
+
require 'psd/renderer/layer_styles/color_overlay'
|
3
2
|
|
4
3
|
class PSD
|
5
4
|
class LayerStyles
|
@@ -31,12 +30,16 @@ class PSD
|
|
31
30
|
'Lmns' => 'lum'
|
32
31
|
}.freeze
|
33
32
|
|
33
|
+
SUPPORTED_STYLES = [
|
34
|
+
ColorOverlay
|
35
|
+
].freeze
|
36
|
+
|
34
37
|
attr_reader :canvas, :node, :data
|
35
38
|
|
36
39
|
def initialize(canvas)
|
37
40
|
@canvas = canvas
|
38
41
|
@node = @canvas.node
|
39
|
-
@data = @node.
|
42
|
+
@data = @node.object_effects
|
40
43
|
|
41
44
|
if @data.nil?
|
42
45
|
@applied = true
|
@@ -48,9 +51,16 @@ class PSD
|
|
48
51
|
|
49
52
|
def apply!
|
50
53
|
return if @applied || data.nil?
|
54
|
+
return unless styles_enabled?
|
55
|
+
|
56
|
+
SUPPORTED_STYLES.each do |style|
|
57
|
+
next unless style.should_apply?(@canvas, data)
|
58
|
+
style.new(self).apply!
|
59
|
+
end
|
60
|
+
end
|
51
61
|
|
52
|
-
|
53
|
-
|
62
|
+
def styles_enabled?
|
63
|
+
data['masterFXSwitch']
|
54
64
|
end
|
55
65
|
end
|
56
66
|
end
|
@@ -1,8 +1,27 @@
|
|
1
1
|
class PSD
|
2
2
|
class LayerStyles
|
3
3
|
class ColorOverlay
|
4
|
-
|
5
|
-
|
4
|
+
# TODO: CMYK support
|
5
|
+
def self.should_apply?(canvas, data)
|
6
|
+
data.has_key?('SoFi') &&
|
7
|
+
data['SoFi']['enab'] &&
|
8
|
+
canvas.node.header.rgb? &&
|
9
|
+
!PSD::Renderer::VectorShape.can_render?(canvas)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.can_apply?(canvas, data)
|
13
|
+
data.has_key?('SoFi') &&
|
14
|
+
data['SoFi']['enab'] &&
|
15
|
+
canvas.node.header.rgb?
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.for_canvas(canvas)
|
19
|
+
data = canvas.node.object_effects
|
20
|
+
return nil if data.nil?
|
21
|
+
return nil unless can_apply?(canvas, data.data)
|
22
|
+
|
23
|
+
styles = LayerStyles.new(canvas)
|
24
|
+
self.new(styles)
|
6
25
|
end
|
7
26
|
|
8
27
|
def initialize(styles)
|
@@ -12,41 +31,22 @@ class PSD
|
|
12
31
|
end
|
13
32
|
|
14
33
|
def apply!
|
15
|
-
# TODO - implement CMYK color overlay
|
16
|
-
return if @node.header.cmyk?
|
17
|
-
|
18
|
-
width = @canvas.width
|
19
|
-
height = @canvas.height
|
20
|
-
|
21
|
-
# puts width, height
|
22
|
-
# puts @canvas.canvas.width, @canvas.canvas.height
|
23
|
-
|
24
34
|
PSD.logger.debug "Layer style: layer = #{@node.name}, type = color overlay, blend mode = #{blending_mode}"
|
25
35
|
|
26
|
-
height.times do |y|
|
27
|
-
width.times do |x|
|
36
|
+
@canvas.height.times do |y|
|
37
|
+
@canvas.width.times do |x|
|
28
38
|
pixel = @canvas[x, y]
|
29
39
|
alpha = ChunkyPNG::Color.a(pixel)
|
30
40
|
next if alpha == 0
|
31
41
|
|
32
|
-
|
33
|
-
@canvas[x, y] =
|
42
|
+
new_pixel = Compose.send(blending_mode, overlay_color, pixel, overlay_opacity)
|
43
|
+
@canvas[x, y] = (new_pixel & 0xFFFFFF00) | alpha
|
34
44
|
end
|
35
45
|
end
|
36
46
|
end
|
37
47
|
|
38
|
-
|
39
|
-
|
40
|
-
def blending_mode
|
41
|
-
@blending_mode ||= BlendMode::BLEND_MODES[BLEND_TRANSLATION[overlay_data['Md ']].to_sym]
|
42
|
-
end
|
43
|
-
|
44
|
-
def overlay_data
|
45
|
-
@data['SoFi']
|
46
|
-
end
|
47
|
-
|
48
|
-
def color_data
|
49
|
-
overlay_data['Clr ']
|
48
|
+
def overlay_color
|
49
|
+
@overlay_color ||= ChunkyPNG::Color.rgb(r, g, b)
|
50
50
|
end
|
51
51
|
|
52
52
|
def r
|
@@ -60,6 +60,25 @@ class PSD
|
|
60
60
|
def b
|
61
61
|
@b ||= color_data['Bl '].round
|
62
62
|
end
|
63
|
+
|
64
|
+
def a
|
65
|
+
@a ||= (overlay_data['Opct'][:value] * 2.55).ceil
|
66
|
+
end
|
67
|
+
alias_method :overlay_opacity, :a
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def blending_mode
|
72
|
+
@blending_mode ||= BlendMode::BLEND_MODES[BLEND_TRANSLATION[overlay_data['Md '][:value]].to_sym]
|
73
|
+
end
|
74
|
+
|
75
|
+
def overlay_data
|
76
|
+
@data['SoFi']
|
77
|
+
end
|
78
|
+
|
79
|
+
def color_data
|
80
|
+
overlay_data['Clr ']
|
81
|
+
end
|
63
82
|
end
|
64
83
|
end
|
65
84
|
end
|
data/lib/psd/renderer/mask.rb
CHANGED
@@ -3,11 +3,18 @@ class PSD
|
|
3
3
|
class Mask
|
4
4
|
attr_accessor :mask_data
|
5
5
|
|
6
|
-
def initialize(canvas)
|
6
|
+
def initialize(canvas, mask_layer = nil)
|
7
7
|
@canvas = canvas
|
8
8
|
@layer = canvas.node
|
9
|
+
@mask_layer = mask_layer || @layer
|
9
10
|
|
10
|
-
@mask_data = @
|
11
|
+
@mask_data = @mask_layer.image.mask_data
|
12
|
+
@mask = @mask_layer.mask
|
13
|
+
|
14
|
+
@mask_width = @mask.width.to_i
|
15
|
+
@mask_height = @mask.height.to_i
|
16
|
+
@mask_left = @mask.left.to_i + @mask_layer.left_offset
|
17
|
+
@mask_top = @mask.top.to_i + @mask_layer.top_offset
|
11
18
|
|
12
19
|
@doc_width = @layer.header.width.to_i
|
13
20
|
@doc_height = @layer.header.height.to_i
|
@@ -15,29 +22,26 @@ class PSD
|
|
15
22
|
|
16
23
|
def apply!
|
17
24
|
PSD.logger.debug "Applying mask to #{@layer.name}"
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
# We're off the document canvas. Crop.
|
33
|
-
if doc_x < 0 || doc_x > @doc_width || doc_y < 0 || doc_y > @doc_height
|
25
|
+
|
26
|
+
@canvas.height.times do |y|
|
27
|
+
@canvas.width.times do |x|
|
28
|
+
doc_x = @canvas.left + x
|
29
|
+
doc_y = @canvas.top + y
|
30
|
+
|
31
|
+
mask_x = doc_x - @mask_left
|
32
|
+
mask_y = doc_y - @mask_top
|
33
|
+
|
34
|
+
color = ChunkyPNG::Color.to_truecolor_alpha_bytes(@canvas.get_pixel(x, y))
|
35
|
+
|
36
|
+
if doc_x < 0 || doc_x >= @doc_width || doc_y < 0 || doc_y >= @doc_height
|
37
|
+
color[3] = 0
|
38
|
+
elsif mask_x < 0 || mask_x >= @mask_width || mask_y < 0 || mask_y >= @mask_height
|
34
39
|
color[3] = 0
|
35
40
|
else
|
36
|
-
color[3] = color[3] * @mask_data[
|
37
|
-
end
|
41
|
+
color[3] = color[3] * @mask_data[@mask_width * mask_y + mask_x] / 255
|
42
|
+
end
|
38
43
|
|
39
|
-
@canvas
|
40
|
-
i += 1
|
44
|
+
@canvas.set_pixel x, y, ChunkyPNG::Color.rgba(*color)
|
41
45
|
end
|
42
46
|
end
|
43
47
|
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'psd/renderer/cairo_helpers'
|
2
|
+
|
3
|
+
class PSD
|
4
|
+
class Renderer
|
5
|
+
class VectorShape
|
6
|
+
include CairoHelpers
|
7
|
+
|
8
|
+
def self.can_render?(canvas)
|
9
|
+
canvas.opts[:render_vectors] && !canvas.node.vector_mask.nil?
|
10
|
+
end
|
11
|
+
|
12
|
+
DPI = 72.0.freeze
|
13
|
+
|
14
|
+
def initialize(canvas)
|
15
|
+
@canvas = canvas
|
16
|
+
@node = @canvas.node
|
17
|
+
@path = @node.vector_mask.paths.map(&:to_hash)
|
18
|
+
|
19
|
+
@stroke_data = @node.vector_stroke ? @node.vector_stroke.data : {}
|
20
|
+
@fill_data = @node.vector_stroke_content ? @node.vector_stroke_content.data : {}
|
21
|
+
|
22
|
+
@paths = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def render!
|
26
|
+
PSD.logger.debug "Beginning vector render for #{@node.name}"
|
27
|
+
|
28
|
+
find_points
|
29
|
+
render_shapes
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def find_points
|
35
|
+
PSD.logger.debug "Formatting vector points..."
|
36
|
+
|
37
|
+
cur_path = nil
|
38
|
+
@path.each do |data|
|
39
|
+
next if [6, 7, 8].include? data[:record_type]
|
40
|
+
|
41
|
+
if [0, 3].include? data[:record_type]
|
42
|
+
@paths << cur_path
|
43
|
+
cur_path = []
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
47
|
+
cur_path << data.tap do |d|
|
48
|
+
if [1, 2, 4, 5].include? data[:record_type]
|
49
|
+
[:preceding, :anchor, :leaving].each do |type|
|
50
|
+
d[type][:horiz] = (d[type][:horiz] * horiz_factor) - @node.left
|
51
|
+
d[type][:vert] = (d[type][:vert] * vert_factor) - @node.top
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@paths << cur_path
|
58
|
+
@paths.compact!
|
59
|
+
|
60
|
+
PSD.logger.debug "Vector shape has #{@paths.size} path(s)"
|
61
|
+
end
|
62
|
+
|
63
|
+
# TODO: stroke alignment
|
64
|
+
# Right now we assume the stroke style is always a overlap stroke.
|
65
|
+
def render_shapes
|
66
|
+
PSD.logger.debug "Rendering #{@paths.size} vector paths with cairo"
|
67
|
+
|
68
|
+
cairo_image_surface(@canvas.width + stroke_size, @canvas.height + stroke_size) do |cr|
|
69
|
+
cr.set_fill_rule Cairo::FILL_RULE_EVEN_ODD
|
70
|
+
cr.set_line_join stroke_join
|
71
|
+
cr.set_line_cap stroke_cap
|
72
|
+
|
73
|
+
cr.translate stroke_size / 2.0, stroke_size / 2.0
|
74
|
+
|
75
|
+
@paths.each do |path|
|
76
|
+
cr.move_to path[0][:anchor][:horiz], path[0][:anchor][:vert]
|
77
|
+
|
78
|
+
path.size.times do |i|
|
79
|
+
point_a = path[i]
|
80
|
+
point_b = path[i+1] || path[0]
|
81
|
+
|
82
|
+
cr.curve_to(
|
83
|
+
point_a[:leaving][:horiz],
|
84
|
+
point_a[:leaving][:vert],
|
85
|
+
point_b[:preceding][:horiz],
|
86
|
+
point_b[:preceding][:vert],
|
87
|
+
point_b[:anchor][:horiz],
|
88
|
+
point_b[:anchor][:vert]
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
cr.close_path if path.last[:closed]
|
93
|
+
end
|
94
|
+
|
95
|
+
cr.set_source_rgba fill_color
|
96
|
+
cr.fill_preserve
|
97
|
+
|
98
|
+
if has_stroke?
|
99
|
+
cr.set_source_rgba stroke_color
|
100
|
+
cr.set_line_width stroke_size
|
101
|
+
cr.stroke
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# For debugging purposes only
|
107
|
+
def draw_debug(canvas)
|
108
|
+
@paths.each do |path|
|
109
|
+
path.each do |point|
|
110
|
+
canvas.circle(point[:anchor][:horiz].to_i, point[:anchor][:vert].to_i, 3, ChunkyPNG::Color::BLACK, ChunkyPNG::Color::BLACK)
|
111
|
+
[:leaving, :preceding].each do |type|
|
112
|
+
canvas.circle(point[type][:horiz].to_i, point[type][:vert].to_i, 3, ChunkyPNG::Color.rgb(255, 0, 0), ChunkyPNG::Color.rgb(255, 0, 0))
|
113
|
+
canvas.line(
|
114
|
+
point[:anchor][:horiz].to_i, point[:anchor][:vert].to_i,
|
115
|
+
point[type][:horiz].to_i, point[type][:vert].to_i,
|
116
|
+
ChunkyPNG::Color::BLACK
|
117
|
+
)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def apply_to_canvas(output)
|
124
|
+
# draw_debug(output)
|
125
|
+
output.resample_nearest_neighbor!(@canvas.width, @canvas.height)
|
126
|
+
@canvas.canvas.compose!(output, 0, 0)
|
127
|
+
end
|
128
|
+
|
129
|
+
def formatted_points
|
130
|
+
@formatted_points ||= @curve_points.map(&:to_a)
|
131
|
+
end
|
132
|
+
|
133
|
+
def horiz_factor
|
134
|
+
@horiz_factor ||= @node.root.width.to_f
|
135
|
+
end
|
136
|
+
|
137
|
+
def vert_factor
|
138
|
+
@vert_factor ||= @node.root.height.to_f
|
139
|
+
end
|
140
|
+
|
141
|
+
def stroke_color
|
142
|
+
@stroke_color ||= (
|
143
|
+
if @stroke_data['strokeEnabled']
|
144
|
+
colors = @stroke_data['strokeStyleContent']['Clr ']
|
145
|
+
[
|
146
|
+
colors['Rd '] / 255.0,
|
147
|
+
colors['Grn '] / 255.0,
|
148
|
+
colors['Bl '] / 255.0,
|
149
|
+
@stroke_data['strokeStyleOpacity'][:value] / 100.0
|
150
|
+
]
|
151
|
+
else
|
152
|
+
[0.0, 0.0, 0.0, 0.0]
|
153
|
+
end
|
154
|
+
)
|
155
|
+
end
|
156
|
+
|
157
|
+
def fill_color
|
158
|
+
@fill_color ||= (
|
159
|
+
overlay = PSD::LayerStyles::ColorOverlay.for_canvas(@canvas)
|
160
|
+
|
161
|
+
if overlay
|
162
|
+
[
|
163
|
+
overlay.r / 255.0,
|
164
|
+
overlay.g / 255.0,
|
165
|
+
overlay.b / 255.0,
|
166
|
+
overlay.a / 255.0
|
167
|
+
]
|
168
|
+
elsif @stroke_data['fillEnabled']
|
169
|
+
colors = @fill_data['Clr ']
|
170
|
+
[
|
171
|
+
colors['Rd '] / 255.0,
|
172
|
+
colors['Grn '] / 255.0,
|
173
|
+
colors['Bl '] / 255.0,
|
174
|
+
@stroke_data['strokeStyleOpacity'][:value] / 100.0
|
175
|
+
]
|
176
|
+
elsif !@node.solid_color.nil?
|
177
|
+
[
|
178
|
+
@node.solid_color.r / 255.0,
|
179
|
+
@node.solid_color.g / 255.0,
|
180
|
+
@node.solid_color.b / 255.0,
|
181
|
+
1.0
|
182
|
+
]
|
183
|
+
else
|
184
|
+
[0.0, 0.0, 0.0, 0.0]
|
185
|
+
end
|
186
|
+
)
|
187
|
+
end
|
188
|
+
|
189
|
+
def stroke_size
|
190
|
+
@stroke_size ||= (
|
191
|
+
if @stroke_data['strokeStyleLineWidth']
|
192
|
+
value = @stroke_data['strokeStyleLineWidth'][:value]
|
193
|
+
|
194
|
+
# Convert to pixels
|
195
|
+
if @stroke_data['strokeStyleLineWidth'][:id] == '#Pnt'
|
196
|
+
value = @stroke_data['strokeStyleResolution'] * value / 72.27
|
197
|
+
end
|
198
|
+
|
199
|
+
value.to_i
|
200
|
+
else
|
201
|
+
0
|
202
|
+
end
|
203
|
+
)
|
204
|
+
end
|
205
|
+
|
206
|
+
def stroke_cap
|
207
|
+
@stroke_cap ||= (
|
208
|
+
if @stroke_data['strokeStyleLineCapType']
|
209
|
+
case @stroke_data['strokeStyleLineCapType']
|
210
|
+
when 'strokeStyleButtCap' then Cairo::LINE_CAP_BUTT
|
211
|
+
when 'strokeStyleRoundCap' then Cairo::LINE_CAP_ROUND
|
212
|
+
when 'strokeStyleSquareCap' then Cairo::LINE_CAP_SQUARE
|
213
|
+
end
|
214
|
+
else
|
215
|
+
Cairo::LINE_CAP_BUTT
|
216
|
+
end
|
217
|
+
)
|
218
|
+
end
|
219
|
+
|
220
|
+
def stroke_join
|
221
|
+
@stroke_join ||= (
|
222
|
+
if @stroke_data['strokeStyleLineJoinType']
|
223
|
+
case @stroke_data['strokeStyleLineJoinType']
|
224
|
+
when 'strokeStyleMiterJoin' then Cairo::LINE_JOIN_MITER
|
225
|
+
when 'strokeStyleRoundJoin' then Cairo::LINE_JOIN_ROUND
|
226
|
+
when 'strokeStyleBevelJoin' then Cairo::LINE_JOIN_BEVEL
|
227
|
+
end
|
228
|
+
else
|
229
|
+
Cairo::LINE_JOIN_MITER
|
230
|
+
end
|
231
|
+
)
|
232
|
+
end
|
233
|
+
|
234
|
+
def has_stroke?
|
235
|
+
stroke_size > 0
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|