prawn-svg 0.37.0 → 0.40.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/.gitignore +1 -0
- data/Gemfile.lock +8 -8
- data/README.md +57 -35
- data/lib/prawn/svg/attributes/clip_path.rb +32 -2
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/stroke.rb +11 -1
- data/lib/prawn/svg/attributes/transform.rb +2 -0
- data/lib/prawn/svg/attributes.rb +1 -1
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +15 -74
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +15 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/stylesheets.rb +92 -5
- data/lib/prawn/svg/document.rb +98 -2
- data/lib/prawn/svg/elements/anchor.rb +9 -0
- data/lib/prawn/svg/elements/base.rb +28 -1
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/clip_path.rb +17 -2
- data/lib/prawn/svg/elements/container.rb +0 -7
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/gradient.rb +0 -19
- data/lib/prawn/svg/elements/image.rb +65 -2
- data/lib/prawn/svg/elements/marker.rb +1 -2
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +9 -16
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +2 -2
- data/lib/prawn/svg/elements/rect.rb +40 -9
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +80 -52
- data/lib/prawn/svg/elements/text_component.rb +293 -197
- data/lib/prawn/svg/elements/text_node.rb +308 -0
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +97 -19
- data/lib/prawn/svg/elements.rb +8 -5
- data/lib/prawn/svg/font.rb +14 -59
- data/lib/prawn/svg/font_metrics.rb +152 -0
- data/lib/prawn/svg/font_registry.rb +294 -54
- data/lib/prawn/svg/gradient_renderer.rb +11 -3
- data/lib/prawn/svg/gradients.rb +5 -2
- data/lib/prawn/svg/interface.rb +4 -1
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/pathable.rb +8 -5
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/properties.rb +125 -32
- data/lib/prawn/svg/renderer.rb +71 -70
- data/lib/prawn/svg/state.rb +1 -1
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +77 -47
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +6 -0
- data/prawn-svg.gemspec +2 -2
- data/scripts/compare_samples +25 -0
- data/spec/integration_spec.rb +36 -16
- data/spec/prawn/svg/attributes/clip_path_spec.rb +246 -0
- data/spec/prawn/svg/attributes/stroke_spec.rb +126 -0
- data/spec/prawn/svg/attributes/transform_spec.rb +4 -0
- data/spec/prawn/svg/calculators/path_length_spec.rb +141 -0
- data/spec/prawn/svg/css/font_face_parser_spec.rb +72 -0
- data/spec/prawn/svg/css/selector_parser_spec.rb +6 -0
- data/spec/prawn/svg/css/stylesheets_spec.rb +370 -1
- data/spec/prawn/svg/document_spec.rb +199 -2
- data/spec/prawn/svg/elements/base_spec.rb +2 -2
- data/spec/prawn/svg/elements/gradient_spec.rb +1 -1
- data/spec/prawn/svg/elements/image_spec.rb +169 -0
- data/spec/prawn/svg/elements/line_spec.rb +1 -1
- data/spec/prawn/svg/elements/marker_spec.rb +2 -1
- data/spec/prawn/svg/elements/mask_spec.rb +382 -0
- data/spec/prawn/svg/elements/path_spec.rb +2 -2
- data/spec/prawn/svg/elements/pattern_spec.rb +232 -0
- data/spec/prawn/svg/elements/polygon_spec.rb +1 -1
- data/spec/prawn/svg/elements/polyline_spec.rb +1 -1
- data/spec/prawn/svg/elements/switch_spec.rb +310 -0
- data/spec/prawn/svg/elements/text_path_spec.rb +183 -0
- data/spec/prawn/svg/elements/text_spec.rb +716 -134
- data/spec/prawn/svg/elements/use_spec.rb +274 -0
- data/spec/prawn/svg/font_metrics_spec.rb +14 -0
- data/spec/prawn/svg/font_registry_spec.rb +347 -24
- data/spec/prawn/svg/font_spec.rb +67 -8
- data/spec/prawn/svg/interface_spec.rb +32 -12
- data/spec/prawn/svg/pathable_spec.rb +2 -2
- data/spec/prawn/svg/properties_spec.rb +198 -0
- data/spec/prawn/svg/ttc_spec.rb +31 -0
- data/spec/sample_css/import_base.css +4 -0
- data/spec/sample_css/import_nested.css +1 -0
- data/spec/sample_svg/alignment_baseline.svg +61 -0
- data/spec/sample_svg/arc_test_a.svg +186 -0
- data/spec/sample_svg/arc_test_b.svg +194 -0
- data/spec/sample_svg/baseline_shift.svg +55 -0
- data/spec/sample_svg/bytes.svg +121 -0
- data/spec/sample_svg/clip_path_text.svg +104 -0
- data/spec/sample_svg/clip_path_units.svg +102 -0
- data/spec/sample_svg/css_import.svg +38 -0
- data/spec/sample_svg/dominant_baseline.svg +41 -0
- data/spec/sample_svg/font_face.svg +77 -0
- data/spec/sample_svg/font_stretch.svg +54 -0
- data/spec/sample_svg/font_weights.svg +140 -0
- data/spec/sample_svg/kerning.svg +45 -0
- data/spec/sample_svg/lang.svg +117 -0
- data/spec/sample_svg/links.svg +42 -2
- data/spec/sample_svg/marker_shorthand.svg +48 -0
- data/spec/sample_svg/mask3.svg +13 -0
- data/spec/sample_svg/mask3b.svg +13 -0
- data/spec/sample_svg/mask_basic.svg +11 -0
- data/spec/sample_svg/mask_contentUnits_objectBoundingBox.svg +11 -0
- data/spec/sample_svg/mask_gradient.svg +13 -0
- data/spec/sample_svg/mask_image.svg +10 -0
- data/spec/sample_svg/mask_multiple.svg +15 -0
- data/spec/sample_svg/mask_nested.svg +15 -0
- data/spec/sample_svg/mask_opacity.svg +21 -0
- data/spec/sample_svg/mask_text.svg +10 -0
- data/spec/sample_svg/mask_text_gradient.svg +31 -0
- data/spec/sample_svg/mask_units_objectBoundingBox.svg +11 -0
- data/spec/sample_svg/mask_units_userSpaceOnUse.svg +10 -0
- data/spec/sample_svg/mask_with_transform.svg +11 -0
- data/spec/sample_svg/multilingual.svg +7 -0
- data/spec/sample_svg/pattern.svg +80 -0
- data/spec/sample_svg/positioning.svg +26 -0
- data/spec/sample_svg/rounded_rectangle.svg +101 -0
- data/spec/sample_svg/stroke_dashoffset.svg +48 -0
- data/spec/sample_svg/stroke_miterlimit.svg +48 -0
- data/spec/sample_svg/subfamilies.svg +5 -1
- data/spec/sample_svg/switch.svg +106 -0
- data/spec/sample_svg/text-decoration.svg +5 -3
- data/spec/sample_svg/text_path.svg +25 -0
- data/spec/sample_svg/text_use.svg +37 -0
- data/spec/sample_svg/use_external.svg +51 -0
- data/spec/sample_svg/use_external_icons.svg +29 -0
- data/spec/sample_svg/vertical-text.svg +152 -0
- data/spec/sample_svg/view.svg +48 -0
- data/spec/sample_svg/word_spacing.svg +45 -0
- data/spec/sample_ttf/TestFamily.ttc +0 -0
- metadata +86 -7
- data/lib/prawn/svg/elements/depth_first_base.rb +0 -52
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Prawn::SVG::CSS
|
|
2
|
+
class FontFaceParser
|
|
3
|
+
SUPPORTED_FORMATS = %w[truetype opentype].freeze
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def parse_src(src)
|
|
7
|
+
split_sources(src).filter_map { |entry| parse_source_entry(entry.strip) }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def split_sources(value)
|
|
13
|
+
entries = []
|
|
14
|
+
current = +''
|
|
15
|
+
depth = 0
|
|
16
|
+
in_quote = nil
|
|
17
|
+
|
|
18
|
+
value.each_char do |char|
|
|
19
|
+
if in_quote
|
|
20
|
+
current << char
|
|
21
|
+
in_quote = nil if char == in_quote
|
|
22
|
+
elsif ['"', "'"].include?(char)
|
|
23
|
+
in_quote = char
|
|
24
|
+
current << char
|
|
25
|
+
elsif char == '('
|
|
26
|
+
depth += 1
|
|
27
|
+
current << char
|
|
28
|
+
elsif char == ')'
|
|
29
|
+
depth -= 1
|
|
30
|
+
current << char
|
|
31
|
+
elsif char == ',' && depth.zero?
|
|
32
|
+
entries << current
|
|
33
|
+
current = +''
|
|
34
|
+
else
|
|
35
|
+
current << char
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
entries << current unless current.strip.empty?
|
|
40
|
+
entries
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_source_entry(entry)
|
|
44
|
+
parts = Prawn::SVG::CSS::ValuesParser.parse(entry)
|
|
45
|
+
return if parts.empty?
|
|
46
|
+
|
|
47
|
+
first = parts[0]
|
|
48
|
+
return unless first.is_a?(Array)
|
|
49
|
+
|
|
50
|
+
case first[0]
|
|
51
|
+
when 'url'
|
|
52
|
+
url = first[1][0]
|
|
53
|
+
return unless url
|
|
54
|
+
|
|
55
|
+
format = extract_format(parts[1])
|
|
56
|
+
{ type: :url, url: url, format: format }
|
|
57
|
+
when 'local'
|
|
58
|
+
name = first[1][0]
|
|
59
|
+
return unless name
|
|
60
|
+
|
|
61
|
+
{ type: :local, name: name }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def extract_format(part)
|
|
66
|
+
return unless part.is_a?(Array) && part[0] == 'format'
|
|
67
|
+
|
|
68
|
+
part[1][0]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
module Prawn::SVG::CSS
|
|
2
2
|
class Stylesheets
|
|
3
|
-
attr_reader :css_parser, :root, :media
|
|
3
|
+
attr_reader :css_parser, :root, :media, :font_face_rules
|
|
4
4
|
|
|
5
|
-
def initialize(css_parser, root, media = :all)
|
|
5
|
+
def initialize(css_parser, root, media = :all, url_loader: nil, warnings: [])
|
|
6
6
|
@css_parser = css_parser
|
|
7
7
|
@root = root
|
|
8
8
|
@media = media
|
|
9
|
+
@warnings = warnings
|
|
10
|
+
@font_face_rules = []
|
|
11
|
+
|
|
12
|
+
@has_url_loader = !url_loader.nil?
|
|
13
|
+
install_url_loader_on_css_parser(url_loader) if url_loader
|
|
9
14
|
end
|
|
10
15
|
|
|
11
16
|
def load
|
|
@@ -16,10 +21,35 @@ module Prawn::SVG::CSS
|
|
|
16
21
|
|
|
17
22
|
private
|
|
18
23
|
|
|
24
|
+
def install_url_loader_on_css_parser(url_loader)
|
|
25
|
+
warnings = @warnings
|
|
26
|
+
|
|
27
|
+
css_parser.define_singleton_method(:load_uri!) do |uri, options = {}, _deprecated = nil|
|
|
28
|
+
uri = Addressable::URI.parse(uri) unless uri.respond_to?(:scheme)
|
|
29
|
+
import_url = uri.to_s
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
return unless circular_reference_check(import_url)
|
|
33
|
+
|
|
34
|
+
src = url_loader.load(import_url)
|
|
35
|
+
add_block!(src, options) if src
|
|
36
|
+
rescue CssParser::CircularReferenceError
|
|
37
|
+
# Silently ignore circular @import references
|
|
38
|
+
rescue Prawn::SVG::UrlLoader::Error => e
|
|
39
|
+
warnings << "Failed to load @import CSS from #{import_url}: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
19
44
|
def load_style_elements
|
|
20
45
|
REXML::XPath.match(root, '//style').each do |source|
|
|
21
46
|
data = source.texts.map(&:value).join
|
|
22
|
-
|
|
47
|
+
|
|
48
|
+
if @has_url_loader
|
|
49
|
+
css_parser.add_block!(data, base_uri: '')
|
|
50
|
+
else
|
|
51
|
+
css_parser.add_block!(data)
|
|
52
|
+
end
|
|
23
53
|
end
|
|
24
54
|
end
|
|
25
55
|
|
|
@@ -32,6 +62,11 @@ module Prawn::SVG::CSS
|
|
|
32
62
|
rule_set.each_declaration { |*data| declarations << data }
|
|
33
63
|
|
|
34
64
|
rule_set.selectors.each do |selector_text|
|
|
65
|
+
if selector_text.strip == '@font-face'
|
|
66
|
+
@font_face_rules << declarations
|
|
67
|
+
next
|
|
68
|
+
end
|
|
69
|
+
|
|
35
70
|
next unless (selector = Prawn::SVG::CSS::SelectorParser.parse(selector_text))
|
|
36
71
|
|
|
37
72
|
xpath = css_selector_to_xpath(selector)
|
|
@@ -62,10 +97,13 @@ module Prawn::SVG::CSS
|
|
|
62
97
|
%("#{value.gsub('\\', '\\\\').gsub('"', '\\"')}") if value
|
|
63
98
|
end
|
|
64
99
|
|
|
100
|
+
RECOGNIZED_PSEUDO_CLASSES = %w[first-child last-child link visited hover active focus].freeze
|
|
101
|
+
|
|
65
102
|
def css_selector_to_xpath(selector)
|
|
66
103
|
selector.map do |element|
|
|
67
104
|
pseudo_classes = Set.new(element[:pseudo_class])
|
|
68
105
|
require_function_name = false
|
|
106
|
+
never_match = false
|
|
69
107
|
|
|
70
108
|
result = case element[:combinator]
|
|
71
109
|
when :child
|
|
@@ -80,11 +118,18 @@ module Prawn::SVG::CSS
|
|
|
80
118
|
end
|
|
81
119
|
|
|
82
120
|
positions = []
|
|
121
|
+
lang_predicates = []
|
|
122
|
+
href_predicates = []
|
|
83
123
|
pseudo_classes.each do |pc|
|
|
84
124
|
case pc
|
|
85
125
|
when 'first-child' then positions << '1'
|
|
86
126
|
when 'last-child' then positions << 'last()'
|
|
87
|
-
when /^nth-child\((
|
|
127
|
+
when /^nth-child\((.+)\)$/ then positions << parse_nth_child_expression($1)
|
|
128
|
+
when /^lang\((.+)\)$/ then lang_predicates << $1
|
|
129
|
+
when 'link' then href_predicates << true
|
|
130
|
+
when 'visited', 'hover', 'active', 'focus' then never_match = true
|
|
131
|
+
else
|
|
132
|
+
never_match = true unless RECOGNIZED_PSEUDO_CLASSES.include?(pc)
|
|
88
133
|
end
|
|
89
134
|
end
|
|
90
135
|
|
|
@@ -119,7 +164,7 @@ module Prawn::SVG::CSS
|
|
|
119
164
|
when '^='
|
|
120
165
|
result << "[starts-with(@#{key}, #{xpath_quote value})]"
|
|
121
166
|
when '$='
|
|
122
|
-
result << "[substring(@#{key}, string-length(@#{key}) - #{value.length - 1}) = #{xpath_quote value}
|
|
167
|
+
result << "[substring(@#{key}, string-length(@#{key}) - #{value.length - 1}) = #{xpath_quote value}]"
|
|
123
168
|
when '*='
|
|
124
169
|
result << "[contains(@#{key}, #{xpath_quote value})]"
|
|
125
170
|
when '~='
|
|
@@ -129,10 +174,52 @@ module Prawn::SVG::CSS
|
|
|
129
174
|
end
|
|
130
175
|
end
|
|
131
176
|
|
|
177
|
+
lang_predicates.each do |lang|
|
|
178
|
+
result << "[lang(#{xpath_quote lang})]"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
result << '[@href or @xlink:href]' unless href_predicates.empty?
|
|
182
|
+
|
|
183
|
+
result << '[false()]' if never_match
|
|
184
|
+
|
|
132
185
|
result
|
|
133
186
|
end.join
|
|
134
187
|
end
|
|
135
188
|
|
|
189
|
+
def parse_nth_child_expression(expr)
|
|
190
|
+
expr = expr.strip
|
|
191
|
+
|
|
192
|
+
return '(position() mod 2 = 1)' if expr == 'odd'
|
|
193
|
+
return '(position() mod 2 = 0)' if expr == 'even'
|
|
194
|
+
return expr if expr.match?(/\A\d+\z/)
|
|
195
|
+
|
|
196
|
+
match = expr.match(/\A(-?\d*)n\s*([+-]\s*\d+)?\z/)
|
|
197
|
+
return 'false()' unless match
|
|
198
|
+
|
|
199
|
+
a = case match[1]
|
|
200
|
+
when '', '+' then 1
|
|
201
|
+
when '-' then -1
|
|
202
|
+
else match[1].to_i
|
|
203
|
+
end
|
|
204
|
+
b = match[2] ? match[2].gsub(/\s/, '').to_i : 0
|
|
205
|
+
|
|
206
|
+
if a.zero?
|
|
207
|
+
b.to_s
|
|
208
|
+
elsif a.positive?
|
|
209
|
+
if b <= 0
|
|
210
|
+
"(position() mod #{a} = #{b % a} and position() >= #{[b, 1].max})"
|
|
211
|
+
else
|
|
212
|
+
"(position() mod #{a} = #{b % a} and position() >= #{b})"
|
|
213
|
+
end
|
|
214
|
+
elsif b <= 0
|
|
215
|
+
# a is negative: matches positions b, b+a, b+2a, ... while > 0
|
|
216
|
+
# i.e. position() <= b and (position() - b) mod |a| = 0
|
|
217
|
+
'false()'
|
|
218
|
+
else
|
|
219
|
+
"((position() - #{b}) mod #{a.abs} = 0 and position() >= 1 and position() <= #{b})"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
136
223
|
def calculate_specificity(selector)
|
|
137
224
|
selector.reduce([0, 0, 0]) do |(a, b, c), element|
|
|
138
225
|
[
|
data/lib/prawn/svg/document.rb
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
require 'tempfile'
|
|
2
|
+
|
|
1
3
|
class Prawn::SVG::Document
|
|
2
4
|
Error = Class.new(StandardError)
|
|
3
5
|
InvalidSVGData = Class.new(Error)
|
|
4
6
|
|
|
5
7
|
DEFAULT_FALLBACK_FONT_NAME = 'Times-Roman'.freeze
|
|
6
8
|
|
|
9
|
+
class << self
|
|
10
|
+
attr_accessor :enable_web_requests_warned
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
# An +Array+ of warnings that occurred while parsing the SVG data.
|
|
8
14
|
attr_reader :warnings
|
|
9
15
|
|
|
@@ -14,7 +20,8 @@ class Prawn::SVG::Document
|
|
|
14
20
|
:url_loader,
|
|
15
21
|
:elements_by_id, :gradients,
|
|
16
22
|
:element_styles,
|
|
17
|
-
:color_mode
|
|
23
|
+
:color_mode,
|
|
24
|
+
:external_svg_cache
|
|
18
25
|
|
|
19
26
|
def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new, attribute_overrides: {})
|
|
20
27
|
begin
|
|
@@ -27,9 +34,18 @@ class Prawn::SVG::Document
|
|
|
27
34
|
@options = options
|
|
28
35
|
@elements_by_id = {}
|
|
29
36
|
@gradients = Prawn::SVG::Gradients.new(self)
|
|
37
|
+
@external_svg_cache = {}
|
|
30
38
|
@fallback_font_name = options.fetch(:fallback_font_name, DEFAULT_FALLBACK_FONT_NAME)
|
|
31
39
|
@font_registry = font_registry
|
|
32
40
|
@color_mode = load_color_mode
|
|
41
|
+
@font_face_tempfiles = []
|
|
42
|
+
|
|
43
|
+
if !options.key?(:enable_web_requests) && !self.class.enable_web_requests_warned
|
|
44
|
+
self.class.enable_web_requests_warned = true
|
|
45
|
+
warn '[prawn-svg] WARNING: :enable_web_requests is not set and currently defaults to true. ' \
|
|
46
|
+
'In prawn-svg 1.0, this will default to false. ' \
|
|
47
|
+
'Please explicitly pass enable_web_requests: true or enable_web_requests: false to suppress this warning.'
|
|
48
|
+
end
|
|
33
49
|
|
|
34
50
|
@url_loader = Prawn::SVG::UrlLoader.new(
|
|
35
51
|
enable_cache: options[:cache_images],
|
|
@@ -43,7 +59,9 @@ class Prawn::SVG::Document
|
|
|
43
59
|
@sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, attributes)
|
|
44
60
|
calculate_sizing(requested_width: options[:width], requested_height: options[:height])
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
stylesheets = Prawn::SVG::CSS::Stylesheets.new(css_parser, root, url_loader: url_loader, warnings: warnings)
|
|
63
|
+
@element_styles = stylesheets.load
|
|
64
|
+
process_font_face_rules(stylesheets.font_face_rules)
|
|
47
65
|
|
|
48
66
|
yield self if block_given?
|
|
49
67
|
end
|
|
@@ -54,8 +72,86 @@ class Prawn::SVG::Document
|
|
|
54
72
|
sizing.calculate
|
|
55
73
|
end
|
|
56
74
|
|
|
75
|
+
def with_sizing(temporary_sizing)
|
|
76
|
+
original = @sizing
|
|
77
|
+
@sizing = temporary_sizing
|
|
78
|
+
yield
|
|
79
|
+
ensure
|
|
80
|
+
@sizing = original
|
|
81
|
+
end
|
|
82
|
+
|
|
57
83
|
private
|
|
58
84
|
|
|
85
|
+
def process_font_face_rules(rules)
|
|
86
|
+
return unless font_registry
|
|
87
|
+
|
|
88
|
+
rules.each do |declarations|
|
|
89
|
+
decl_hash = {}
|
|
90
|
+
declarations.each { |name, value, _| decl_hash[name] = value }
|
|
91
|
+
|
|
92
|
+
family = unquote_css_string(decl_hash['font-family'])
|
|
93
|
+
next unless family
|
|
94
|
+
|
|
95
|
+
src = decl_hash['src']
|
|
96
|
+
next unless src
|
|
97
|
+
|
|
98
|
+
weight = decl_hash['font-weight']
|
|
99
|
+
style = decl_hash['font-style']
|
|
100
|
+
stretch = decl_hash['font-stretch']
|
|
101
|
+
|
|
102
|
+
load_font_face_source(family, weight, style, stretch, src)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def load_font_face_source(family, weight, style, stretch, src)
|
|
107
|
+
sources = Prawn::SVG::CSS::FontFaceParser.parse_src(src)
|
|
108
|
+
|
|
109
|
+
sources.detect do |source|
|
|
110
|
+
case source[:type]
|
|
111
|
+
when :local
|
|
112
|
+
load_local_font_face(family, weight, style, stretch, source[:name])
|
|
113
|
+
when :url
|
|
114
|
+
load_url_font_face(family, weight, style, stretch, source)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def load_local_font_face(family, weight, style, stretch, local_name)
|
|
120
|
+
font_data = font_registry.find_local_font_data(local_name, weight, style, stretch)
|
|
121
|
+
return unless font_data
|
|
122
|
+
|
|
123
|
+
font_registry.register_font_face(family, weight, style, stretch, font_data)
|
|
124
|
+
true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def load_url_font_face(family, weight, style, stretch, source)
|
|
128
|
+
return if source[:format] && !Prawn::SVG::CSS::FontFaceParser::SUPPORTED_FORMATS.include?(source[:format])
|
|
129
|
+
|
|
130
|
+
data = url_loader.load(source[:url])
|
|
131
|
+
tempfile = Tempfile.new(['prawn-svg-font', '.ttf'])
|
|
132
|
+
tempfile.binmode
|
|
133
|
+
tempfile.write(data)
|
|
134
|
+
tempfile.close
|
|
135
|
+
@font_face_tempfiles << tempfile
|
|
136
|
+
|
|
137
|
+
font_registry.register_font_face(family, weight, style, stretch, tempfile.path)
|
|
138
|
+
true
|
|
139
|
+
rescue Prawn::SVG::UrlLoader::Error => e
|
|
140
|
+
warnings << "Failed to load @font-face font from #{source[:url]}: #{e.message}"
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def unquote_css_string(value)
|
|
145
|
+
return unless value
|
|
146
|
+
|
|
147
|
+
value = value.strip
|
|
148
|
+
if (value.start_with?('"') && value.end_with?('"')) || (value.start_with?("'") && value.end_with?("'"))
|
|
149
|
+
value[1..-2]
|
|
150
|
+
else
|
|
151
|
+
value
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
59
155
|
def load_color_mode
|
|
60
156
|
case @options[:color_mode]
|
|
61
157
|
when nil, :rgb then :rgb
|
|
@@ -2,12 +2,14 @@ class Prawn::SVG::Elements::Base
|
|
|
2
2
|
extend Forwardable
|
|
3
3
|
|
|
4
4
|
include Prawn::SVG::Elements::CallDuplicator
|
|
5
|
+
include Prawn::SVG::Elements::BboxScaling
|
|
5
6
|
|
|
6
7
|
include Prawn::SVG::Calculators::Pixels
|
|
7
8
|
|
|
8
9
|
include Prawn::SVG::Attributes::Transform
|
|
9
10
|
include Prawn::SVG::Attributes::Opacity
|
|
10
11
|
include Prawn::SVG::Attributes::ClipPath
|
|
12
|
+
include Prawn::SVG::Attributes::Mask
|
|
11
13
|
include Prawn::SVG::Attributes::Stroke
|
|
12
14
|
include Prawn::SVG::Attributes::Space
|
|
13
15
|
|
|
@@ -36,7 +38,7 @@ class Prawn::SVG::Elements::Base
|
|
|
36
38
|
@attributes = {}
|
|
37
39
|
@properties = Prawn::SVG::Properties.new
|
|
38
40
|
|
|
39
|
-
if source && !state.inside_use
|
|
41
|
+
if source && add_to_elements_by_id? && !state.inside_use
|
|
40
42
|
id = source.attributes['id']
|
|
41
43
|
id = id.strip if id
|
|
42
44
|
|
|
@@ -56,6 +58,10 @@ class Prawn::SVG::Elements::Base
|
|
|
56
58
|
apply_calls_from_standard_attributes
|
|
57
59
|
apply
|
|
58
60
|
|
|
61
|
+
if state.anchor_href && bounding_box
|
|
62
|
+
add_call('svg:add_link', state.anchor_href, bounding_box)
|
|
63
|
+
end
|
|
64
|
+
|
|
59
65
|
process_child_elements if container?
|
|
60
66
|
|
|
61
67
|
append_calls_to_parent unless computed_properties.display == 'none'
|
|
@@ -114,6 +120,10 @@ class Prawn::SVG::Elements::Base
|
|
|
114
120
|
@calls.concat duplicate_calls(other.base_calls)
|
|
115
121
|
end
|
|
116
122
|
|
|
123
|
+
def add_yield_call(&block)
|
|
124
|
+
add_call('svg:yield', block)
|
|
125
|
+
end
|
|
126
|
+
|
|
117
127
|
def new_call_context_from_base
|
|
118
128
|
old_calls = @calls
|
|
119
129
|
@calls = @base_calls
|
|
@@ -150,6 +160,7 @@ class Prawn::SVG::Elements::Base
|
|
|
150
160
|
parse_transform_attribute_and_call
|
|
151
161
|
parse_opacity_attributes_and_call
|
|
152
162
|
parse_clip_path_attribute_and_call
|
|
163
|
+
parse_mask_attribute_and_call
|
|
153
164
|
apply_colors
|
|
154
165
|
parse_stroke_attributes_and_call
|
|
155
166
|
apply_drawing_call
|
|
@@ -191,6 +202,9 @@ class Prawn::SVG::Elements::Base
|
|
|
191
202
|
add_call "#{type}_color", color.value
|
|
192
203
|
when Prawn::SVG::Elements::Gradient
|
|
193
204
|
add_call 'svg:render_gradient', type.to_sym, **color.gradient_arguments(self)
|
|
205
|
+
when Prawn::SVG::Elements::Pattern
|
|
206
|
+
args = color.pattern_arguments(self)
|
|
207
|
+
add_call 'svg:render_pattern', type.to_sym, **args if args
|
|
194
208
|
when nil
|
|
195
209
|
nil
|
|
196
210
|
else
|
|
@@ -269,6 +283,14 @@ class Prawn::SVG::Elements::Base
|
|
|
269
283
|
['hidden', 'scroll'].include?(computed_properties.overflow)
|
|
270
284
|
end
|
|
271
285
|
|
|
286
|
+
def transformable?
|
|
287
|
+
true
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def add_to_elements_by_id?
|
|
291
|
+
true
|
|
292
|
+
end
|
|
293
|
+
|
|
272
294
|
def stroke_width
|
|
273
295
|
if computed_properties.stroke.none?
|
|
274
296
|
0
|
|
@@ -282,4 +304,9 @@ class Prawn::SVG::Elements::Base
|
|
|
282
304
|
document.element_styles[new_source] = document.element_styles[source]
|
|
283
305
|
new_source
|
|
284
306
|
end
|
|
307
|
+
|
|
308
|
+
def set_display_none
|
|
309
|
+
properties.display = 'none'
|
|
310
|
+
computed_properties.display = 'none'
|
|
311
|
+
end
|
|
285
312
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Prawn::SVG::Elements::BboxScaling
|
|
2
|
+
private
|
|
3
|
+
|
|
4
|
+
def build_object_bounding_box_calls(bbox, source_element = source)
|
|
5
|
+
bbox_left = bbox[0]
|
|
6
|
+
bbox_top = bbox[1]
|
|
7
|
+
bbox_right = bbox[2]
|
|
8
|
+
bbox_bottom = bbox[3]
|
|
9
|
+
bbox_w = bbox_right - bbox_left
|
|
10
|
+
bbox_h = bbox_top - bbox_bottom
|
|
11
|
+
|
|
12
|
+
unit_sizing = Prawn::SVG::Calculators::DocumentSizing.new([1, 1])
|
|
13
|
+
unit_sizing.document_width = 1
|
|
14
|
+
unit_sizing.document_height = 1
|
|
15
|
+
unit_sizing.calculate
|
|
16
|
+
|
|
17
|
+
result_calls = document.with_sizing(unit_sizing) do
|
|
18
|
+
new_state = state.dup
|
|
19
|
+
new_state.viewport_sizing = unit_sizing
|
|
20
|
+
new_state.inside_use = true
|
|
21
|
+
|
|
22
|
+
container = Prawn::SVG::Elements::Container.new(document, source_element, [], new_state)
|
|
23
|
+
container.process
|
|
24
|
+
|
|
25
|
+
container.base_calls
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
scale_calls_to_bbox(duplicate_calls(result_calls), bbox_left, bbox_top, bbox_w, bbox_h)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def scale_calls_to_bbox(calls, bbox_left, bbox_top, bbox_w, bbox_h)
|
|
32
|
+
calls.map do |name, args, kwargs, children|
|
|
33
|
+
new_args = case name
|
|
34
|
+
when 'rectangle'
|
|
35
|
+
point, width, height = args
|
|
36
|
+
[scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h), width * bbox_w, height * bbox_h]
|
|
37
|
+
when 'rounded_rectangle'
|
|
38
|
+
point, width, height, radius = args
|
|
39
|
+
[scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h), width * bbox_w, height * bbox_h, radius * bbox_w]
|
|
40
|
+
when 'move_to', 'line_to'
|
|
41
|
+
[scale_point(args[0], bbox_left, bbox_top, bbox_w, bbox_h)]
|
|
42
|
+
when 'circle'
|
|
43
|
+
point, radius = args
|
|
44
|
+
scaled_point = scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h)
|
|
45
|
+
if bbox_w == bbox_h
|
|
46
|
+
[scaled_point, radius * bbox_w]
|
|
47
|
+
else
|
|
48
|
+
new_children = children.any? ? scale_calls_to_bbox(children, bbox_left, bbox_top, bbox_w, bbox_h) : children
|
|
49
|
+
next ['ellipse', [scaled_point, radius * bbox_w, radius * bbox_h], kwargs, new_children]
|
|
50
|
+
end
|
|
51
|
+
when 'ellipse'
|
|
52
|
+
point, rx, ry = args
|
|
53
|
+
[scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h), rx * bbox_w, ry * bbox_h]
|
|
54
|
+
when 'curve_to'
|
|
55
|
+
dest = scale_point(args[0], bbox_left, bbox_top, bbox_w, bbox_h)
|
|
56
|
+
[dest]
|
|
57
|
+
else
|
|
58
|
+
args
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
new_kwargs = if name == 'curve_to' && kwargs[:bounds]
|
|
62
|
+
b = kwargs[:bounds]
|
|
63
|
+
{ bounds: b.map { |p| scale_point(p, bbox_left, bbox_top, bbox_w, bbox_h) } }
|
|
64
|
+
else
|
|
65
|
+
kwargs
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
new_children = children.any? ? scale_calls_to_bbox(children, bbox_left, bbox_top, bbox_w, bbox_h) : children
|
|
69
|
+
[name, new_args, new_kwargs, new_children]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h)
|
|
74
|
+
x = bbox_left + (point[0] * bbox_w)
|
|
75
|
+
y = bbox_top - ((1.0 - point[1]) * bbox_h)
|
|
76
|
+
[x, y]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
class Prawn::SVG::Elements::ClipPath < Prawn::SVG::Elements::Base
|
|
2
2
|
def parse
|
|
3
3
|
state.inside_clip_path = true
|
|
4
|
-
|
|
5
|
-
computed_properties.display = 'none'
|
|
4
|
+
set_display_none
|
|
6
5
|
end
|
|
7
6
|
|
|
8
7
|
def container?
|
|
9
8
|
true
|
|
10
9
|
end
|
|
10
|
+
|
|
11
|
+
def build_clip_calls(element)
|
|
12
|
+
units = attributes['clipPathUnits'] || 'userSpaceOnUse'
|
|
13
|
+
|
|
14
|
+
if units == 'objectBoundingBox'
|
|
15
|
+
bbox = element.bounding_box
|
|
16
|
+
if bbox.nil?
|
|
17
|
+
document.warnings << 'clipPath with clipPathUnits="objectBoundingBox" requires element to have a bounding box'
|
|
18
|
+
return nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
build_object_bounding_box_calls(bbox)
|
|
22
|
+
else
|
|
23
|
+
duplicate_calls(base_calls)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
11
26
|
end
|
|
@@ -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
|
|
@@ -181,23 +181,4 @@ class Prawn::SVG::Elements::Gradient < Prawn::SVG::Elements::Base
|
|
|
181
181
|
end
|
|
182
182
|
end
|
|
183
183
|
end
|
|
184
|
-
|
|
185
|
-
def percentage_or_proportion(string, default = 0)
|
|
186
|
-
string = string.to_s.strip
|
|
187
|
-
percentage = false
|
|
188
|
-
|
|
189
|
-
if string[-1] == '%'
|
|
190
|
-
percentage = true
|
|
191
|
-
string = string[0..-2]
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
value = Float(string, exception: false)
|
|
195
|
-
return default unless value
|
|
196
|
-
|
|
197
|
-
if percentage
|
|
198
|
-
value / 100.0
|
|
199
|
-
else
|
|
200
|
-
value
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
184
|
end
|