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,308 @@
1
+ module Prawn::SVG
2
+ class Elements::TextNode
3
+ Chunk = Struct.new(:text, :x, :y, :dx, :dy, :rotate, :base_width, :offset, :fixed_width, :font_runs)
4
+
5
+ attr_reader :component, :chunks
6
+ attr_accessor :text
7
+
8
+ def initialize(component, text, leading_space, trailing_space)
9
+ @component = component
10
+ @text = text
11
+ @leading_space = leading_space
12
+ @trailing_space = trailing_space
13
+ end
14
+
15
+ def leading_space?
16
+ @leading_space
17
+ end
18
+
19
+ def trailing_space?
20
+ @trailing_space
21
+ end
22
+
23
+ def calculated_width
24
+ @chunks.reduce(0) do |total, chunk|
25
+ total + (chunk.fixed_width || chunk.base_width) + chunk.offset
26
+ end
27
+ end
28
+
29
+ def total_flexible_and_fixed_width
30
+ flexible = fixed = 0
31
+ chunks.each do |chunk|
32
+ if chunk.fixed_width.nil?
33
+ flexible += chunk.base_width
34
+ fixed += chunk.offset
35
+ else
36
+ fixed += chunk.offset + chunk.fixed_width
37
+ end
38
+ end
39
+ [flexible, fixed]
40
+ end
41
+
42
+ def lay_out(prawn)
43
+ remaining_text = @text
44
+ @chunks = []
45
+
46
+ while remaining_text != ''
47
+ x = y = dx = dy = rotate = nil
48
+ remaining = rotation_remaining = false
49
+
50
+ comp = component
51
+ while comp
52
+ shifted = comp.x_values.shift
53
+ x ||= shifted
54
+ shifted = comp.y_values.shift
55
+ y ||= shifted
56
+ shifted = comp.dx.shift
57
+ dx ||= shifted
58
+ shifted = comp.dy.shift
59
+ dy ||= shifted
60
+
61
+ shifted = comp.rotation.length > 1 ? comp.rotation.shift : comp.rotation.first
62
+ if shifted && rotate.nil?
63
+ rotate = shifted
64
+ remaining ||= comp.rotation != [0]
65
+ end
66
+
67
+ remaining ||= comp.x_values.any? || comp.y_values.any? || comp.dx.any? || comp.dy.any? || (rotate && rotate != 0)
68
+ rotation_remaining ||= comp.rotation.length > 1
69
+ comp = comp.parent_component
70
+ end
71
+
72
+ rotate = (-rotate if rotate && rotate != 0)
73
+
74
+ text_to_draw = remaining ? remaining_text[0..0] : remaining_text
75
+
76
+ kerning = component.kerning_enabled?
77
+ opts = { size: component.computed_properties.numeric_font_size, kerning: kerning }
78
+
79
+ fallback_fonts = component.fallback_fonts
80
+ font_runs = fallback_fonts&.any? ? split_into_font_runs(prawn, text_to_draw, fallback_fonts) : nil
81
+
82
+ kerning_spacing = text_to_draw.length > 1 ? (component.kerning_pixels || 0) * (text_to_draw.length - 1) : 0
83
+ letter_spacing = text_to_draw.length > 1 ? (component.letter_spacing_pixels || 0) * (text_to_draw.length - 1) : 0
84
+ word_spacing = (component.word_spacing_pixels || 0) * text_to_draw.count(' ')
85
+ base_width = width_of_text(prawn, text_to_draw, font_runs, opts) + kerning_spacing + letter_spacing + word_spacing
86
+
87
+ offset = dx ? [0, dx].max : 0
88
+
89
+ @chunks << Chunk.new(text_to_draw, x, y, dx, dy, rotate, base_width, offset, nil, font_runs)
90
+
91
+ if remaining
92
+ remaining_text = remaining_text[1..]
93
+ else
94
+ # we can get to this path with rotations still pending
95
+ # solve this by shifting them out by the number of
96
+ # characters we've just drawn
97
+ shift = remaining_text.length - 1
98
+ if rotation_remaining && shift.positive?
99
+ comp = component
100
+ while comp
101
+ count = [shift, comp.rotation.length - 1].min
102
+ comp.rotation.shift(count) if count.positive?
103
+ comp = comp.parent_component
104
+ end
105
+ end
106
+
107
+ break
108
+ end
109
+ end
110
+ end
111
+
112
+ def render(prawn, size, cursor, y_offset)
113
+ chunks.each do |chunk|
114
+ cursor.x = chunk.x if chunk.x
115
+ cursor.x += chunk.dx if chunk.dx
116
+ cursor.y = chunk.y if chunk.y
117
+ cursor.y -= chunk.dy if chunk.dy
118
+
119
+ width = chunk.fixed_width || chunk.base_width
120
+
121
+ unless component.inside_clip_path
122
+ decoration = component.computed_properties.text_decoration
123
+ unless decoration == 'none'
124
+ render_underline(prawn, size, cursor, y_offset, width) if decoration.include?('underline')
125
+ render_overline(prawn, size, cursor, y_offset, width) if decoration.include?('overline')
126
+ render_line_through(prawn, size, cursor, y_offset, width) if decoration.include?('line-through')
127
+ end
128
+ render_link_annotation(prawn, size, cursor, y_offset, width)
129
+ end
130
+
131
+ kerning = component.kerning_enabled?
132
+ opts = { size: size, at: [cursor.x, cursor.y + (y_offset || 0)], kerning: kerning }
133
+ opts[:rotate] = chunk.rotate if chunk.rotate
134
+
135
+ scaling =
136
+ if chunk.fixed_width && component.current_length_adjust_is_scaling?
137
+ chunk.fixed_width * 100 / chunk.base_width
138
+ else
139
+ 100
140
+ end
141
+
142
+ spacing_enabled = chunk.fixed_width && !component.current_length_adjust_is_scaling? && chunk.text.length > 1
143
+
144
+ # This isn't perfect. It assumes the parent component which started the textLength context
145
+ # has a character at the end of its text nodes. If it doesn't, the last character in its
146
+ # children should not take the space. This is possible but would involve a lot more work so
147
+ # I will park it for now.
148
+ parent_spacing = spacing_enabled && !component.text_length
149
+ spacing =
150
+ if spacing_enabled
151
+ ((chunk.fixed_width - chunk.base_width) / (chunk.text.length - (parent_spacing ? 0 : 1))) + (component.kerning_pixels || 0) + (component.letter_spacing_pixels || 0)
152
+ end
153
+
154
+ # Inside clip paths, text renders white in a soft mask to define the
155
+ # clipping region. Override any fill color from the SVG element.
156
+ prawn.fill_color('ffffff') if component.inside_clip_path
157
+
158
+ combined_spacing = spacing || combined_kerning_and_letter_spacing
159
+ prawn.horizontal_text_scaling(scaling) do
160
+ prawn.character_spacing(combined_spacing || prawn.character_spacing) do
161
+ prawn.word_spacing(component.word_spacing_pixels || prawn.word_spacing) do
162
+ prawn.text_rendering_mode(calculate_text_rendering_mode) do
163
+ render_text_directly(prawn, chunk.text, chunk.font_runs, opts)
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ cursor.x += chunk.fixed_width || chunk.base_width
170
+
171
+ # If we're in a textLength context for one of our parents, we'll need to add spacing
172
+ # to the end of our string. See comment above for why this isn't quite right.
173
+ cursor.x += spacing if parent_spacing
174
+ end
175
+ end
176
+
177
+ def width_of_text(prawn, text, font_runs, opts)
178
+ if font_runs.nil?
179
+ prawn.width_of(text, **opts)
180
+ else
181
+ font_runs.sum(0.0) do |font_name, run_text|
182
+ if font_name
183
+ width = nil
184
+ prawn.font(font_name) { width = prawn.width_of(run_text, **opts) }
185
+ width
186
+ else
187
+ prawn.width_of(run_text, **opts)
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ def render_text_directly(prawn, text, font_runs, opts)
194
+ if font_runs.nil?
195
+ prawn.draw_text(text, **opts)
196
+ else
197
+ kerning = opts[:kerning]
198
+ x = opts[:at][0]
199
+ font_runs.each do |font_name, run_text|
200
+ run_opts = opts.merge(at: [x, opts[:at][1]])
201
+ if font_name
202
+ prawn.font(font_name) do
203
+ prawn.draw_text(run_text, **run_opts)
204
+ x += prawn.width_of(run_text, size: opts[:size], kerning: kerning)
205
+ end
206
+ else
207
+ prawn.draw_text(run_text, **run_opts)
208
+ x += prawn.width_of(run_text, size: opts[:size], kerning: kerning)
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ def split_into_font_runs(prawn, text, fallback_fonts)
215
+ primary_font = prawn.font
216
+ runs = []
217
+ current_font = nil
218
+ current_text = +''
219
+
220
+ prawn.save_font do
221
+ text.each_char do |char|
222
+ font_for_char = font_for_glyph(prawn, char, primary_font, fallback_fonts)
223
+
224
+ if font_for_char != current_font && !current_text.empty?
225
+ runs << [current_font, current_text]
226
+ current_text = +''
227
+ end
228
+ current_font = font_for_char
229
+ current_text << char
230
+ end
231
+ end
232
+
233
+ runs << [current_font, current_text] unless current_text.empty?
234
+ runs
235
+ end
236
+
237
+ def font_for_glyph(prawn, char, primary_font, fallback_fonts)
238
+ return nil if primary_font.glyph_present?(char)
239
+
240
+ fallback_fonts.each do |fb|
241
+ prawn.font(fb)
242
+ return fb if prawn.font.glyph_present?(char)
243
+ end
244
+
245
+ nil
246
+ end
247
+
248
+ def render_underline(prawn, size, cursor, y_offset, width)
249
+ offset, thickness = FontMetrics.underline_metrics(prawn, size)
250
+
251
+ prawn.fill_rectangle [cursor.x, cursor.y + (y_offset || 0) + offset + (thickness / 2.0)], width, thickness
252
+ end
253
+
254
+ def render_overline(prawn, size, cursor, y_offset, width)
255
+ offset, thickness = FontMetrics.overline_metrics(prawn, size)
256
+
257
+ prawn.fill_rectangle [cursor.x, cursor.y + (y_offset || 0) + offset + (thickness / 2.0)], width, thickness
258
+ end
259
+
260
+ def render_line_through(prawn, size, cursor, y_offset, width)
261
+ offset, thickness = FontMetrics.strikethrough_metrics(prawn, size)
262
+
263
+ prawn.fill_rectangle [cursor.x, cursor.y + (y_offset || 0) + offset + (thickness / 2.0)], width, thickness
264
+ end
265
+
266
+ def render_link_annotation(prawn, size, cursor, y_offset, width)
267
+ href = component.state.anchor_href
268
+ return unless href
269
+
270
+ text_bottom = cursor.y + (y_offset || 0) - scaled_font_size(prawn, :descender, size)
271
+ font_height = scaled_font_size(prawn, :height, size)
272
+
273
+ LinkRenderer.new(href, [cursor.x, text_bottom + font_height, cursor.x + width, text_bottom]).render(prawn)
274
+ end
275
+
276
+ def scaled_font_size(prawn, method_name, size)
277
+ (prawn.font.public_send(method_name) / prawn.font_size) * size
278
+ end
279
+
280
+ def combined_kerning_and_letter_spacing
281
+ kerning = component.kerning_pixels
282
+ letter = component.letter_spacing_pixels
283
+
284
+ if kerning && letter
285
+ kerning + letter
286
+ else
287
+ kerning || letter
288
+ end
289
+ end
290
+
291
+ def calculate_text_rendering_mode
292
+ return :fill if component.inside_clip_path
293
+
294
+ fill = !component.computed_properties.fill.none? # rubocop:disable Style/InverseMethods
295
+ stroke = !component.computed_properties.stroke.none? # rubocop:disable Style/InverseMethods
296
+
297
+ if fill && stroke
298
+ :fill_stroke
299
+ elsif fill
300
+ :fill
301
+ elsif stroke
302
+ :stroke
303
+ else
304
+ :invisible
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,153 @@
1
+ module Prawn::SVG
2
+ class Elements::TextPath < Elements::TextComponent
3
+ def parse
4
+ href = href_attribute
5
+ unless href&.start_with?('#')
6
+ warnings << '<textPath> must reference a path element'
7
+ raise SkipElementQuietly
8
+ end
9
+
10
+ @path_element = document.elements_by_id[href[1..]]
11
+ unless @path_element.is_a?(Elements::Path)
12
+ warnings << "<textPath> reference '#{href}' is not a path element"
13
+ raise SkipElementQuietly
14
+ end
15
+
16
+ @path_length = Calculators::PathLength.new(@path_element.commands)
17
+
18
+ raise SkipElementQuietly if @path_length.total_length <= 0
19
+
20
+ @start_offset = parse_start_offset
21
+
22
+ super
23
+ end
24
+
25
+ def render_component(prawn, renderer, _cursor, _translate_x = nil, accumulated_baseline_shift = 0)
26
+ raise SkipElementQuietly if computed_properties.display == 'none'
27
+
28
+ add_yield_call do
29
+ size = computed_properties.numeric_font_size
30
+ y_offset = dominant_baseline_offset(prawn, size || prawn.font_size) || 0
31
+
32
+ with_svg_fonts(prawn) do
33
+ total_baseline_shift = accumulated_baseline_shift + baseline_shift_offset(prawn, size)
34
+ y_offset += total_baseline_shift
35
+
36
+ current_distance = @start_offset
37
+ render_children_along_path(prawn, children, size, y_offset, current_distance)
38
+ end
39
+ end
40
+
41
+ renderer.render_calls(prawn, base_calls)
42
+ end
43
+
44
+ def transformable?
45
+ false
46
+ end
47
+
48
+ private
49
+
50
+ def render_children_along_path(prawn, children, size, y_offset, current_distance)
51
+ children.each do |child|
52
+ case child
53
+ when Elements::TextNode
54
+ current_distance = render_text_node_along_path(prawn, child, size, y_offset, current_distance)
55
+ when Elements::TextComponent
56
+ prawn.save_graphics_state
57
+ child_size = child.computed_properties.numeric_font_size
58
+ child_y_offset = child.dominant_baseline_offset(prawn, child_size || prawn.font_size)
59
+ child.with_svg_fonts(prawn) do
60
+ current_distance = render_children_along_path(prawn, child.children, child_size, child_y_offset, current_distance)
61
+ end
62
+ prawn.restore_graphics_state
63
+ end
64
+ end
65
+
66
+ current_distance
67
+ end
68
+
69
+ def render_text_node_along_path(prawn, text_node, size, y_offset, current_distance)
70
+ text_node.chunks.each do |chunk|
71
+ advances = kerned_advances(prawn, text_node, chunk.text, size)
72
+
73
+ chunk.text.each_char.with_index do |char, i|
74
+ char_width = text_node.width_of_text(prawn, char, nil, { size: size, kerning: false })
75
+ advance = advances[i]
76
+
77
+ midpoint = current_distance + (char_width / 2.0)
78
+ point = @path_length.point_at(midpoint)
79
+ break unless point
80
+
81
+ mid_x, mid_y_svg, angle = point
82
+ angle_rad = angle * Math::PI / 180.0
83
+ half_width = char_width / 2.0
84
+
85
+ # Offset back from midpoint to left edge along the tangent
86
+ px = mid_x - (half_width * Math.cos(angle_rad))
87
+ py_svg = mid_y_svg - (half_width * Math.sin(angle_rad))
88
+ py = document.sizing.output_height - py_svg
89
+
90
+ draw_opts = { size: size, at: [px, py + (y_offset || 0)] }
91
+ draw_opts[:rotate] = -angle
92
+
93
+ render_mode = calculate_text_rendering_mode(text_node.component)
94
+
95
+ prawn.text_rendering_mode(render_mode) do
96
+ text_node.render_text_directly(prawn, char, nil, draw_opts)
97
+ end
98
+
99
+ current_distance += advance
100
+ end
101
+ end
102
+
103
+ current_distance
104
+ end
105
+
106
+ # Compute the advance width for each character, accounting for kerning
107
+ # with the following character. The advance is the character's own width
108
+ # plus any kerning adjustment with its neighbour.
109
+ def kerned_advances(prawn, text_node, text, size)
110
+ opts_no_kern = { size: size, kerning: false }
111
+ opts_kern = { size: size, kerning: true }
112
+
113
+ text.each_char.with_index.map do |char, i|
114
+ char_width = text_node.width_of_text(prawn, char, nil, opts_no_kern)
115
+ next_char = text[i + 1]
116
+
117
+ if next_char
118
+ pair_width = text_node.width_of_text(prawn, "#{char}#{next_char}", nil, opts_kern)
119
+ next_width = text_node.width_of_text(prawn, next_char, nil, opts_no_kern)
120
+ char_width + (pair_width - char_width - next_width)
121
+ else
122
+ char_width
123
+ end
124
+ end
125
+ end
126
+
127
+ def calculate_text_rendering_mode(component)
128
+ fill = !component.computed_properties.fill.none? # rubocop:disable Style/InverseMethods
129
+ stroke = !component.computed_properties.stroke.none? # rubocop:disable Style/InverseMethods
130
+
131
+ if fill && stroke
132
+ :fill_stroke
133
+ elsif fill
134
+ :fill
135
+ elsif stroke
136
+ :stroke
137
+ else
138
+ :invisible
139
+ end
140
+ end
141
+
142
+ def parse_start_offset
143
+ value = attributes['startOffset']
144
+ return 0.0 unless value
145
+
146
+ if value.end_with?('%')
147
+ @path_length.total_length * value.to_f / 100.0
148
+ else
149
+ x_pixels(value)
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,142 @@
1
+ class Prawn::SVG::Elements::Use < Prawn::SVG::Elements::Base
2
+ attr_reader :referenced_element_class, :referenced_element_source
3
+
4
+ def parse
5
+ href = href_attribute
6
+ raise SkipElementError, 'use tag must have an href or xlink:href' if href.nil?
7
+
8
+ if href.start_with?('#')
9
+ resolve_local_reference(href[1..])
10
+ else
11
+ resolve_external_reference(href)
12
+ end
13
+
14
+ raise SkipElementError, "use tag references '#{href}' which could not be resolved" if referenced_element_class.nil?
15
+
16
+ @referenced_element_class = Prawn::SVG::Elements::Viewport if referenced_element_source.name == 'symbol'
17
+
18
+ state.inside_use = true
19
+
20
+ @x = attributes['x']
21
+ @y = attributes['y']
22
+ @width = attributes['width']
23
+ @height = attributes['height']
24
+ end
25
+
26
+ def container?
27
+ true
28
+ end
29
+
30
+ def apply
31
+ add_call_and_enter 'translate', x_pixels(@x || 0), -y_pixels(@y || 0) if @x || @y
32
+ end
33
+
34
+ def process_child_elements
35
+ add_call 'save'
36
+
37
+ source = clone_element_source(referenced_element_source)
38
+
39
+ if referenced_element_class == Prawn::SVG::Elements::Viewport
40
+ source.attributes['width'] = @width || '100%'
41
+ source.attributes['height'] = @height || '100%'
42
+ end
43
+
44
+ child = referenced_element_class.new(document, source, calls, state.dup)
45
+ child.process
46
+
47
+ add_call 'restore'
48
+ end
49
+
50
+ private
51
+
52
+ def resolve_local_reference(id)
53
+ referenced_element = @document.elements_by_id[id]
54
+
55
+ if referenced_element
56
+ @referenced_element_class = referenced_element.class
57
+ @referenced_element_source = referenced_element.source
58
+ else
59
+ # Perhaps the element is defined further down in the document. This is not recommended but still valid SVG,
60
+ # so we'll support it with an exception case that's not particularly performant.
61
+ raw_element = REXML::XPath.match(@document.root, %(//*[@id="#{id.gsub('"', '\"')}"])).first
62
+
63
+ if raw_element
64
+ @referenced_element_class = Prawn::SVG::Elements::TAG_CLASS_MAPPING[raw_element.name.to_sym]
65
+ @referenced_element_source = raw_element
66
+ end
67
+ end
68
+ end
69
+
70
+ def resolve_external_reference(href)
71
+ url, fragment = split_href(href)
72
+ raise SkipElementError, 'use tag with external href must include a fragment identifier' unless fragment
73
+
74
+ external_root, external_element_styles = load_external_svg(url)
75
+
76
+ raw_element = REXML::XPath.match(external_root, %(//*[@id="#{fragment.gsub('"', '\"')}"])).first
77
+ return unless raw_element
78
+
79
+ @referenced_element_class = Prawn::SVG::Elements::TAG_CLASS_MAPPING[raw_element.name.to_sym]
80
+ return unless @referenced_element_class
81
+
82
+ @referenced_element_source = raw_element
83
+
84
+ import_external_styles(raw_element, external_element_styles)
85
+ import_external_defs(external_root, external_element_styles)
86
+ end
87
+
88
+ def split_href(href)
89
+ if href.include?('#')
90
+ fragment_index = href.rindex('#')
91
+ [href[0...fragment_index], href[(fragment_index + 1)..]]
92
+ else
93
+ [href, nil]
94
+ end
95
+ end
96
+
97
+ def load_external_svg(url)
98
+ @document.external_svg_cache[url] ||= begin
99
+ data = @document.url_loader.load(url)
100
+
101
+ root = begin
102
+ REXML::Document.new(data).root
103
+ rescue REXML::ParseException
104
+ raise SkipElementError, "use tag references external SVG at '#{url}' which could not be parsed"
105
+ end
106
+
107
+ raise SkipElementError, "use tag references external URL '#{url}' which does not contain SVG" unless root
108
+
109
+ css_parser = CssParser::Parser.new
110
+ stylesheets = Prawn::SVG::CSS::Stylesheets.new(css_parser, root)
111
+ element_styles = stylesheets.load
112
+
113
+ [root, element_styles]
114
+ end
115
+ rescue Prawn::SVG::UrlLoader::Error => e
116
+ raise SkipElementError, "use tag could not load external SVG from '#{url}': #{e.message}"
117
+ end
118
+
119
+ def import_external_styles(element, external_element_styles)
120
+ copy_styles_recursive(element, external_element_styles)
121
+ end
122
+
123
+ def import_external_defs(external_root, external_element_styles)
124
+ REXML::XPath.match(external_root, '//defs').each do |defs_element|
125
+ defs_element.elements.each do |child|
126
+ id = child.attributes['id']
127
+ next unless id && !id.empty?
128
+
129
+ copy_styles_recursive(child, external_element_styles)
130
+ end
131
+ end
132
+ end
133
+
134
+ def copy_styles_recursive(element, external_element_styles)
135
+ styles = external_element_styles[element]
136
+ @document.element_styles[element] = styles if styles
137
+
138
+ element.elements.each do |child|
139
+ copy_styles_recursive(child, external_element_styles)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,29 @@
1
+ class Prawn::SVG::Elements::Viewport < Prawn::SVG::Elements::Base
2
+ def parse
3
+ parent_dimensions = [state.viewport_sizing.viewport_width, state.viewport_sizing.viewport_height]
4
+
5
+ @sizing = Prawn::SVG::Calculators::DocumentSizing.new(parent_dimensions, attributes)
6
+ @sizing.calculate
7
+
8
+ @x = x_pixels(attributes['x'] || 0)
9
+ @y = y_pixels(attributes['y'] || 0)
10
+
11
+ state.viewport_sizing = @sizing
12
+ end
13
+
14
+ def apply
15
+ add_call 'transformation_matrix', 1, 0, 0, 1, @x, -@y if @x != 0 || @y != 0
16
+
17
+ if overflow_hidden?
18
+ add_call 'rectangle', [0, y(0)], @sizing.output_width, @sizing.output_height
19
+ add_call 'clip'
20
+ end
21
+
22
+ add_call 'transformation_matrix', @sizing.x_scale, 0, 0, @sizing.y_scale, 0, 0
23
+ add_call 'transformation_matrix', 1, 0, 0, 1, -@sizing.x_offset, @sizing.y_offset
24
+ end
25
+
26
+ def container?
27
+ true
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ module Prawn::SVG::Elements
2
+ COMMA_WSP_REGEXP = /(?:\s+,?\s*|,\s*)/.freeze
3
+ end
4
+
5
+ require 'prawn/svg/elements/call_duplicator'
6
+ require 'prawn/svg/elements/bbox_scaling'
7
+
8
+ %w[base direct_render_base root anchor container switch clip_path mask viewport text text_component text_node text_path line polyline polygon circle ellipse
9
+ rect path use image gradient pattern marker ignored].each do |filename|
10
+ require "prawn/svg/elements/#{filename}"
11
+ end
12
+
13
+ module Prawn::SVG::Elements
14
+ TAG_CLASS_MAPPING = {
15
+ g: Prawn::SVG::Elements::Container,
16
+ symbol: Prawn::SVG::Elements::Container,
17
+ defs: Prawn::SVG::Elements::Container,
18
+ a: Prawn::SVG::Elements::Anchor,
19
+ clipPath: Prawn::SVG::Elements::ClipPath,
20
+ switch: Prawn::SVG::Elements::Switch,
21
+ svg: Prawn::SVG::Elements::Viewport,
22
+ text: Prawn::SVG::Elements::Text,
23
+ line: Prawn::SVG::Elements::Line,
24
+ polyline: Prawn::SVG::Elements::Polyline,
25
+ polygon: Prawn::SVG::Elements::Polygon,
26
+ circle: Prawn::SVG::Elements::Circle,
27
+ ellipse: Prawn::SVG::Elements::Ellipse,
28
+ rect: Prawn::SVG::Elements::Rect,
29
+ path: Prawn::SVG::Elements::Path,
30
+ use: Prawn::SVG::Elements::Use,
31
+ image: Prawn::SVG::Elements::Image,
32
+ linearGradient: Prawn::SVG::Elements::Gradient,
33
+ radialGradient: Prawn::SVG::Elements::Gradient,
34
+ pattern: Prawn::SVG::Elements::Pattern,
35
+ marker: Prawn::SVG::Elements::Marker,
36
+ style: Prawn::SVG::Elements::Ignored, # because it is pre-parsed by Document
37
+ title: Prawn::SVG::Elements::Ignored,
38
+ desc: Prawn::SVG::Elements::Ignored,
39
+ metadata: Prawn::SVG::Elements::Ignored,
40
+ foreignObject: Prawn::SVG::Elements::Ignored,
41
+ 'font-face': Prawn::SVG::Elements::Ignored,
42
+ filter: Prawn::SVG::Elements::Ignored, # unsupported
43
+ mask: Prawn::SVG::Elements::Mask,
44
+ view: Prawn::SVG::Elements::Ignored
45
+ }.freeze
46
+ end
@@ -0,0 +1,25 @@
1
+ module Prawn
2
+ module SVG
3
+ module Extension
4
+ #
5
+ # Draws an SVG document into the PDF.
6
+ #
7
+ # +options+ must contain the key :at, which takes a tuple of x and y co-ordinates.
8
+ #
9
+ # +options+ can optionally contain the key :width or :height. If both are
10
+ # specified, only :width will be used. If neither are specified, the resolution
11
+ # given in the SVG will be used.
12
+ #
13
+ # Example usage:
14
+ #
15
+ # svg IO.read("example.svg"), :at => [100, 300], :width => 600
16
+ #
17
+ def svg(data, options = {}, &block)
18
+ svg = Prawn::SVG::Interface.new(data, self, options, &block)
19
+ svg.draw
20
+ { warnings: svg.document.warnings, width: svg.document.sizing.output_width,
21
+ height: svg.document.sizing.output_height }
22
+ end
23
+ end
24
+ end
25
+ end