prawn-svg 0.37.0 → 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/lib/prawn/svg/attributes/transform.rb +2 -0
- data/lib/prawn/svg/elements/base.rb +13 -1
- 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/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/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/positioning.svg +26 -0
- data/spec/sample_svg/subfamilies.svg +5 -1
- data/spec/sample_svg/text_use.svg +37 -0
- metadata +9 -3
- data/lib/prawn/svg/elements/depth_first_base.rb +0 -52
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9d63836ebf08ec1ebd6320b61a8f4b93c5df29e91e6e1397b53c65d548e8afe
|
4
|
+
data.tar.gz: 25ad6658329c42589d086e592589908a1c556a631503578219c308ab095fa513
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d73e5adc3fa832d2ed9159cf02cea3b47cf3923ff0e145af4de426effeb6157d4d133d600bf708ccd963a736d517fccb8a7c8d410ee479025288193659d34859
|
7
|
+
data.tar.gz: 20d5d9f28438c3ca4c23454541f763b1766f3df9719ced30a3451abc08e21913cf4276d2c148d9af695bab9e4670b01dcc1985fe5d9a9ebd94cfc08cee755818
|
data/Gemfile.lock
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module Prawn::SVG::Attributes::Transform
|
2
2
|
def parse_transform_attribute_and_call
|
3
|
+
# Some elements do not support transforms
|
4
|
+
return unless transformable?
|
3
5
|
return unless (transform = attributes['transform'])
|
4
6
|
|
5
7
|
matrix = matrix_for_pdf(parse_transform_attribute(transform))
|
@@ -36,7 +36,7 @@ class Prawn::SVG::Elements::Base
|
|
36
36
|
@attributes = {}
|
37
37
|
@properties = Prawn::SVG::Properties.new
|
38
38
|
|
39
|
-
if source && !state.inside_use
|
39
|
+
if source && add_to_elements_by_id? && !state.inside_use
|
40
40
|
id = source.attributes['id']
|
41
41
|
id = id.strip if id
|
42
42
|
|
@@ -114,6 +114,10 @@ class Prawn::SVG::Elements::Base
|
|
114
114
|
@calls.concat duplicate_calls(other.base_calls)
|
115
115
|
end
|
116
116
|
|
117
|
+
def add_yield_call(&block)
|
118
|
+
add_call('svg:yield', block)
|
119
|
+
end
|
120
|
+
|
117
121
|
def new_call_context_from_base
|
118
122
|
old_calls = @calls
|
119
123
|
@calls = @base_calls
|
@@ -269,6 +273,14 @@ class Prawn::SVG::Elements::Base
|
|
269
273
|
['hidden', 'scroll'].include?(computed_properties.overflow)
|
270
274
|
end
|
271
275
|
|
276
|
+
def transformable?
|
277
|
+
true
|
278
|
+
end
|
279
|
+
|
280
|
+
def add_to_elements_by_id?
|
281
|
+
true
|
282
|
+
end
|
283
|
+
|
272
284
|
def stroke_width
|
273
285
|
if computed_properties.stroke.none?
|
274
286
|
0
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#
|
2
|
+
# This element base class is used for elements that render directly to the Prawn canvas.
|
3
|
+
#
|
4
|
+
# Initially when I wrote prawn-svg, I was expecting to have multiple renderers and thought separating the codebase
|
5
|
+
# from the Prawn renderer would be a good idea. However, it turns out that the Prawn renderer was the only one
|
6
|
+
# that was targeted, and it ended up being tightly coupled with the Prawn library.
|
7
|
+
#
|
8
|
+
# This class is probably how I should have written it. Direct render is required to do text rendering properly
|
9
|
+
# because we need to know the width of all the things we print. As of the time of this comment it's the only
|
10
|
+
# system that uses DirectRenderBase in prawn-svg.
|
11
|
+
#
|
12
|
+
class Prawn::SVG::Elements::DirectRenderBase < Prawn::SVG::Elements::Base
|
13
|
+
# Called by Renderer when it finds the svg:render call added below.
|
14
|
+
def render(prawn, renderer); end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def parse_and_apply
|
19
|
+
parse_standard_attributes
|
20
|
+
parse
|
21
|
+
apply_calls_from_standard_attributes
|
22
|
+
@parent_calls << ['svg:render', [self], [], []] unless computed_properties.display == 'none'
|
23
|
+
rescue SkipElementQuietly
|
24
|
+
rescue SkipElementError => e
|
25
|
+
@document.warnings << e.message
|
26
|
+
end
|
27
|
+
end
|
@@ -16,9 +16,9 @@ class Prawn::SVG::Elements::Polygon < Prawn::SVG::Elements::Base
|
|
16
16
|
def commands
|
17
17
|
@commands ||= [
|
18
18
|
Prawn::SVG::Pathable::Move.new(@points[0])
|
19
|
-
] + @points[1..].map
|
19
|
+
] + @points[1..].map do |point|
|
20
20
|
Prawn::SVG::Pathable::Line.new(point)
|
21
|
-
|
21
|
+
end + [
|
22
22
|
Prawn::SVG::Pathable::Close.new(@points[0])
|
23
23
|
]
|
24
24
|
end
|
@@ -1,70 +1,72 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Prawn::SVG
|
2
|
+
class Elements::Text < Elements::DirectRenderBase
|
3
|
+
Cursor = Struct.new(:x, :y)
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
# of the element, delegating it to our root text component.
|
5
|
+
def parse
|
6
|
+
@root_component = Elements::TextComponent.new(document, source, [], state.dup)
|
7
|
+
@root_component.process
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
reintroduce_trailing_and_leading_whitespace
|
10
|
+
end
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
def render(prawn, renderer)
|
13
|
+
@root_component.lay_out(prawn)
|
14
14
|
|
15
|
-
|
16
|
-
|
15
|
+
translate_x =
|
16
|
+
case @root_component.computed_properties.text_anchor
|
17
|
+
when 'middle'
|
18
|
+
-@root_component.calculated_width / 2.0
|
19
|
+
when 'end'
|
20
|
+
-@root_component.calculated_width
|
21
|
+
end
|
17
22
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@text_root.apply_step(@calls)
|
22
|
-
end
|
23
|
+
cursor = Cursor.new(0, document.sizing.output_height)
|
24
|
+
@root_component.render_component(prawn, renderer, cursor, translate_x)
|
25
|
+
end
|
23
26
|
|
24
|
-
|
25
|
-
false
|
26
|
-
end
|
27
|
+
private
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
def reintroduce_trailing_and_leading_whitespace
|
30
|
+
text_nodes = []
|
31
|
+
build_text_node_queue(text_nodes, @root_component)
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
remove_whitespace_only_text_nodes_and_start_and_end(text_nodes)
|
34
|
+
remove_text_nodes_that_are_completely_empty(text_nodes)
|
35
|
+
apportion_leading_and_trailing_spaces(text_nodes)
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
38
|
+
def build_text_node_queue(queue, component)
|
39
|
+
component.children.each do |element|
|
40
|
+
case element
|
41
|
+
when Elements::TextNode
|
42
|
+
queue << element
|
43
|
+
else
|
44
|
+
build_text_node_queue(queue, element)
|
45
|
+
end
|
44
46
|
end
|
45
47
|
end
|
46
|
-
end
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
def remove_whitespace_only_text_nodes_and_start_and_end(text_nodes)
|
50
|
+
text_nodes.pop while text_nodes.last && text_nodes.last.text.empty?
|
51
|
+
text_nodes.shift while text_nodes.first && text_nodes.first.text.empty?
|
52
|
+
end
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
54
|
+
def remove_text_nodes_that_are_completely_empty(text_nodes)
|
55
|
+
text_nodes.reject! do |text_node|
|
56
|
+
text_node.text.empty? && !text_node.trailing_space? && !text_node.leading_space?
|
57
|
+
end
|
56
58
|
end
|
57
|
-
end
|
58
59
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
60
|
+
def apportion_leading_and_trailing_spaces(text_nodes)
|
61
|
+
text_nodes.each_cons(2) do |a, b|
|
62
|
+
if a.text.empty?
|
63
|
+
# Empty strings can only get a leading space from the previous non-empty text,
|
64
|
+
# and never get a trailing space
|
65
|
+
elsif a.trailing_space?
|
66
|
+
a.text += ' '
|
67
|
+
elsif b.leading_space?
|
68
|
+
b.text = " #{b.text}"
|
69
|
+
end
|
68
70
|
end
|
69
71
|
end
|
70
72
|
end
|
@@ -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
|