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,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
|