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