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.
- checksums.yaml +7 -0
- data/COPYING +2 -0
- data/GPLv2 +339 -0
- data/GPLv3 +674 -0
- data/LICENSE +65 -0
- data/LICENSE-prawn-svg +21 -0
- data/README.md +107 -0
- data/data/fonts/Courier-Bold.afm +342 -0
- data/data/fonts/Courier-BoldOblique.afm +342 -0
- data/data/fonts/Courier-Oblique.afm +342 -0
- data/data/fonts/Courier.afm +342 -0
- data/data/fonts/Helvetica-Bold.afm +2827 -0
- data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/data/fonts/Helvetica.afm +3051 -0
- data/data/fonts/MustRead.html +19 -0
- data/data/fonts/Symbol.afm +213 -0
- data/data/fonts/Times-Bold.afm +2588 -0
- data/data/fonts/Times-BoldItalic.afm +2384 -0
- data/data/fonts/Times-Italic.afm +2667 -0
- data/data/fonts/Times-Roman.afm +2419 -0
- data/data/fonts/ZapfDingbats.afm +225 -0
- data/lib/pdf/core.rb +4 -0
- data/lib/prawn/measurement_extensions.rb +4 -0
- data/lib/prawn/svg/attributes/clip_path.rb +46 -0
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/opacity.rb +30 -0
- data/lib/prawn/svg/attributes/space.rb +10 -0
- data/lib/prawn/svg/attributes/stroke.rb +50 -0
- data/lib/prawn/svg/attributes/transform.rb +10 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +57 -0
- data/lib/prawn/svg/color.rb +240 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/font_family_parser.rb +39 -0
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/css/selector_parser.rb +178 -0
- data/lib/prawn/svg/css/stylesheets.rb +233 -0
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +170 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +312 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
- data/lib/prawn/svg/elements/circle.rb +25 -0
- data/lib/prawn/svg/elements/clip_path.rb +26 -0
- data/lib/prawn/svg/elements/container.rb +10 -0
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/ellipse.rb +22 -0
- data/lib/prawn/svg/elements/gradient.rb +184 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +177 -0
- data/lib/prawn/svg/elements/line.rb +27 -0
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +294 -0
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +25 -0
- data/lib/prawn/svg/elements/polyline.rb +23 -0
- data/lib/prawn/svg/elements/rect.rb +64 -0
- data/lib/prawn/svg/elements/root.rb +22 -0
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +99 -0
- data/lib/prawn/svg/elements/text_component.rb +346 -0
- 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 +142 -0
- data/lib/prawn/svg/elements/viewport.rb +29 -0
- data/lib/prawn/svg/elements.rb +46 -0
- data/lib/prawn/svg/extension.rb +25 -0
- data/lib/prawn/svg/font.rb +20 -0
- data/lib/prawn/svg/font_metrics.rb +144 -0
- data/lib/prawn/svg/font_registry.rb +315 -0
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +332 -0
- data/lib/prawn/svg/gradients.rb +53 -0
- data/lib/prawn/svg/interface.rb +72 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/loaders/data.rb +17 -0
- data/lib/prawn/svg/loaders/file.rb +111 -0
- data/lib/prawn/svg/loaders/web.rb +24 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/pathable.rb +153 -0
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/pdfmatrix.rb +37 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +329 -0
- data/lib/prawn/svg/renderer.rb +249 -0
- data/lib/prawn/svg/state.rb +24 -0
- data/lib/prawn/svg/transform_parser.rb +78 -0
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +95 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +5 -0
- data/lib/prawn/table.rb +4 -0
- data/lib/prawn-svg.rb +53 -0
- data/lib/prawn.rb +10 -0
- data/lib/prawnx/annotations.rb +21 -0
- data/lib/prawnx/document/bounding_box.rb +193 -0
- data/lib/prawnx/document/column_box.rb +91 -0
- data/lib/prawnx/document/internals.rb +144 -0
- data/lib/prawnx/document/span.rb +25 -0
- data/lib/prawnx/document.rb +253 -0
- data/lib/prawnx/encoding.rb +29 -0
- data/lib/prawnx/errors.rb +37 -0
- data/lib/prawnx/font.rb +214 -0
- data/lib/prawnx/fonts/afm.rb +171 -0
- data/lib/prawnx/fonts/otf.rb +50 -0
- data/lib/prawnx/fonts/ttf.rb +410 -0
- data/lib/prawnx/graphic_state.rb +41 -0
- data/lib/prawnx/graphics/color.rb +148 -0
- data/lib/prawnx/graphics/patterns.rb +100 -0
- data/lib/prawnx/graphics/style.rb +171 -0
- data/lib/prawnx/graphics.rb +208 -0
- data/lib/prawnx/grid.rb +216 -0
- data/lib/prawnx/images/image.rb +244 -0
- data/lib/prawnx/images/png_decoder.rb +65 -0
- data/lib/prawnx/images.rb +115 -0
- data/lib/prawnx/measurement_extensions.rb +40 -0
- data/lib/prawnx/measurements.rb +62 -0
- data/lib/prawnx/outline.rb +146 -0
- data/lib/prawnx/page.rb +125 -0
- data/lib/prawnx/page_geometry.rb +27 -0
- data/lib/prawnx/pdf/outline_node.rb +53 -0
- data/lib/prawnx/pdf/reference.rb +31 -0
- data/lib/prawnx/pdf/writer.rb +105 -0
- data/lib/prawnx/pdf.rb +66 -0
- data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
- data/lib/prawnx/pdf_a.rb +130 -0
- data/lib/prawnx/pdf_core.rb +25 -0
- data/lib/prawnx/repeater.rb +79 -0
- data/lib/prawnx/security.rb +108 -0
- data/lib/prawnx/stamp.rb +74 -0
- data/lib/prawnx/table/cell/kinds.rb +246 -0
- data/lib/prawnx/table/cell.rb +331 -0
- data/lib/prawnx/table/cells.rb +208 -0
- data/lib/prawnx/table.rb +296 -0
- data/lib/prawnx/text/box.rb +16 -0
- data/lib/prawnx/text/formatted/arranger.rb +217 -0
- data/lib/prawnx/text/formatted/box.rb +312 -0
- data/lib/prawnx/text/formatted/fragment.rb +205 -0
- data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
- data/lib/prawnx/text/formatted/parser.rb +149 -0
- data/lib/prawnx/text/formatted/wrap.rb +103 -0
- data/lib/prawnx/text.rb +281 -0
- data/lib/prawnx/transformation_stack.rb +43 -0
- data/lib/prawnx/true_type/cff_subset.rb +157 -0
- data/lib/prawnx/true_type/file.rb +371 -0
- data/lib/prawnx/true_type/open_type_layout.rb +189 -0
- data/lib/prawnx/true_type/simple_subsets.rb +94 -0
- data/lib/prawnx/true_type/subset.rb +187 -0
- data/lib/prawnx/version.rb +7 -0
- data/lib/prawnx/view.rb +28 -0
- data/lib/prawnx.rb +102 -0
- 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
|