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
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
data/README.md
CHANGED
@@ -114,9 +114,9 @@ prawn-svg supports CSS, both in `<style>` blocks and `style` attributes.
|
|
114
114
|
In CSS selectors you can use element names, IDs, classes, attributes (existence, `=`, `^=`, `$=`, `*=`, `~=`, `|=`)
|
115
115
|
and all combinators (` `, `>`, `+`, `~`).
|
116
116
|
The pseudo-classes `:first-child`, `:last-child` and `:nth-child(n)` (where n is a number) also work.
|
117
|
+
`!important` is supported.
|
117
118
|
|
118
|
-
Pseudo-elements and the other pseudo-classes are not supported.
|
119
|
-
implemented, but `!important` is not.
|
119
|
+
Pseudo-elements and the other pseudo-classes are not supported.
|
120
120
|
|
121
121
|
## Not supported
|
122
122
|
|
@@ -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))
|
@@ -1,7 +1,5 @@
|
|
1
1
|
module Prawn::SVG::CSS
|
2
2
|
class Stylesheets
|
3
|
-
USER_AGENT_STYLESHEET = 'svg, symbol, image, marker, pattern, foreignObject { overflow: hidden }'.freeze
|
4
|
-
|
5
3
|
attr_reader :css_parser, :root, :media
|
6
4
|
|
7
5
|
def initialize(css_parser, root, media = :all)
|
@@ -11,7 +9,6 @@ module Prawn::SVG::CSS
|
|
11
9
|
end
|
12
10
|
|
13
11
|
def load
|
14
|
-
load_user_agent_stylesheet
|
15
12
|
load_style_elements
|
16
13
|
xpath_styles = gather_xpath_styles
|
17
14
|
associate_xpath_styles_with_elements(xpath_styles)
|
@@ -19,10 +16,6 @@ module Prawn::SVG::CSS
|
|
19
16
|
|
20
17
|
private
|
21
18
|
|
22
|
-
def load_user_agent_stylesheet
|
23
|
-
css_parser.add_block!(USER_AGENT_STYLESHEET)
|
24
|
-
end
|
25
|
-
|
26
19
|
def load_style_elements
|
27
20
|
REXML::XPath.match(root, '//style').each do |source|
|
28
21
|
data = source.texts.map(&:value).join
|
@@ -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
|
@@ -142,7 +146,7 @@ class Prawn::SVG::Elements::Base
|
|
142
146
|
source.elements.select do |elem|
|
143
147
|
# To be strict, we shouldn't treat namespace-less elements as SVG, but for
|
144
148
|
# backwards compatibility, and because it doesn't hurt, we will.
|
145
|
-
|
149
|
+
[SVG_NAMESPACE, ''].include?(elem.namespace)
|
146
150
|
end
|
147
151
|
end
|
148
152
|
|
@@ -203,20 +207,27 @@ class Prawn::SVG::Elements::Base
|
|
203
207
|
end
|
204
208
|
|
205
209
|
def extract_attributes_and_properties
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
@properties.set(name, value)
|
210
|
-
end
|
210
|
+
# Apply user agent stylesheet
|
211
|
+
if %w[svg symbol image marker pattern foreignObject].include?(source.name)
|
212
|
+
@properties.set('overflow', 'hidden')
|
211
213
|
end
|
212
214
|
|
213
|
-
|
214
|
-
|
215
|
+
# Apply presentation attributes, and set attributes that aren't presentation attributes
|
215
216
|
source.attributes.each do |name, value|
|
216
217
|
# Properties#set returns nil if it's not a recognised property name
|
217
218
|
@properties.set(name, value) or @attributes[name] = value
|
218
219
|
end
|
219
220
|
|
221
|
+
# Apply stylesheet styles
|
222
|
+
if (styles = document.element_styles[source])
|
223
|
+
styles.each do |name, value, important|
|
224
|
+
@properties.set(name, value, important: important)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Apply inline styles
|
229
|
+
@properties.load_hash(parse_css_declarations(source.attributes['style'] || ''))
|
230
|
+
|
220
231
|
state.computed_properties.compute_properties(@properties)
|
221
232
|
end
|
222
233
|
|
@@ -262,6 +273,14 @@ class Prawn::SVG::Elements::Base
|
|
262
273
|
['hidden', 'scroll'].include?(computed_properties.overflow)
|
263
274
|
end
|
264
275
|
|
276
|
+
def transformable?
|
277
|
+
true
|
278
|
+
end
|
279
|
+
|
280
|
+
def add_to_elements_by_id?
|
281
|
+
true
|
282
|
+
end
|
283
|
+
|
265
284
|
def stroke_width
|
266
285
|
if computed_properties.stroke.none?
|
267
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
|