prawn-svg 0.36.2 → 0.38.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/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/lib/prawn/svg/attributes/transform.rb +2 -0
- data/lib/prawn/svg/css/stylesheets.rb +0 -7
- data/lib/prawn/svg/elements/base.rb +28 -9
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/polygon.rb +2 -2
- data/lib/prawn/svg/elements/text.rb +54 -52
- data/lib/prawn/svg/elements/text_component.rb +176 -198
- data/lib/prawn/svg/elements/text_node.rb +174 -0
- data/lib/prawn/svg/elements.rb +1 -1
- data/lib/prawn/svg/font.rb +13 -57
- data/lib/prawn/svg/font_metrics.rb +97 -0
- data/lib/prawn/svg/font_registry.rb +119 -55
- data/lib/prawn/svg/properties.rb +9 -4
- data/lib/prawn/svg/renderer.rb +20 -64
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +1 -0
- data/scripts/compare_samples +25 -0
- data/spec/prawn/svg/attributes/transform_spec.rb +4 -0
- data/spec/prawn/svg/css/stylesheets_spec.rb +0 -2
- data/spec/prawn/svg/elements/base_spec.rb +2 -2
- data/spec/prawn/svg/elements/text_spec.rb +210 -130
- data/spec/prawn/svg/font_registry_spec.rb +121 -10
- data/spec/prawn/svg/font_spec.rb +30 -8
- data/spec/sample_svg/bytes.svg +121 -0
- data/spec/sample_svg/important.svg +14 -0
- data/spec/sample_svg/positioning.svg +26 -0
- data/spec/sample_svg/presentation_attribute_precedence.svg +12 -0
- data/spec/sample_svg/subfamilies.svg +5 -1
- data/spec/sample_svg/text_use.svg +37 -0
- metadata +11 -3
- data/lib/prawn/svg/elements/depth_first_base.rb +0 -52
@@ -1,250 +1,228 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
module Prawn::SVG
|
2
|
+
class Elements::TextComponent < Elements::DirectRenderBase
|
3
|
+
attr_reader :children, :parent_component
|
4
|
+
attr_reader :x_values, :y_values, :dx, :dy, :rotation, :text_length, :length_adjust
|
5
|
+
attr_reader :font
|
6
|
+
|
7
|
+
def initialize(document, source, _calls, state, parent_component = nil)
|
8
|
+
if parent_component.nil? && source.name != 'text'
|
9
|
+
raise SkipElementError, 'attempted to <use> a component inside a text element, this is not supported'
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
+
super(document, source, [], state)
|
13
|
+
@parent_component = parent_component
|
12
14
|
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
state.text.dx = (attributes['dx'] || '').split(COMMA_WSP_REGEXP).collect { |n| x_pixels(n) }
|
17
|
-
state.text.dy = (attributes['dy'] || '').split(COMMA_WSP_REGEXP).collect { |n| y_pixels(n) }
|
18
|
-
state.text.rotation = (attributes['rotate'] || '').split(COMMA_WSP_REGEXP).collect(&:to_f)
|
19
|
-
state.text.text_length = normalize_length(attributes['textLength'])
|
20
|
-
state.text.length_adjust = attributes['lengthAdjust']
|
21
|
-
state.text.spacing = calculate_character_spacing
|
22
|
-
state.text.mode = calculate_text_rendering_mode
|
16
|
+
def parse
|
17
|
+
raise SkipElementError, '<text> elements are not supported in clip paths' if state.inside_clip_path
|
23
18
|
|
24
|
-
|
19
|
+
@x_values = parse_wsp('x').map { |n| x(n) }
|
20
|
+
@y_values = parse_wsp('y').map { |n| y(n) }
|
21
|
+
@dx = parse_wsp('dx').map { |n| x_pixels(n) }
|
22
|
+
@dy = parse_wsp('dy').map { |n| y_pixels(n) }
|
23
|
+
@rotation = parse_wsp('rotate').map(&:to_f)
|
24
|
+
@text_length = normalize_length(attributes['textLength'])
|
25
|
+
@length_adjust = attributes['lengthAdjust']
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
when 'tspan', 'tref'
|
32
|
-
append_child(child)
|
27
|
+
@font = select_font
|
28
|
+
|
29
|
+
@children = svg_text_children.flat_map do |child|
|
30
|
+
if child.node_type == :text
|
31
|
+
build_text_node(child)
|
33
32
|
else
|
34
|
-
|
33
|
+
case child.name
|
34
|
+
when 'tspan', 'tref'
|
35
|
+
build_child(child)
|
36
|
+
else
|
37
|
+
warnings << "Unknown tag '#{child.name}' inside text tag; ignoring"
|
38
|
+
[]
|
39
|
+
end
|
35
40
|
end
|
36
41
|
end
|
37
42
|
end
|
38
|
-
end
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
# text_anchor and dominant_baseline aren't Prawn options; we have to do some math to support them
|
47
|
-
# and so we handle them in Prawn::SVG::Interface#rewrite_call_arguments
|
48
|
-
opts = {
|
49
|
-
size: computed_properties.numeric_font_size,
|
50
|
-
style: font&.subfamily,
|
51
|
-
text_anchor: computed_properties.text_anchor
|
52
|
-
}
|
53
|
-
|
54
|
-
unless computed_properties.dominant_baseline == 'auto'
|
55
|
-
opts[:dominant_baseline] =
|
56
|
-
computed_properties.dominant_baseline
|
57
|
-
end
|
58
|
-
opts[:decoration] = computed_properties.text_decoration unless computed_properties.text_decoration == 'none'
|
59
|
-
|
60
|
-
if state.text.parent
|
61
|
-
add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing == state.text.parent.spacing
|
62
|
-
add_call_and_enter 'text_rendering_mode', state.text.mode unless state.text.mode == state.text.parent.mode
|
63
|
-
else
|
64
|
-
add_call_and_enter 'character_spacing', state.text.spacing unless state.text.spacing.zero?
|
65
|
-
add_call_and_enter 'text_rendering_mode', state.text.mode unless state.text.mode == :fill
|
66
|
-
end
|
67
|
-
|
68
|
-
@commands.each do |command|
|
69
|
-
case command
|
70
|
-
when Printable
|
71
|
-
apply_text(command.text, opts)
|
72
|
-
when self.class
|
73
|
-
add_call 'save'
|
74
|
-
command.apply_step(calls)
|
75
|
-
add_call 'restore'
|
76
|
-
else
|
77
|
-
raise
|
44
|
+
def lay_out(prawn)
|
45
|
+
@children.each do |child|
|
46
|
+
prawn.save_font do
|
47
|
+
prawn.font(font.name, style: font.subfamily) if font
|
48
|
+
child.lay_out(prawn)
|
49
|
+
end
|
78
50
|
end
|
79
|
-
end
|
80
|
-
|
81
|
-
# It's possible there was no text to render. In that case, add a 'noop' so character_spacing/text_rendering_mode
|
82
|
-
# don't blow up when they find they don't have a block to execute.
|
83
|
-
add_call 'noop' if calls.empty?
|
84
|
-
end
|
85
51
|
|
86
|
-
|
52
|
+
if @text_length
|
53
|
+
flexible_width, fixed_width = total_flexible_and_fixed_width
|
87
54
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
trailing = text[-1] == ' '
|
95
|
-
text = text.strip.gsub(/ {2,}/, ' ')
|
55
|
+
if flexible_width.positive?
|
56
|
+
target_width = [@text_length - fixed_width, 0].max
|
57
|
+
factor = target_width / flexible_width
|
58
|
+
apply_factor_to_base_width(factor)
|
59
|
+
end
|
60
|
+
end
|
96
61
|
end
|
97
62
|
|
98
|
-
|
99
|
-
|
63
|
+
def render_component(prawn, renderer, cursor, translate_x = nil)
|
64
|
+
raise SkipElementQuietly if computed_properties.display == 'none'
|
100
65
|
|
101
|
-
|
102
|
-
|
103
|
-
new_state.text = TextState.new(state.text)
|
66
|
+
add_yield_call do
|
67
|
+
prawn.translate(translate_x, 0) if translate_x
|
104
68
|
|
105
|
-
|
106
|
-
@commands << element
|
107
|
-
element.parse_step
|
108
|
-
end
|
69
|
+
size = computed_properties.numeric_font_size
|
109
70
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
remaining = rotation_remaining = false
|
114
|
-
|
115
|
-
list = state.text
|
116
|
-
while list
|
117
|
-
shifted = list.x.shift
|
118
|
-
x ||= shifted
|
119
|
-
shifted = list.y.shift
|
120
|
-
y ||= shifted
|
121
|
-
shifted = list.dx.shift
|
122
|
-
dx ||= shifted
|
123
|
-
shifted = list.dy.shift
|
124
|
-
dy ||= shifted
|
125
|
-
|
126
|
-
shifted = list.rotation.length > 1 ? list.rotation.shift : list.rotation.first
|
127
|
-
if shifted && rotate.nil?
|
128
|
-
rotate = shifted
|
129
|
-
remaining ||= list.rotation != [0]
|
71
|
+
if computed_properties.dominant_baseline == 'middle'
|
72
|
+
height = FontMetrics.x_height_in_points(prawn, size || prawn.font_size)
|
73
|
+
y_offset = -height / 2.0
|
130
74
|
end
|
131
75
|
|
132
|
-
|
133
|
-
|
134
|
-
|
76
|
+
prawn.save_font do
|
77
|
+
prawn.font(font.name, style: font.subfamily) if font
|
78
|
+
|
79
|
+
children.each do |child|
|
80
|
+
case child
|
81
|
+
when Elements::TextNode
|
82
|
+
child.render(prawn, size, cursor, y_offset)
|
83
|
+
when self.class
|
84
|
+
prawn.save_graphics_state
|
85
|
+
child.render_component(prawn, renderer, cursor)
|
86
|
+
prawn.restore_graphics_state
|
87
|
+
else
|
88
|
+
raise
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
135
92
|
end
|
136
93
|
|
137
|
-
|
138
|
-
|
94
|
+
renderer.render_calls(prawn, base_calls)
|
95
|
+
end
|
96
|
+
|
97
|
+
def calculated_width
|
98
|
+
children.reduce(0) { |total, child| total + child.calculated_width }
|
99
|
+
end
|
139
100
|
|
140
|
-
|
141
|
-
|
101
|
+
def current_length_adjust_is_scaling?
|
102
|
+
if @text_length
|
103
|
+
@length_adjust == 'spacingAndGlyphs'
|
104
|
+
elsif parent_component
|
105
|
+
parent_component.current_length_adjust_is_scaling?
|
142
106
|
else
|
143
|
-
|
107
|
+
false
|
144
108
|
end
|
109
|
+
end
|
145
110
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
end
|
111
|
+
def letter_spacing_pixels
|
112
|
+
if computed_properties.letter_spacing == 'normal'
|
113
|
+
nil
|
114
|
+
else
|
115
|
+
x_pixels(computed_properties.letter_spacing)
|
152
116
|
end
|
117
|
+
end
|
153
118
|
|
154
|
-
|
155
|
-
add_call 'draw_text', text[0..0], **opts.dup
|
156
|
-
text = text[1..]
|
157
|
-
else
|
158
|
-
add_call 'draw_text', text, **opts.dup
|
159
|
-
|
160
|
-
# we can get to this path with rotations still pending
|
161
|
-
# solve this by shifting them out by the number of
|
162
|
-
# characters we've just drawn
|
163
|
-
shift = text.length - 1
|
164
|
-
if rotation_remaining && shift.positive?
|
165
|
-
list = state.text
|
166
|
-
while list
|
167
|
-
count = [shift, list.rotation.length - 1].min
|
168
|
-
list.rotation.shift(count) if count.positive?
|
169
|
-
list = list.parent
|
170
|
-
end
|
171
|
-
end
|
119
|
+
protected
|
172
120
|
|
173
|
-
|
121
|
+
def build_text_node(child)
|
122
|
+
if state.preserve_space
|
123
|
+
text = child.value.tr("\n\t", ' ')
|
124
|
+
else
|
125
|
+
text = child.value.tr("\n", '').tr("\t", ' ')
|
126
|
+
leading = text[0] == ' '
|
127
|
+
trailing = text[-1] == ' '
|
128
|
+
text = text.strip.gsub(/ {2,}/, ' ')
|
174
129
|
end
|
130
|
+
|
131
|
+
Elements::TextNode.new(self, text, leading, trailing)
|
132
|
+
end
|
133
|
+
|
134
|
+
def build_child(child)
|
135
|
+
component = self.class.new(document, child, [], state.dup, self)
|
136
|
+
component.process
|
137
|
+
component
|
175
138
|
end
|
176
|
-
end
|
177
139
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
140
|
+
def svg_text_children
|
141
|
+
text_children.select do |child|
|
142
|
+
child.node_type == :text || (
|
143
|
+
child.node_type == :element &&
|
144
|
+
[SVG_NAMESPACE, ''].include?(child.namespace)
|
145
|
+
|
183
146
|
)
|
184
|
-
|
147
|
+
end
|
185
148
|
end
|
186
|
-
end
|
187
149
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
150
|
+
def text_children
|
151
|
+
if name == 'tref'
|
152
|
+
reference = find_referenced_element
|
153
|
+
reference ? reference.source.children : []
|
154
|
+
else
|
155
|
+
source.children
|
156
|
+
end
|
194
157
|
end
|
195
|
-
end
|
196
158
|
|
197
|
-
|
198
|
-
|
159
|
+
def find_referenced_element
|
160
|
+
href = href_attribute
|
199
161
|
|
200
|
-
|
201
|
-
|
202
|
-
|
162
|
+
if href && href[0..0] == '#'
|
163
|
+
element = document.elements_by_id[href[1..]]
|
164
|
+
element if element.name == 'text'
|
165
|
+
end
|
203
166
|
end
|
204
|
-
end
|
205
167
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
168
|
+
def select_font
|
169
|
+
font_families = [computed_properties.font_family, document.fallback_font_name]
|
170
|
+
font_style = :italic if computed_properties.font_style == 'italic'
|
171
|
+
font_weight = computed_properties.font_weight
|
210
172
|
|
211
|
-
|
212
|
-
|
213
|
-
|
173
|
+
font_families.compact.each do |name|
|
174
|
+
font = document.font_registry.load(name, font_weight, font_style)
|
175
|
+
return font if font
|
176
|
+
end
|
177
|
+
|
178
|
+
warnings << "Font family '#{computed_properties.font_family}' style '#{computed_properties.font_style}' is not a known font, and the fallback font could not be found."
|
179
|
+
nil
|
214
180
|
end
|
215
181
|
|
216
|
-
|
217
|
-
|
218
|
-
|
182
|
+
def total_flexible_and_fixed_width
|
183
|
+
flexible = fixed = 0
|
184
|
+
@children.each do |child|
|
185
|
+
child.total_flexible_and_fixed_width.tap do |a, b|
|
186
|
+
flexible += a
|
187
|
+
fixed += b
|
188
|
+
end
|
189
|
+
end
|
190
|
+
[flexible, fixed]
|
191
|
+
end
|
219
192
|
|
220
|
-
|
221
|
-
|
222
|
-
|
193
|
+
def apply_factor_to_base_width(factor)
|
194
|
+
@children.each do |child|
|
195
|
+
if child.is_a?(Elements::TextNode)
|
196
|
+
child.chunks.reject(&:fixed_width).each do |chunk|
|
197
|
+
chunk.fixed_width = chunk.base_width * factor
|
198
|
+
end
|
199
|
+
elsif child.is_a?(self.class)
|
200
|
+
child.apply_factor_to_base_width(factor)
|
201
|
+
else
|
202
|
+
raise
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
223
206
|
|
224
|
-
|
225
|
-
|
226
|
-
stroke = !computed_properties.stroke.none? # rubocop:disable Style/InverseMethods
|
207
|
+
# overridden from Base, we don't want to call fill/stroke as draw_text does this for us
|
208
|
+
def apply_drawing_call; end
|
227
209
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
:fill
|
232
|
-
elsif stroke
|
233
|
-
:stroke
|
234
|
-
else
|
235
|
-
:invisible
|
210
|
+
# overridden from Base, transforms can't be applied to tspan elements
|
211
|
+
def transformable?
|
212
|
+
source.name != 'tspan'
|
236
213
|
end
|
237
|
-
end
|
238
214
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
215
|
+
# overridden from Base, we want the id to point to the Text element
|
216
|
+
def add_to_elements_by_id?
|
217
|
+
source.name != 'text'
|
218
|
+
end
|
243
219
|
|
244
|
-
|
245
|
-
|
220
|
+
def normalize_length(length)
|
221
|
+
x_pixels(length) if length&.match(/\d/)
|
222
|
+
end
|
246
223
|
|
247
|
-
|
248
|
-
|
224
|
+
def parse_wsp(name)
|
225
|
+
(attributes[name] || '').split(COMMA_WSP_REGEXP)
|
226
|
+
end
|
249
227
|
end
|
250
228
|
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module Prawn::SVG
|
2
|
+
class Elements::TextNode
|
3
|
+
Chunk = Struct.new(:text, :x, :y, :dx, :dy, :rotate, :base_width, :offset, :fixed_width)
|
4
|
+
|
5
|
+
attr_reader :component, :chunks
|
6
|
+
attr_accessor :text
|
7
|
+
|
8
|
+
def initialize(component, text, leading_space, trailing_space)
|
9
|
+
@component = component
|
10
|
+
@text = text
|
11
|
+
@leading_space = leading_space
|
12
|
+
@trailing_space = trailing_space
|
13
|
+
end
|
14
|
+
|
15
|
+
def leading_space?
|
16
|
+
@leading_space
|
17
|
+
end
|
18
|
+
|
19
|
+
def trailing_space?
|
20
|
+
@trailing_space
|
21
|
+
end
|
22
|
+
|
23
|
+
def calculated_width
|
24
|
+
@chunks.reduce(0) do |total, chunk|
|
25
|
+
total + (chunk.fixed_width || chunk.base_width) + chunk.offset
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def total_flexible_and_fixed_width
|
30
|
+
flexible = fixed = 0
|
31
|
+
chunks.each do |chunk|
|
32
|
+
if chunk.fixed_width.nil?
|
33
|
+
flexible += chunk.base_width
|
34
|
+
fixed += chunk.offset
|
35
|
+
else
|
36
|
+
fixed += chunk.offset + chunk.fixed_width
|
37
|
+
end
|
38
|
+
end
|
39
|
+
[flexible, fixed]
|
40
|
+
end
|
41
|
+
|
42
|
+
def lay_out(prawn)
|
43
|
+
remaining_text = @text
|
44
|
+
@chunks = []
|
45
|
+
|
46
|
+
while remaining_text != ''
|
47
|
+
x = y = dx = dy = rotate = nil
|
48
|
+
remaining = rotation_remaining = false
|
49
|
+
|
50
|
+
comp = component
|
51
|
+
while comp
|
52
|
+
shifted = comp.x_values.shift
|
53
|
+
x ||= shifted
|
54
|
+
shifted = comp.y_values.shift
|
55
|
+
y ||= shifted
|
56
|
+
shifted = comp.dx.shift
|
57
|
+
dx ||= shifted
|
58
|
+
shifted = comp.dy.shift
|
59
|
+
dy ||= shifted
|
60
|
+
|
61
|
+
shifted = comp.rotation.length > 1 ? comp.rotation.shift : comp.rotation.first
|
62
|
+
if shifted && rotate.nil?
|
63
|
+
rotate = shifted
|
64
|
+
remaining ||= comp.rotation != [0]
|
65
|
+
end
|
66
|
+
|
67
|
+
remaining ||= comp.x_values.any? || comp.y_values.any? || comp.dx.any? || comp.dy.any? || (rotate && rotate != 0)
|
68
|
+
rotation_remaining ||= comp.rotation.length > 1
|
69
|
+
comp = comp.parent_component
|
70
|
+
end
|
71
|
+
|
72
|
+
rotate = (-rotate if rotate && rotate != 0)
|
73
|
+
|
74
|
+
text_to_draw = remaining ? remaining_text[0..0] : remaining_text
|
75
|
+
|
76
|
+
opts = { size: component.computed_properties.numeric_font_size, kerning: true }
|
77
|
+
|
78
|
+
total_spacing = text_to_draw.length > 1 ? (component.letter_spacing_pixels || 0) * (text_to_draw.length - 1) : 0
|
79
|
+
base_width = prawn.width_of(text_to_draw, opts) + total_spacing
|
80
|
+
|
81
|
+
offset = dx ? [0, dx].max : 0
|
82
|
+
|
83
|
+
@chunks << Chunk.new(text_to_draw, x, y, dx, dy, rotate, base_width, offset, nil)
|
84
|
+
|
85
|
+
if remaining
|
86
|
+
remaining_text = remaining_text[1..]
|
87
|
+
else
|
88
|
+
# we can get to this path with rotations still pending
|
89
|
+
# solve this by shifting them out by the number of
|
90
|
+
# characters we've just drawn
|
91
|
+
shift = remaining_text.length - 1
|
92
|
+
if rotation_remaining && shift.positive?
|
93
|
+
comp = component
|
94
|
+
while comp
|
95
|
+
count = [shift, comp.rotation.length - 1].min
|
96
|
+
comp.rotation.shift(count) if count.positive?
|
97
|
+
comp = comp.parent_component
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
break
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def render(prawn, size, cursor, y_offset)
|
107
|
+
chunks.each do |chunk|
|
108
|
+
cursor.x = chunk.x if chunk.x
|
109
|
+
cursor.x += chunk.dx if chunk.dx
|
110
|
+
cursor.y = chunk.y if chunk.y
|
111
|
+
cursor.y -= chunk.dy if chunk.dy
|
112
|
+
|
113
|
+
render_underline(prawn, size, cursor, y_offset, chunk.fixed_width || chunk.base_width) if component.computed_properties.text_decoration == 'underline'
|
114
|
+
|
115
|
+
opts = { size: size, at: [cursor.x, cursor.y + (y_offset || 0)] }
|
116
|
+
opts[:rotate] = chunk.rotate if chunk.rotate
|
117
|
+
|
118
|
+
scaling =
|
119
|
+
if chunk.fixed_width && component.current_length_adjust_is_scaling?
|
120
|
+
chunk.fixed_width * 100 / chunk.base_width
|
121
|
+
else
|
122
|
+
100
|
123
|
+
end
|
124
|
+
|
125
|
+
spacing_enabled = chunk.fixed_width && !component.current_length_adjust_is_scaling? && chunk.text.length > 1
|
126
|
+
|
127
|
+
# This isn't perfect. It assumes the parent component which started the textLength context
|
128
|
+
# has a character at the end of its text nodes. If it doesn't, the last character in its
|
129
|
+
# children should not take the space. This is possible but would involve a lot more work so
|
130
|
+
# I will park it for now.
|
131
|
+
parent_spacing = spacing_enabled && !component.text_length
|
132
|
+
spacing =
|
133
|
+
if spacing_enabled
|
134
|
+
((chunk.fixed_width - chunk.base_width) / (chunk.text.length - (parent_spacing ? 0 : 1))) + (component.letter_spacing_pixels || 0)
|
135
|
+
end
|
136
|
+
|
137
|
+
prawn.horizontal_text_scaling(scaling) do
|
138
|
+
prawn.character_spacing(spacing || component.letter_spacing_pixels || prawn.character_spacing) do
|
139
|
+
prawn.text_rendering_mode(calculate_text_rendering_mode) do
|
140
|
+
prawn.draw_text(chunk.text, **opts)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
cursor.x += chunk.fixed_width || chunk.base_width
|
146
|
+
|
147
|
+
# If we're in a textLength context for one of our parents, we'll need to add spacing
|
148
|
+
# to the end of our string. See comment above for why this isn't quite right.
|
149
|
+
cursor.x += spacing if parent_spacing
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def render_underline(prawn, size, cursor, y_offset, width)
|
154
|
+
offset, thickness = FontMetrics.underline_metrics(prawn, size)
|
155
|
+
|
156
|
+
prawn.fill_rectangle [cursor.x, cursor.y + (y_offset || 0) + offset + (thickness / 2.0)], width, thickness
|
157
|
+
end
|
158
|
+
|
159
|
+
def calculate_text_rendering_mode
|
160
|
+
fill = !component.computed_properties.fill.none? # rubocop:disable Style/InverseMethods
|
161
|
+
stroke = !component.computed_properties.stroke.none? # rubocop:disable Style/InverseMethods
|
162
|
+
|
163
|
+
if fill && stroke
|
164
|
+
:fill_stroke
|
165
|
+
elsif fill
|
166
|
+
:fill
|
167
|
+
elsif stroke
|
168
|
+
:stroke
|
169
|
+
else
|
170
|
+
:invisible
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/prawn/svg/elements.rb
CHANGED
@@ -4,7 +4,7 @@ end
|
|
4
4
|
|
5
5
|
require 'prawn/svg/elements/call_duplicator'
|
6
6
|
|
7
|
-
%w[base
|
7
|
+
%w[base direct_render_base root container clip_path viewport text text_component text_node line polyline polygon circle ellipse
|
8
8
|
rect path use image gradient marker ignored].each do |filename|
|
9
9
|
require "prawn/svg/elements/#{filename}"
|
10
10
|
end
|