prawn-svg 0.35.1 → 0.36.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +3 -5
  3. data/README.md +4 -7
  4. data/lib/prawn/svg/attributes/opacity.rb +23 -8
  5. data/lib/prawn/svg/attributes/stroke.rb +7 -9
  6. data/lib/prawn/svg/attributes/transform.rb +1 -1
  7. data/lib/prawn/svg/calculators/pixels.rb +9 -22
  8. data/lib/prawn/svg/color.rb +58 -59
  9. data/lib/prawn/svg/css/font_parser.rb +46 -0
  10. data/lib/prawn/svg/document.rb +5 -1
  11. data/lib/prawn/svg/elements/base.rb +22 -30
  12. data/lib/prawn/svg/elements/gradient.rb +99 -74
  13. data/lib/prawn/svg/elements/line.rb +1 -1
  14. data/lib/prawn/svg/elements/marker.rb +2 -0
  15. data/lib/prawn/svg/elements/root.rb +1 -1
  16. data/lib/prawn/svg/elements/text_component.rb +3 -3
  17. data/lib/prawn/svg/funciri.rb +14 -0
  18. data/lib/prawn/svg/gradient_renderer.rb +313 -0
  19. data/lib/prawn/svg/length.rb +43 -0
  20. data/lib/prawn/svg/paint.rb +67 -0
  21. data/lib/prawn/svg/percentage.rb +24 -0
  22. data/lib/prawn/svg/properties.rb +208 -104
  23. data/lib/prawn/svg/renderer.rb +5 -0
  24. data/lib/prawn/svg/state.rb +5 -3
  25. data/lib/prawn/svg/transform_parser.rb +19 -13
  26. data/lib/prawn/svg/transform_utils.rb +37 -0
  27. data/lib/prawn/svg/version.rb +1 -1
  28. data/lib/prawn-svg.rb +7 -3
  29. data/prawn-svg.gemspec +4 -4
  30. data/spec/prawn/svg/attributes/opacity_spec.rb +27 -20
  31. data/spec/prawn/svg/attributes/transform_spec.rb +5 -2
  32. data/spec/prawn/svg/calculators/pixels_spec.rb +1 -2
  33. data/spec/prawn/svg/color_spec.rb +22 -52
  34. data/spec/prawn/svg/elements/base_spec.rb +9 -10
  35. data/spec/prawn/svg/elements/gradient_spec.rb +92 -36
  36. data/spec/prawn/svg/elements/marker_spec.rb +13 -15
  37. data/spec/prawn/svg/funciri_spec.rb +59 -0
  38. data/spec/prawn/svg/length_spec.rb +89 -0
  39. data/spec/prawn/svg/paint_spec.rb +96 -0
  40. data/spec/prawn/svg/pathable_spec.rb +3 -3
  41. data/spec/prawn/svg/pdfmatrix_spec.rb +60 -0
  42. data/spec/prawn/svg/percentage_spec.rb +60 -0
  43. data/spec/prawn/svg/properties_spec.rb +124 -107
  44. data/spec/prawn/svg/transform_parser_spec.rb +13 -13
  45. data/spec/sample_svg/gradient_stress_test.svg +115 -0
  46. metadata +17 -5
  47. data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +0 -23
@@ -0,0 +1,67 @@
1
+ module Prawn::SVG
2
+ Paint = Struct.new(:color, :url) do
3
+ class << self
4
+ def none
5
+ new(:none, nil)
6
+ end
7
+
8
+ def black
9
+ new(Color.black, nil)
10
+ end
11
+
12
+ def parse(value)
13
+ case CSS::ValuesParser.parse(value)
14
+ in [['url', [url]]]
15
+ # If there is no fallback color, and the URL is unresolvable, the spec says that the document is in error.
16
+ # Chrome appears to treat this the same as an explicit 'none', so we do the same.
17
+ new(:none, url)
18
+ in [['url', [url]], keyword_or_color]
19
+ parse_keyword_or_color(keyword_or_color, url)
20
+ in [['url', [url]], keyword_or_color, ['icc-color', _args]] # rubocop:disable Lint/DuplicateBranch
21
+ parse_keyword_or_color(keyword_or_color, url)
22
+ in [keyword_or_color, ['icc-color', _args]]
23
+ parse_keyword_or_color(keyword_or_color, nil)
24
+ in [keyword_or_color] # rubocop:disable Lint/DuplicateBranch
25
+ parse_keyword_or_color(keyword_or_color, nil)
26
+ else
27
+ nil
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def parse_keyword_or_color(value, url)
34
+ if value.is_a?(String)
35
+ keyword = value.downcase
36
+ return new(keyword.to_sym, url) if ['none', 'currentcolor'].include?(keyword)
37
+ end
38
+
39
+ color = Color.parse(value)
40
+ new(color, url) if color
41
+ end
42
+ end
43
+
44
+ def none?
45
+ color == :none && (url.nil? || !!@unresolved_url)
46
+ end
47
+
48
+ def resolve(gradients, current_color, color_mode)
49
+ if url
50
+ if url[0] == '#' && gradients && (gradient = gradients[url[1..]])
51
+ return gradient
52
+ else
53
+ @unresolved_url = true
54
+ end
55
+ end
56
+
57
+ case color
58
+ when :currentcolor
59
+ current_color
60
+ when :none
61
+ nil
62
+ else
63
+ color_mode == :cmyk ? color.to_cmyk : color
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,24 @@
1
+ Prawn::SVG::Percentage = Struct.new(:value)
2
+
3
+ class Prawn::SVG::Percentage
4
+ REGEXP = /\A([+-]?\d*(?:\.\d+)?)%\z/i.freeze
5
+
6
+ def self.parse(value, positive_only: false)
7
+ if (matches = value.match(REGEXP))
8
+ number = Float(matches[1], exception: false)
9
+ new(number) if number && (!positive_only || number >= 0)
10
+ end
11
+ end
12
+
13
+ def to_f
14
+ value
15
+ end
16
+
17
+ def to_factor
18
+ value / 100.0
19
+ end
20
+
21
+ def to_pixels(axis_length, _font_size)
22
+ value * axis_length / 100.0
23
+ end
24
+ end
@@ -1,127 +1,231 @@
1
- class Prawn::SVG::Properties
2
- Config = Struct.new(:default, :inheritable?, :keywords, :keyword_restricted?, :attr, :ivar)
3
-
4
- EM = 16
5
- FONT_SIZES = {
6
- 'xx-small' => EM / 4,
7
- 'x-small' => EM / 4 * 2,
8
- 'small' => EM / 4 * 3,
9
- 'medium' => EM / 4 * 4,
10
- 'large' => EM / 4 * 5,
11
- 'x-large' => EM / 4 * 6,
12
- 'xx-large' => EM / 4 * 7
13
- }.freeze
14
-
15
- PROPERTIES = {
16
- 'clip-path' => Config.new('none', false, %w[inherit none]),
17
- 'color' => Config.new('', true),
18
- 'display' => Config.new('inline', false, %w[inherit inline none], true),
19
- 'fill' => Config.new('black', true, %w[inherit none currentColor]),
20
- 'fill-opacity' => Config.new('1', true),
21
- 'fill-rule' => Config.new('nonzero', true, %w[inherit nonzero evenodd]),
22
- 'font-family' => Config.new('sans-serif', true),
23
- 'font-size' => Config.new('medium', true,
24
- %w[inherit xx-small x-small small medium large x-large xx-large larger smaller]),
25
- 'font-style' => Config.new('normal', true, %w[inherit normal italic oblique], true),
26
- 'font-variant' => Config.new('normal', true, %w[inherit normal small-caps], true),
27
- 'font-weight' => Config.new('normal', true, %w[inherit normal bold 100 200 300 400 500 600 700 800 900], true), # bolder/lighter not supported
28
- 'letter-spacing' => Config.new('normal', true, %w[inherit normal]),
29
- 'marker-end' => Config.new('none', true, %w[inherit none]),
30
- 'marker-mid' => Config.new('none', true, %w[inherit none]),
31
- 'marker-start' => Config.new('none', true, %w[inherit none]),
32
- 'opacity' => Config.new('1', false),
33
- 'overflow' => Config.new('visible', false, %w[inherit visible hidden scroll auto], true),
34
- 'stop-color' => Config.new('black', false, %w[inherit none currentColor]),
35
- 'stroke' => Config.new('none', true, %w[inherit none currentColor]),
36
- 'stroke-dasharray' => Config.new('none', true, %w[inherit none]),
37
- 'stroke-linecap' => Config.new('butt', true, %w[inherit butt round square], true),
38
- 'stroke-linejoin' => Config.new('miter', true, %w[inherit miter round bevel], true),
39
- 'stroke-opacity' => Config.new('1', true),
40
- 'stroke-width' => Config.new('1', true),
41
- 'text-anchor' => Config.new('start', true, %w[inherit start middle end], true),
42
- 'text-decoration' => Config.new('none', true, %w[inherit none underline], true),
43
- 'dominant-baseline' => Config.new('auto', true, %w[inherit auto middle], true)
44
- }.freeze
45
-
46
- PROPERTIES.each do |name, value|
47
- value.attr = name.gsub('-', '_')
48
- value.ivar = "@#{value.attr}"
49
- end
1
+ module Prawn::SVG
2
+ class Properties
3
+ Config = Struct.new(:default, :inheritable?, :valid_values, :attr, :ivar)
4
+
5
+ EM = 16
6
+ FONT_SIZES = {
7
+ 'xx-small' => EM / 4,
8
+ 'x-small' => EM / 4 * 2,
9
+ 'small' => EM / 4 * 3,
10
+ 'medium' => EM / 4 * 4,
11
+ 'large' => EM / 4 * 5,
12
+ 'x-large' => EM / 4 * 6,
13
+ 'xx-large' => EM / 4 * 7
14
+ }.freeze
15
+
16
+ PROPERTIES = {
17
+ 'clip-path' => Config.new('none', false, ['none', :funciri]),
18
+ 'color' => Config.new(Color.black, true, [:color]),
19
+ 'display' => Config.new('inline', false, %w[inline none]),
20
+ 'dominant-baseline' => Config.new('auto', true, %w[auto middle]),
21
+ 'fill' => Config.new(Paint.black, true, [:paint]),
22
+ 'fill-opacity' => Config.new(1.0, true, [:number]),
23
+ 'fill-rule' => Config.new('nonzero', true, %w[nonzero evenodd]),
24
+ 'font-family' => Config.new('sans-serif', true, [:any]),
25
+ # Only the computed (numeric) value of font-size is inherited, not the value itself
26
+ 'font-size' => Config.new(nil, false,
27
+ [:positive_length, :positive_percentage, 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'larger', 'smaller']),
28
+ 'font-style' => Config.new('normal', true, %w[normal italic oblique]),
29
+ 'font-variant' => Config.new('normal', true, %w[normal small-caps]),
30
+ 'font-weight' => Config.new('normal', true, %w[normal bold 100 200 300 400 500 600 700 800 900]), # bolder/lighter not supported
31
+ 'letter-spacing' => Config.new('normal', true, [:length, 'normal']),
32
+ 'marker-end' => Config.new('none', true, [:funciri, 'none']),
33
+ 'marker-mid' => Config.new('none', true, [:funciri, 'none']),
34
+ 'marker-start' => Config.new('none', true, [:funciri, 'none']),
35
+ 'opacity' => Config.new(1.0, false, [:number]),
36
+ 'overflow' => Config.new('visible', false, %w[visible hidden scroll auto]),
37
+ 'stop-color' => Config.new(Color.black, false, [:color_with_icc, 'currentcolor']),
38
+ 'stop-opacity' => Config.new(1.0, false, [:number]),
39
+ 'stroke' => Config.new(Paint.none, true, [:paint]),
40
+ 'stroke-dasharray' => Config.new('none', true, [:dasharray, 'none']),
41
+ 'stroke-linecap' => Config.new('butt', true, %w[butt round square]),
42
+ 'stroke-linejoin' => Config.new('miter', true, %w[miter round bevel]),
43
+ 'stroke-opacity' => Config.new(1.0, true, [:number]),
44
+ 'stroke-width' => Config.new(1.0, true, [:positive_length, :positive_percentage]),
45
+ 'text-anchor' => Config.new('start', true, %w[start middle end]),
46
+ 'text-decoration' => Config.new('none', true, %w[none underline]),
47
+ 'visibility' => Config.new('visible', true, %w[visible hidden collapse])
48
+ }.freeze
49
+
50
+ PROPERTIES.each do |name, value|
51
+ value.attr = name.gsub('-', '_')
52
+ value.ivar = "@#{value.attr}"
53
+ end
50
54
 
51
- PROPERTY_CONFIGS = PROPERTIES.values
52
- NAMES = PROPERTIES.keys
53
- ATTR_NAMES = PROPERTIES.keys.map { |name| name.gsub('-', '_') }
55
+ PROPERTY_CONFIGS = PROPERTIES.values
56
+ NAMES = PROPERTIES.keys
57
+ ATTR_NAMES = PROPERTIES.keys.map { |name| name.gsub('-', '_') }
54
58
 
55
- attr_accessor(*ATTR_NAMES)
59
+ attr_accessor(*ATTR_NAMES)
56
60
 
57
- def load_default_stylesheet
58
- PROPERTY_CONFIGS.each do |config|
59
- instance_variable_set(config.ivar, config.default)
61
+ def initialize
62
+ @numeric_font_size = EM
60
63
  end
61
64
 
62
- self
63
- end
65
+ def load_default_stylesheet
66
+ PROPERTY_CONFIGS.each do |config|
67
+ instance_variable_set(config.ivar, config.default)
68
+ end
64
69
 
65
- def set(name, value)
66
- if (config = PROPERTIES[name.to_s.downcase])
67
- value = value.strip
68
- keyword = value.downcase
69
- keywords = config.keywords || ['inherit']
70
+ self
71
+ end
70
72
 
71
- if keywords.include?(keyword)
72
- value = keyword
73
- elsif config.keyword_restricted?
74
- value = config.default
73
+ def set(name, value)
74
+ name = name.to_s.downcase
75
+ if (config = PROPERTIES[name])
76
+ if (value = parse_value(config, value.strip))
77
+ instance_variable_set(config.ivar, value)
78
+ end
79
+ elsif name == 'font'
80
+ apply_font_shorthand(value)
75
81
  end
82
+ end
76
83
 
77
- instance_variable_set(config.ivar, value)
84
+ def numeric_font_size
85
+ @numeric_font_size or raise 'numeric_font_size not set; this is only present in computed properties'
86
+ end
87
+
88
+ def to_h
89
+ PROPERTIES.each.with_object({}) do |(name, config), result|
90
+ result[name] = instance_variable_get(config.ivar)
91
+ end
78
92
  end
79
- end
80
93
 
81
- def to_h
82
- PROPERTIES.each.with_object({}) do |(name, config), result|
83
- result[name] = instance_variable_get(config.ivar)
94
+ def load_hash(hash)
95
+ hash.each { |name, value| set(name, value) if value }
84
96
  end
85
- end
86
97
 
87
- def load_hash(hash)
88
- hash.each { |name, value| set(name, value) if value }
89
- end
98
+ def compute_properties(other)
99
+ PROPERTY_CONFIGS.each do |config|
100
+ value = other.send(config.attr)
101
+
102
+ if value && value != 'inherit'
103
+ instance_variable_set(config.ivar, value)
90
104
 
91
- def compute_properties(other)
92
- PROPERTY_CONFIGS.each do |config|
93
- value = other.send(config.attr)
105
+ elsif value.nil? && !config.inheritable?
106
+ instance_variable_set(config.ivar, config.default)
107
+ end
108
+ end
94
109
 
95
- if value && value != 'inherit'
96
- value = compute_font_size_property(value).to_s if config.attr == 'font_size'
97
- instance_variable_set(config.ivar, value)
110
+ @numeric_font_size = calculate_numeric_font_size
111
+ nil
112
+ end
98
113
 
99
- elsif value.nil? && !config.inheritable?
100
- instance_variable_set(config.ivar, config.default)
114
+ private
115
+
116
+ def calculate_numeric_font_size
117
+ case font_size
118
+ when Length
119
+ font_size.to_pixels(nil, numeric_font_size)
120
+ when Percentage
121
+ font_size.to_factor * numeric_font_size
122
+ when Numeric
123
+ font_size
124
+ when 'larger'
125
+ numeric_font_size + 4
126
+ when 'smaller'
127
+ numeric_font_size - 4
128
+ when nil, 'inherit'
129
+ numeric_font_size
130
+ when String
131
+ FONT_SIZES[font_size] or raise "Unknown font size keyword: #{font_size}"
132
+ else
133
+ raise "Unknown font size property value: #{font_size.inspect}"
101
134
  end
102
135
  end
103
- end
104
136
 
105
- def numerical_font_size
106
- # px = pt for PDFs
107
- FONT_SIZES[font_size] || font_size.to_f
108
- end
137
+ def parse_value(config, value)
138
+ keyword = value.downcase
139
+
140
+ return 'inherit' if keyword == 'inherit'
141
+
142
+ config.valid_values.detect do |type|
143
+ result = parse_value_with_type(type, value, keyword)
144
+ break result if result
145
+ end
146
+ end
147
+
148
+ def parse_value_with_type(type, value, keyword)
149
+ case type
150
+ when String
151
+ keyword if type == keyword
152
+ when :color
153
+ values = Prawn::SVG::CSS::ValuesParser.parse(value)
154
+ Prawn::SVG::Color.parse(values[0]) if values.length == 1
155
+ when :color_with_icc
156
+ case Prawn::SVG::CSS::ValuesParser.parse(value)
157
+ in [other, ['icc-color', _args]]
158
+ Prawn::SVG::Color.parse(other)
159
+ in [other] # rubocop:disable Lint/DuplicateBranch
160
+ Prawn::SVG::Color.parse(other)
161
+ else
162
+ nil
163
+ end
164
+ when :paint
165
+ Paint.parse(value)
166
+ when :funciri
167
+ FuncIRI.parse(value)
168
+ when :dasharray
169
+ value.split(Prawn::SVG::Elements::COMMA_WSP_REGEXP).map do |value|
170
+ Length.parse(value) || Percentage.parse(value) || break
171
+ end
172
+ when :number
173
+ Float(value, exception: false)
174
+ when :length
175
+ Length.parse(value)
176
+ when :percentage
177
+ Percentage.parse(value)
178
+ when :positive_length
179
+ Length.parse(value, positive_only: true)
180
+ when :positive_percentage
181
+ Percentage.parse(value, positive_only: true)
182
+ when :any
183
+ value
184
+ else
185
+ raise "Unknown valid value type: #{type}"
186
+ end
187
+ end
188
+
189
+ FONT_KEYWORD_MAPPING = ['font-style', 'font-variant', 'font-weight'].each.with_object({}) do |property, result|
190
+ PROPERTIES[property].valid_values.each do |value|
191
+ result[value] = property
192
+ end
193
+ end
194
+
195
+ def apply_font_shorthand(value)
196
+ if value.strip.match(/\s/).nil?
197
+ case value.strip.downcase
198
+ when 'inherit'
199
+ load_hash('font-style' => 'inherit', 'font-variant' => 'inherit', 'font-weight' => 'inherit', 'font-size' => 'inherit', 'font-family' => 'inherit')
200
+ when 'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'
201
+ load_hash('font-style' => 'normal', 'font-variant' => 'normal', 'font-weight' => 'normal', 'font-size' => 'medium', 'font-family' => 'sans-serif')
202
+ end
203
+
204
+ return
205
+ end
206
+
207
+ properties = ['font-style', 'font-variant', 'font-weight'].each.with_object({}) do |property, result|
208
+ result[property] = PROPERTIES[property].default
209
+ end
210
+
211
+ values = CSS::FontParser.parse(value)
212
+ return if values.length < 2
213
+
214
+ properties['font-family'] = values.pop
215
+ font_size = values.pop.sub(%r{/.*}, '')
216
+
217
+ values.each do |keyword|
218
+ keyword = keyword.downcase
219
+ if (property = FONT_KEYWORD_MAPPING[keyword])
220
+ properties[property] = keyword
221
+ else
222
+ break
223
+ end
224
+ end or return
109
225
 
110
- private
111
-
112
- def compute_font_size_property(value)
113
- if value[-1] == '%'
114
- numerical_font_size * (value.to_f / 100.0)
115
- elsif value == 'larger'
116
- numerical_font_size + 4
117
- elsif value == 'smaller'
118
- numerical_font_size - 4
119
- elsif value.match(/(\d|\.)em\z/i)
120
- numerical_font_size * value.to_f
121
- elsif value.match(/(\d|\.)rem\z/i)
122
- value.to_f * EM
123
- else
124
- FONT_SIZES[value] || value.to_f
226
+ set('font-size', font_size) or return
227
+ load_hash(properties)
228
+ value
125
229
  end
126
230
  end
127
231
  end
@@ -216,6 +216,11 @@ module Prawn
216
216
  Renderer.new(prawn, sub_document, sub_options).draw
217
217
  document.warnings.concat(sub_document.warnings)
218
218
  yield
219
+
220
+ when 'svg:render_gradient'
221
+ type = arguments.first
222
+ GradientRenderer.new(prawn, type, **kwarguments).draw
223
+ yield
219
224
  end
220
225
  end
221
226
 
@@ -1,14 +1,16 @@
1
1
  class Prawn::SVG::State
2
2
  attr_accessor :text, :preserve_space,
3
- :fill_opacity, :stroke_opacity, :stroke_width,
3
+ :opacity, :last_fill_opacity, :last_stroke_opacity,
4
+ :stroke_width,
4
5
  :computed_properties,
5
6
  :viewport_sizing,
6
7
  :inside_use, :inside_clip_path
7
8
 
8
9
  def initialize
9
10
  @stroke_width = 1
10
- @fill_opacity = 1
11
- @stroke_opacity = 1
11
+ @opacity = 1
12
+ @last_fill_opacity = 1
13
+ @last_stroke_opacity = 1
12
14
  @computed_properties = Prawn::SVG::Properties.new.load_default_stylesheet
13
15
  end
14
16
 
@@ -1,32 +1,38 @@
1
1
  module Prawn::SVG::TransformParser
2
- def parse_transform_attribute(transform)
2
+ include Prawn::SVG::PDFMatrix
3
+
4
+ def parse_transform_attribute(transform, space: :pdf)
3
5
  matrix = Matrix.identity(3)
4
6
 
7
+ flip = space == :svg ? 1.0 : -1.0
8
+
5
9
  parse_css_method_calls(transform).each do |name, arguments|
6
10
  case name
7
11
  when 'translate'
8
12
  x, y = arguments
9
- matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
13
+ matrix *= translation_matrix(x_pixels(x.to_f), flip * y_pixels(y.to_f))
10
14
 
11
15
  when 'translateX'
12
16
  x = arguments.first
13
- matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, 0], [0, 0, 1]]
17
+ matrix *= translation_matrix(x_pixels(x.to_f), 0.0)
14
18
 
15
19
  when 'translateY'
16
20
  y = arguments.first
17
- matrix *= Matrix[[1, 0, 0], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
21
+ matrix *= translation_matrix(0.0, flip * y_pixels(y.to_f))
18
22
 
19
23
  when 'rotate'
20
24
  angle, x, y = arguments.collect(&:to_f)
21
25
  angle = angle * Math::PI / 180.0
22
26
 
27
+ rotation = rotation_matrix(angle, space: space)
28
+
23
29
  case arguments.length
24
30
  when 1
25
- matrix *= Matrix[[Math.cos(angle), Math.sin(angle), 0], [-Math.sin(angle), Math.cos(angle), 0], [0, 0, 1]]
31
+ matrix *= rotation
26
32
  when 3
27
- matrix *= Matrix[[1, 0, x_pixels(x.to_f)], [0, 1, -y_pixels(y.to_f)], [0, 0, 1]]
28
- matrix *= Matrix[[Math.cos(angle), Math.sin(angle), 0], [-Math.sin(angle), Math.cos(angle), 0], [0, 0, 1]]
29
- matrix *= Matrix[[1, 0, -x_pixels(x.to_f)], [0, 1, y_pixels(y.to_f)], [0, 0, 1]]
33
+ matrix *= translation_matrix(x_pixels(x.to_f), flip * y_pixels(y.to_f))
34
+ matrix *= rotation
35
+ matrix *= translation_matrix(-x_pixels(x.to_f), -flip * y_pixels(y.to_f))
30
36
  else
31
37
  warnings << "transform 'rotate' must have either one or three arguments"
32
38
  end
@@ -34,20 +40,20 @@ module Prawn::SVG::TransformParser
34
40
  when 'scale'
35
41
  x_scale = arguments[0].to_f
36
42
  y_scale = (arguments[1] || x_scale).to_f
37
- matrix *= Matrix[[x_scale, 0, 0], [0, y_scale, 0], [0, 0, 1]]
43
+ matrix *= scale_matrix(x_scale, y_scale)
38
44
 
39
45
  when 'skewX'
40
46
  angle = arguments[0].to_f * Math::PI / 180.0
41
- matrix *= Matrix[[1, -Math.tan(angle), 0], [0, 1, 0], [0, 0, 1]]
47
+ matrix *= Matrix[[1, flip * Math.tan(angle), 0], [0, 1, 0], [0, 0, 1]]
42
48
 
43
49
  when 'skewY'
44
50
  angle = arguments[0].to_f * Math::PI / 180.0
45
- matrix *= Matrix[[1, 0, 0], [-Math.tan(angle), 1, 0], [0, 0, 1]]
51
+ matrix *= Matrix[[1, 0, 0], [flip * Math.tan(angle), 1, 0], [0, 0, 1]]
46
52
 
47
53
  when 'matrix'
48
54
  if arguments.length == 6
49
55
  a, b, c, d, e, f = arguments.collect(&:to_f)
50
- matrix *= Matrix[[a, -c, e], [-b, d, -f], [0, 0, 1]]
56
+ matrix *= Matrix[[a, flip * c, e], [flip * b, d, flip * f], [0, 0, 1]]
51
57
  else
52
58
  warnings << "transform 'matrix' must have six arguments"
53
59
  end
@@ -57,7 +63,7 @@ module Prawn::SVG::TransformParser
57
63
  end
58
64
  end
59
65
 
60
- matrix.to_a[0..1].transpose.flatten
66
+ matrix
61
67
  end
62
68
 
63
69
  private
@@ -0,0 +1,37 @@
1
+ module Prawn::SVG::PDFMatrix
2
+ def load_matrix(matrix)
3
+ if matrix.is_a?(Matrix) && matrix.row_count == 3 && matrix.column_count == 3
4
+ matrix
5
+ elsif matrix.is_a?(Array) && matrix.length == 6
6
+ Matrix[
7
+ [matrix[0], matrix[2], matrix[4]],
8
+ [matrix[1], matrix[3], matrix[5]],
9
+ [0.0, 0.0, 1.0]
10
+ ]
11
+ else
12
+ raise ArgumentError, 'unexpected matrix format'
13
+ end
14
+ end
15
+
16
+ def matrix_for_pdf(matrix)
17
+ matrix.to_a[0..1].transpose.flatten
18
+ end
19
+
20
+ def translation_matrix(x = 0, y = 0)
21
+ Matrix[[1.0, 0.0, x.to_f], [0.0, 1.0, y.to_f], [0.0, 0.0, 1.0]]
22
+ end
23
+
24
+ def scale_matrix(x = 1, y = x)
25
+ Matrix[[x.to_f, 0.0, 0.0], [0.0, y.to_f, 0.0], [0.0, 0.0, 1.0]]
26
+ end
27
+
28
+ def rotation_matrix(angle, space: :pdf)
29
+ dir = space == :svg ? 1.0 : -1.0
30
+
31
+ Matrix[
32
+ [Math.cos(angle), -dir * Math.sin(angle), 0],
33
+ [dir * Math.sin(angle), Math.cos(angle), 0],
34
+ [0, 0, 1]
35
+ ]
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  module Prawn
2
2
  module SVG
3
- VERSION = '0.35.1'.freeze
3
+ VERSION = '0.36.0'.freeze
4
4
  end
5
5
  end
data/lib/prawn-svg.rb CHANGED
@@ -10,12 +10,17 @@ require 'prawn/svg/calculators/arc_to_bezier_curve'
10
10
  require 'prawn/svg/calculators/aspect_ratio'
11
11
  require 'prawn/svg/calculators/document_sizing'
12
12
  require 'prawn/svg/calculators/pixels'
13
+ require 'prawn/svg/transform_utils'
13
14
  require 'prawn/svg/transform_parser'
14
15
  require 'prawn/svg/url_loader'
15
16
  require 'prawn/svg/loaders/data'
16
17
  require 'prawn/svg/loaders/file'
17
18
  require 'prawn/svg/loaders/web'
18
19
  require 'prawn/svg/color'
20
+ require 'prawn/svg/funciri'
21
+ require 'prawn/svg/length'
22
+ require 'prawn/svg/paint'
23
+ require 'prawn/svg/percentage'
19
24
  require 'prawn/svg/attributes'
20
25
  require 'prawn/svg/properties'
21
26
  require 'prawn/svg/pathable'
@@ -23,6 +28,7 @@ require 'prawn/svg/elements'
23
28
  require 'prawn/svg/extension'
24
29
  require 'prawn/svg/renderer'
25
30
  require 'prawn/svg/interface'
31
+ require 'prawn/svg/css/font_parser'
26
32
  require 'prawn/svg/css/font_family_parser'
27
33
  require 'prawn/svg/css/selector_parser'
28
34
  require 'prawn/svg/css/stylesheets'
@@ -30,12 +36,10 @@ require 'prawn/svg/css/values_parser'
30
36
  require 'prawn/svg/ttf'
31
37
  require 'prawn/svg/font'
32
38
  require 'prawn/svg/gradients'
39
+ require 'prawn/svg/gradient_renderer'
33
40
  require 'prawn/svg/document'
34
41
  require 'prawn/svg/state'
35
42
 
36
- require 'prawn/svg/extensions/additional_gradient_transforms'
37
- Prawn::Document.prepend Prawn::SVG::Extensions::AdditionalGradientTransforms
38
-
39
43
  module Prawn
40
44
  Svg = SVG # backwards compatibility
41
45
  end
data/prawn-svg.gemspec CHANGED
@@ -19,8 +19,8 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.required_ruby_version = '>= 2.7.0'
21
21
 
22
- gem.add_runtime_dependency 'css_parser', '~> 1.6'
23
- gem.add_runtime_dependency 'matrix', '~> 0.4.2'
24
- gem.add_runtime_dependency 'prawn', '>= 0.11.1', '< 3'
25
- gem.add_runtime_dependency 'rexml', '>=3.2.0', '< 4'
22
+ gem.add_dependency 'css_parser', '~> 1.6'
23
+ gem.add_dependency 'matrix', '~> 0.4.2'
24
+ gem.add_dependency 'prawn', '>= 0.11.1', '< 3'
25
+ gem.add_dependency 'rexml', '>= 3.3.9', '< 4'
26
26
  end