prawnx 0.1.1

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 (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. metadata +251 -0
@@ -0,0 +1,329 @@
1
+ module Prawn::SVG
2
+ class Properties
3
+ Config = Struct.new(:default, :inheritable?, :valid_values, :attr, :ivar, :id)
4
+
5
+ EM = 16
6
+ STRETCH_ORDER = %w[
7
+ ultra-condensed extra-condensed condensed semi-condensed normal
8
+ semi-expanded expanded extra-expanded ultra-expanded
9
+ ].freeze
10
+
11
+ FONT_SIZES = {
12
+ 'xx-small' => EM / 4,
13
+ 'x-small' => EM / 4 * 2,
14
+ 'small' => EM / 4 * 3,
15
+ 'medium' => EM / 4 * 4,
16
+ 'large' => EM / 4 * 5,
17
+ 'x-large' => EM / 4 * 6,
18
+ 'xx-large' => EM / 4 * 7
19
+ }.freeze
20
+
21
+ PROPERTIES = {
22
+ 'alignment-baseline' => Config.new('auto', false,
23
+ %w[auto baseline before-edge text-before-edge middle central after-edge text-after-edge ideographic alphabetic hanging mathematical]),
24
+ 'baseline-shift' => Config.new('baseline', false, ['baseline', 'sub', 'super', :length, :percentage]),
25
+ 'clip-path' => Config.new('none', false, ['none', :funciri]),
26
+ 'clip-rule' => Config.new('nonzero', true, %w[nonzero evenodd]),
27
+ 'color' => Config.new(Color.black, true, [:color]),
28
+ 'display' => Config.new('inline', false, %w[inline none]),
29
+ 'dominant-baseline' => Config.new('auto', true, %w[auto middle central hanging alphabetic text-before-edge text-after-edge]),
30
+ 'fill' => Config.new(Paint.black, true, [:paint]),
31
+ 'fill-opacity' => Config.new(1.0, true, [:number]),
32
+ 'fill-rule' => Config.new('nonzero', true, %w[nonzero evenodd]),
33
+ 'font-family' => Config.new('sans-serif', true, [:any]),
34
+ # Only the computed (numeric) value of font-size is inherited, not the value itself
35
+ 'font-size' => Config.new(nil, false,
36
+ [:positive_length, :positive_percentage, 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'larger', 'smaller']),
37
+ 'font-stretch' => Config.new('normal', true, STRETCH_ORDER + %w[wider narrower]),
38
+ 'font-style' => Config.new('normal', true, %w[normal italic oblique]),
39
+ 'font-variant' => Config.new('normal', true, %w[normal small-caps]),
40
+ 'font-weight' => Config.new('normal', true, %w[normal bold bolder lighter 100 200 300 400 500 600 700 800 900]),
41
+ # font-kerning is SVG 2 / CSS Fonts Level 3, supported here for forwards compatibility
42
+ # since modern browsers have deprecated the SVG 1.1 kerning property.
43
+ 'font-kerning' => Config.new('auto', true, %w[auto normal none]),
44
+ 'kerning' => Config.new('auto', true, [:length, 'auto']),
45
+ 'letter-spacing' => Config.new('normal', true, [:length, 'normal']),
46
+ 'word-spacing' => Config.new('normal', true, [:length, 'normal']),
47
+ 'marker-end' => Config.new('none', true, [:funciri, 'none']),
48
+ 'marker-mid' => Config.new('none', true, [:funciri, 'none']),
49
+ 'marker-start' => Config.new('none', true, [:funciri, 'none']),
50
+ 'mask' => Config.new('none', false, [:funciri, 'none']),
51
+ 'opacity' => Config.new(1.0, false, [:number]),
52
+ 'overflow' => Config.new('visible', false, %w[visible hidden scroll auto]),
53
+ 'stop-color' => Config.new(Color.black, false, [:color_with_icc, 'currentcolor']),
54
+ 'stop-opacity' => Config.new(1.0, false, [:number]),
55
+ 'stroke' => Config.new(Paint.none, true, [:paint]),
56
+ 'stroke-dasharray' => Config.new('none', true, [:dasharray, 'none']),
57
+ 'stroke-dashoffset' => Config.new(0, true, [:length, :percentage]),
58
+ 'stroke-linecap' => Config.new('butt', true, %w[butt round square]),
59
+ 'stroke-linejoin' => Config.new('miter', true, %w[miter round bevel]),
60
+ 'stroke-miterlimit' => Config.new(4.0, true, [:positive_number]),
61
+ 'stroke-opacity' => Config.new(1.0, true, [:number]),
62
+ 'stroke-width' => Config.new(1.0, true, [:positive_length, :positive_percentage]),
63
+ 'text-anchor' => Config.new('start', true, %w[start middle end]),
64
+ 'text-decoration' => Config.new('none', true, [:text_decoration]),
65
+ 'visibility' => Config.new('visible', true, %w[visible hidden collapse]),
66
+ 'writing-mode' => Config.new('horizontal-tb', true, %w[horizontal-tb vertical-rl vertical-lr])
67
+ }.freeze
68
+
69
+ PROPERTIES.each do |name, value|
70
+ value.attr = name.gsub('-', '_')
71
+ value.id = value.attr.to_sym
72
+ value.ivar = "@#{value.attr}"
73
+ end
74
+
75
+ PROPERTY_CONFIGS = PROPERTIES.values
76
+ NAMES = PROPERTIES.keys
77
+ ATTR_NAMES = PROPERTIES.keys.map { |name| name.gsub('-', '_') }
78
+
79
+ attr_accessor(*ATTR_NAMES)
80
+ attr_reader :important_ids
81
+
82
+ def initialize
83
+ @numeric_font_size = EM
84
+ @important_ids = []
85
+ end
86
+
87
+ def load_default_stylesheet
88
+ PROPERTY_CONFIGS.each do |config|
89
+ instance_variable_set(config.ivar, config.default)
90
+ end
91
+
92
+ self
93
+ end
94
+
95
+ def set(name, value, important: false)
96
+ name = name.to_s.downcase
97
+ if (config = PROPERTIES[name])
98
+ if (value = parse_value(config, value.strip)) && (important || !@important_ids.include?(config.id))
99
+ @important_ids << config.id if important
100
+ instance_variable_set(config.ivar, value)
101
+ end
102
+ elsif name == 'font'
103
+ apply_font_shorthand(value)
104
+ elsif name == 'marker'
105
+ apply_marker_shorthand(value, important: important)
106
+ end
107
+ end
108
+
109
+ def numeric_font_size
110
+ @numeric_font_size or raise 'numeric_font_size not set; this is only present in computed properties'
111
+ end
112
+
113
+ def to_h
114
+ PROPERTIES.each.with_object({}) do |(name, config), result|
115
+ result[name] = instance_variable_get(config.ivar)
116
+ end
117
+ end
118
+
119
+ def load_hash(hash)
120
+ hash.each { |name, value| set(name, value) if value }
121
+ end
122
+
123
+ def compute_properties(other)
124
+ parent_font_stretch = @font_stretch_before_relative || font_stretch
125
+ parent_font_weight = font_weight
126
+
127
+ PROPERTY_CONFIGS.each do |config|
128
+ value = other.send(config.attr)
129
+
130
+ if value && value != 'inherit' && (!@important_ids.include?(config.id) || other.important_ids.include?(config.id))
131
+ instance_variable_set(config.ivar, value)
132
+
133
+ elsif value.nil? && !config.inheritable?
134
+ instance_variable_set(config.ivar, config.default)
135
+ end
136
+ end
137
+
138
+ if ['wider', 'narrower'].include?(font_stretch)
139
+ @font_stretch_before_relative ||= parent_font_stretch
140
+ else
141
+ @font_stretch_before_relative = nil
142
+ end
143
+
144
+ @font_stretch = resolve_font_stretch(parent_font_stretch)
145
+ @font_weight = resolve_font_weight(parent_font_weight)
146
+ @important_ids += other.important_ids
147
+ @numeric_font_size = calculate_numeric_font_size
148
+ nil
149
+ end
150
+
151
+ private
152
+
153
+ BOLDER_MAP = {
154
+ '100' => '400', '200' => '400', '300' => '400',
155
+ '400' => '700', '500' => '700',
156
+ '600' => '900', '700' => '900', '800' => '900', '900' => '900'
157
+ }.freeze
158
+
159
+ LIGHTER_MAP = {
160
+ '100' => '100', '200' => '100', '300' => '100',
161
+ '400' => '100', '500' => '100',
162
+ '600' => '400', '700' => '400',
163
+ '800' => '700', '900' => '700'
164
+ }.freeze
165
+
166
+ FONT_WEIGHT_TO_NUMERIC = {
167
+ 'normal' => '400', 'bold' => '700'
168
+ }.freeze
169
+
170
+ def resolve_font_weight(parent_weight)
171
+ case font_weight
172
+ when 'bolder'
173
+ numeric = FONT_WEIGHT_TO_NUMERIC[parent_weight] || parent_weight || '400'
174
+ BOLDER_MAP[numeric] || '900'
175
+ when 'lighter'
176
+ numeric = FONT_WEIGHT_TO_NUMERIC[parent_weight] || parent_weight || '400'
177
+ LIGHTER_MAP[numeric] || '100'
178
+ else
179
+ font_weight
180
+ end
181
+ end
182
+
183
+ def resolve_font_stretch(parent_stretch)
184
+ case font_stretch
185
+ when 'wider'
186
+ index = STRETCH_ORDER.index(parent_stretch || 'normal') || 4
187
+ STRETCH_ORDER[[index + 1, STRETCH_ORDER.length - 1].min]
188
+ when 'narrower'
189
+ index = STRETCH_ORDER.index(parent_stretch || 'normal') || 4
190
+ STRETCH_ORDER[[index - 1, 0].max]
191
+ else
192
+ font_stretch
193
+ end
194
+ end
195
+
196
+ def calculate_numeric_font_size
197
+ case font_size
198
+ when Length
199
+ font_size.to_pixels(nil, numeric_font_size)
200
+ when Percentage
201
+ font_size.to_factor * numeric_font_size
202
+ when Numeric
203
+ font_size
204
+ when 'larger'
205
+ numeric_font_size + 4
206
+ when 'smaller'
207
+ numeric_font_size - 4
208
+ when nil, 'inherit'
209
+ numeric_font_size
210
+ when String
211
+ FONT_SIZES[font_size] or raise "Unknown font size keyword: #{font_size}"
212
+ else
213
+ raise "Unknown font size property value: #{font_size.inspect}"
214
+ end
215
+ end
216
+
217
+ def parse_value(config, value)
218
+ keyword = value.downcase
219
+
220
+ return 'inherit' if keyword == 'inherit'
221
+
222
+ config.valid_values.detect do |type|
223
+ result = parse_value_with_type(type, value, keyword)
224
+ break result if result
225
+ end
226
+ end
227
+
228
+ def parse_value_with_type(type, value, keyword)
229
+ case type
230
+ when String
231
+ keyword if type == keyword
232
+ when :color
233
+ values = Prawn::SVG::CSS::ValuesParser.parse(value)
234
+ Prawn::SVG::Color.parse(values[0]) if values.length == 1
235
+ when :color_with_icc
236
+ case Prawn::SVG::CSS::ValuesParser.parse(value)
237
+ in [other, ['icc-color', _args]]
238
+ Prawn::SVG::Color.parse(other)
239
+ in [other] # rubocop:disable Lint/DuplicateBranch
240
+ Prawn::SVG::Color.parse(other)
241
+ else
242
+ nil
243
+ end
244
+ when :paint
245
+ Paint.parse(value)
246
+ when :funciri
247
+ FuncIRI.parse(value)
248
+ when :dasharray
249
+ value.split(Prawn::SVG::Elements::COMMA_WSP_REGEXP).map do |value|
250
+ Length.parse(value) || Percentage.parse(value) || break
251
+ end
252
+ when :number
253
+ Float(value, exception: false)
254
+ when :length
255
+ Length.parse(value)
256
+ when :percentage
257
+ Percentage.parse(value)
258
+ when :positive_number
259
+ value = Float(value, exception: false)
260
+ value if value && value >= 1
261
+ when :positive_length
262
+ Length.parse(value, positive_only: true)
263
+ when :positive_percentage
264
+ Percentage.parse(value, positive_only: true)
265
+ when :text_decoration
266
+ parts = keyword.split
267
+ valid = %w[none underline overline line-through]
268
+ return nil unless parts.all? { |p| valid.include?(p) }
269
+ return 'none' if parts.include?('none')
270
+
271
+ parts.uniq.sort.join(' ')
272
+ when :any
273
+ value
274
+ else
275
+ raise "Unknown valid value type: #{type}"
276
+ end
277
+ end
278
+
279
+ FONT_KEYWORD_MAPPING = ['font-style', 'font-variant', 'font-weight'].each.with_object({}) do |property, result|
280
+ PROPERTIES[property].valid_values.each do |value|
281
+ result[value] = property
282
+ end
283
+ end
284
+
285
+ def apply_font_shorthand(value)
286
+ if value.strip.match(/\s/).nil?
287
+ case value.strip.downcase
288
+ when 'inherit'
289
+ load_hash('font-style' => 'inherit', 'font-variant' => 'inherit', 'font-weight' => 'inherit',
290
+ 'font-stretch' => 'inherit', 'font-size' => 'inherit', 'font-family' => 'inherit')
291
+ when 'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'
292
+ load_hash('font-style' => 'normal', 'font-variant' => 'normal', 'font-weight' => 'normal',
293
+ 'font-stretch' => 'normal', 'font-size' => 'medium', 'font-family' => 'sans-serif')
294
+ end
295
+
296
+ return
297
+ end
298
+
299
+ properties = ['font-style', 'font-variant', 'font-weight', 'font-stretch'].each.with_object({}) do |property, result|
300
+ result[property] = PROPERTIES[property].default
301
+ end
302
+
303
+ values = CSS::FontParser.parse(value)
304
+ return if values.length < 2
305
+
306
+ properties['font-family'] = values.pop
307
+ font_size = values.pop.sub(%r{/.*}, '')
308
+
309
+ values.each do |keyword|
310
+ keyword = keyword.downcase
311
+ if (property = FONT_KEYWORD_MAPPING[keyword])
312
+ properties[property] = keyword
313
+ else
314
+ break
315
+ end
316
+ end or return
317
+
318
+ set('font-size', font_size) or return
319
+ load_hash(properties)
320
+ value
321
+ end
322
+
323
+ def apply_marker_shorthand(value, important: false)
324
+ %w[marker-start marker-mid marker-end].each do |property|
325
+ set(property, value, important: important)
326
+ end
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,249 @@
1
+ module Prawn
2
+ module SVG
3
+ class Renderer
4
+ attr_reader :prawn, :document, :options
5
+
6
+ #
7
+ # Creates a Prawn::SVG object.
8
+ #
9
+ # +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object.
10
+ #
11
+ # See README.md for the options that can be passed to this method.
12
+ #
13
+ def initialize(prawn, document, options)
14
+ @prawn = prawn
15
+ @document = document
16
+ @options = options
17
+ end
18
+
19
+ #
20
+ # Draws the SVG to the Prawn::Document object.
21
+ #
22
+ def draw
23
+ if sizing.invalid?
24
+ document.warnings << 'Zero or negative sizing data means this SVG cannot be rendered'
25
+ return
26
+ end
27
+
28
+ document.warnings.clear
29
+
30
+ prawn.save_font do
31
+ prawn.bounding_box(position, width: sizing.output_width, height: sizing.output_height) do
32
+ prawn.save_graphics_state do
33
+ clip_rectangle 0, 0, sizing.output_width, sizing.output_height
34
+
35
+ calls = []
36
+ root_element = Prawn::SVG::Elements::Root.new(document, document.root, calls)
37
+ root_element.process
38
+
39
+ proc_creator(prawn, calls).call
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def sizing
46
+ document.sizing
47
+ end
48
+
49
+ def position
50
+ options[:at] || [x_based_on_requested_alignment, y_based_on_requested_alignment]
51
+ end
52
+
53
+ def render_calls(prawn, calls)
54
+ issue_prawn_command(prawn, calls)
55
+ end
56
+
57
+ private
58
+
59
+ def x_based_on_requested_alignment
60
+ case options[:position]
61
+ when :left, nil
62
+ 0
63
+ when :center, :centre
64
+ (sizing.bounds[0] - sizing.output_width) / 2.0
65
+ when :right
66
+ sizing.bounds[0] - sizing.output_width
67
+ when Numeric
68
+ options[:position]
69
+ else
70
+ raise ArgumentError, 'options[:position] must be one of nil, :left, :right, :center or a number'
71
+ end
72
+ end
73
+
74
+ def y_based_on_requested_alignment
75
+ case options[:vposition]
76
+ when nil
77
+ prawn.cursor
78
+ when :top
79
+ sizing.bounds[1]
80
+ when :center, :centre
81
+ sizing.bounds[1] - ((sizing.bounds[1] - sizing.output_height) / 2.0)
82
+ when :bottom
83
+ sizing.output_height
84
+ when Numeric
85
+ sizing.bounds[1] - options[:vposition]
86
+ else
87
+ raise ArgumentError, 'options[:vposition] must be one of nil, :top, :right, :bottom or a number'
88
+ end
89
+ end
90
+
91
+ def proc_creator(prawn, calls)
92
+ proc { issue_prawn_command(prawn, calls) }
93
+ end
94
+
95
+ def issue_prawn_command(prawn, calls)
96
+ calls.each do |call, arguments, kwarguments, children|
97
+ skip = false
98
+
99
+ rewrite_call_arguments(prawn, call, arguments, kwarguments, children) do
100
+ issue_prawn_command(prawn, children) if children.any?
101
+ skip = true
102
+ end
103
+
104
+ if skip
105
+ # the call has been overridden
106
+ elsif children.empty? && call != 'transparent' # some prawn calls complain if they aren't supplied a block
107
+ if kwarguments.empty?
108
+ prawn.send(call, *arguments)
109
+ else
110
+ prawn.send(call, *arguments, **kwarguments)
111
+ end
112
+ elsif kwarguments.empty?
113
+ prawn.send(call, *arguments, &proc_creator(prawn, children))
114
+ else
115
+ prawn.send(call, *arguments, **kwarguments, &proc_creator(prawn, children))
116
+ end
117
+ end
118
+ end
119
+
120
+ def rewrite_call_arguments(prawn, call, arguments, kwarguments, children)
121
+ case call
122
+ when 'svg:render'
123
+ element = arguments.first
124
+ raise ArgumentError, "Expected a Prawn::SVG::Elements::DirectRenderBase, got #{element.class}" unless element.is_a?(Prawn::SVG::Elements::DirectRenderBase)
125
+
126
+ begin
127
+ element.render(prawn, self)
128
+ rescue Prawn::SVG::Elements::Base::SkipElementQuietly
129
+ rescue Prawn::SVG::Elements::Base::SkipElementError => e
130
+ @document.warnings << e.message
131
+ end
132
+
133
+ yield
134
+
135
+ when 'svg:yield'
136
+ block = arguments.first
137
+ block.call
138
+ yield
139
+
140
+ when 'transformation_matrix'
141
+ left = prawn.bounds.absolute_left
142
+ top = prawn.bounds.absolute_top
143
+ arguments[4] += left - ((left * arguments[0]) + (top * arguments[2]))
144
+ arguments[5] += top - ((left * arguments[1]) + (top * arguments[3]))
145
+
146
+ when 'curve_to'
147
+ # Prawn appends the destination to bounds, mutating bounds.
148
+ # Preserve it for path replay by passing a .dup instead.
149
+ # See: https://github.com/prawnpdf/prawn/issues/1401
150
+ prawn.curve_to(arguments.first, **kwarguments.merge(bounds: kwarguments[:bounds].dup))
151
+ yield
152
+
153
+ when 'miter_limit'
154
+ prawn.add_content("#{arguments.first} M")
155
+ yield
156
+
157
+ when 'clip'
158
+ even_odd = kwarguments[:clip_rule] == :even_odd
159
+ prawn.add_content(even_odd ? 'W* n' : 'W n')
160
+ yield
161
+
162
+ when 'save'
163
+ prawn.save_graphics_state
164
+ yield
165
+
166
+ when 'restore'
167
+ prawn.restore_graphics_state
168
+ yield
169
+
170
+ when 'end_path'
171
+ yield
172
+ prawn.add_content 'n' # end path
173
+
174
+ when 'fill_and_stroke'
175
+ yield
176
+ # Using separate fill and stroke operations instead of the combined B operator.
177
+ # Chromium's PDF renderer (PDFium) doesn't correctly apply stroke alpha (CA) with
178
+ # the B operator when fill and stroke have different opacities.
179
+ # Also avoids Prawn's 'b' which includes closepath (h) that breaks dashes and line caps.
180
+ even_odd = kwarguments[:fill_rule] == :even_odd
181
+ prawn.add_content(even_odd ? 'f*' : 'f')
182
+ issue_prawn_command(prawn, children)
183
+ prawn.add_content('S')
184
+
185
+ when 'noop'
186
+ yield
187
+
188
+ when 'svg:render_sub_document'
189
+ sub_document = arguments.first
190
+ sub_options = inheritable_options.merge({ at: [0, 0] })
191
+
192
+ Renderer.new(prawn, sub_document, sub_options).draw
193
+ document.warnings.concat(sub_document.warnings)
194
+ yield
195
+
196
+ when 'soft_mask'
197
+ with_large_page_dimensions(prawn) do
198
+ prawn.soft_mask do
199
+ issue_prawn_command(prawn, children) if children.any?
200
+ end
201
+ end
202
+ # Clear children so issue_prawn_command doesn't re-process them
203
+ # after soft_mask has already rendered them above.
204
+ children.clear
205
+ yield
206
+
207
+ when 'svg:render_gradient'
208
+ type = arguments.first
209
+ GradientRenderer.new(prawn, type, **kwarguments).draw
210
+ yield
211
+
212
+ when 'svg:render_pattern'
213
+ type = arguments[0]
214
+ PatternRenderer.new(prawn, type, self, **kwarguments).draw
215
+ yield
216
+
217
+ when 'svg:add_link'
218
+ LinkRenderer.new(*arguments).render(prawn)
219
+ yield
220
+ end
221
+ end
222
+
223
+ # Prawn's soft_mask hardcodes BBox to page.dimensions, which causes
224
+ # Chrome and Firefox to clip mask content that extends beyond the page.
225
+ # Temporarily override dimensions to use a large BBox.
226
+ def with_large_page_dimensions(prawn)
227
+ page = prawn.state.page
228
+ large_bbox = [-10_000, -10_000, 10_000, 10_000]
229
+ page.define_singleton_method(:dimensions) { large_bbox }
230
+ yield
231
+ ensure
232
+ page.singleton_class.remove_method(:dimensions)
233
+ end
234
+
235
+ def inheritable_options
236
+ (options || {}).slice(Prawn::SVG::Interface::INHERITABLE_OPTIONS)
237
+ end
238
+
239
+ def clip_rectangle(x, y, width, height)
240
+ prawn.move_to x, y
241
+ prawn.line_to x + width, y
242
+ prawn.line_to x + width, y + height
243
+ prawn.line_to x, y + height
244
+ prawn.close_path
245
+ prawn.add_content 'W n' # clip to path
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,24 @@
1
+ class Prawn::SVG::State
2
+ attr_accessor :text, :preserve_space,
3
+ :opacity, :last_fill_opacity, :last_stroke_opacity,
4
+ :stroke_width,
5
+ :computed_properties,
6
+ :viewport_sizing,
7
+ :inside_use, :inside_clip_path, :anchor_href
8
+
9
+ def initialize
10
+ @stroke_width = 1
11
+ @opacity = 1
12
+ @last_fill_opacity = 1
13
+ @last_stroke_opacity = 1
14
+ @computed_properties = Prawn::SVG::Properties.new.load_default_stylesheet
15
+ end
16
+
17
+ def initialize_dup(_other)
18
+ @computed_properties = @computed_properties.dup
19
+ end
20
+
21
+ def disable_drawing
22
+ inside_clip_path
23
+ end
24
+ end
@@ -0,0 +1,78 @@
1
+ module Prawn::SVG::TransformParser
2
+ include Prawn::SVG::PDFMatrix
3
+
4
+ def parse_transform_attribute(transform, space: :pdf)
5
+ matrix = Matrix.identity(3)
6
+
7
+ flip = space == :svg ? 1.0 : -1.0
8
+
9
+ parse_css_method_calls(transform).each do |name, arguments|
10
+ case name
11
+ when 'translate'
12
+ x, y = arguments
13
+ matrix *= translation_matrix(x_pixels(x.to_f), flip * y_pixels(y.to_f))
14
+
15
+ when 'translateX'
16
+ x = arguments.first
17
+ matrix *= translation_matrix(x_pixels(x.to_f), 0.0)
18
+
19
+ when 'translateY'
20
+ y = arguments.first
21
+ matrix *= translation_matrix(0.0, flip * y_pixels(y.to_f))
22
+
23
+ when 'rotate'
24
+ angle, x, y = arguments.collect(&:to_f)
25
+ angle = angle * Math::PI / 180.0
26
+
27
+ rotation = rotation_matrix(angle, space: space)
28
+
29
+ case arguments.length
30
+ when 1
31
+ matrix *= rotation
32
+ when 3
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))
36
+ else
37
+ warnings << "transform 'rotate' must have either one or three arguments"
38
+ end
39
+
40
+ when 'scale'
41
+ x_scale = arguments[0].to_f
42
+ y_scale = (arguments[1] || x_scale).to_f
43
+ matrix *= scale_matrix(x_scale, y_scale)
44
+
45
+ when 'skewX'
46
+ angle = arguments[0].to_f * Math::PI / 180.0
47
+ matrix *= Matrix[[1, flip * Math.tan(angle), 0], [0, 1, 0], [0, 0, 1]]
48
+
49
+ when 'skewY'
50
+ angle = arguments[0].to_f * Math::PI / 180.0
51
+ matrix *= Matrix[[1, 0, 0], [flip * Math.tan(angle), 1, 0], [0, 0, 1]]
52
+
53
+ when 'matrix'
54
+ if arguments.length == 6
55
+ a, b, c, d, e, f = arguments.collect(&:to_f)
56
+ matrix *= Matrix[[a, flip * c, e], [flip * b, d, flip * f], [0, 0, 1]]
57
+ else
58
+ warnings << "transform 'matrix' must have six arguments"
59
+ end
60
+
61
+ else
62
+ warnings << "Unknown/unsupported transformation '#{name}'; ignoring"
63
+ end
64
+ end
65
+
66
+ matrix
67
+ end
68
+
69
+ private
70
+
71
+ def parse_css_method_calls(string)
72
+ string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
73
+ name, argument_string = call
74
+ arguments = argument_string.strip.split(/\s*[,\s]\s*/)
75
+ [name, arguments]
76
+ end
77
+ end
78
+ end