prawn-svg 0.38.1 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +3 -1
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes.rb +1 -1
- data/lib/prawn/svg/document.rb +8 -0
- data/lib/prawn/svg/elements/anchor.rb +9 -0
- data/lib/prawn/svg/elements/base.rb +6 -0
- data/lib/prawn/svg/elements/mask.rb +153 -0
- data/lib/prawn/svg/elements/text_component.rb +34 -13
- data/lib/prawn/svg/elements/text_node.rb +97 -6
- data/lib/prawn/svg/elements.rb +3 -3
- data/lib/prawn/svg/font_metrics.rb +27 -16
- data/lib/prawn/svg/gradient_renderer.rb +11 -3
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/properties.rb +1 -0
- data/lib/prawn/svg/renderer.rb +29 -2
- data/lib/prawn/svg/state.rb +1 -1
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +1 -0
- data/spec/integration_spec.rb +20 -0
- data/spec/prawn/svg/elements/mask_spec.rb +382 -0
- data/spec/prawn/svg/elements/text_spec.rb +133 -131
- data/spec/prawn/svg/font_metrics_spec.rb +14 -0
- data/spec/sample_svg/links.svg +42 -2
- data/spec/sample_svg/mask3.svg +13 -0
- data/spec/sample_svg/mask3b.svg +13 -0
- data/spec/sample_svg/mask_basic.svg +11 -0
- data/spec/sample_svg/mask_contentUnits_objectBoundingBox.svg +11 -0
- data/spec/sample_svg/mask_gradient.svg +13 -0
- data/spec/sample_svg/mask_image.svg +10 -0
- data/spec/sample_svg/mask_multiple.svg +15 -0
- data/spec/sample_svg/mask_nested.svg +15 -0
- data/spec/sample_svg/mask_opacity.svg +21 -0
- data/spec/sample_svg/mask_text.svg +10 -0
- data/spec/sample_svg/mask_text_gradient.svg +31 -0
- data/spec/sample_svg/mask_units_objectBoundingBox.svg +11 -0
- data/spec/sample_svg/mask_units_userSpaceOnUse.svg +10 -0
- data/spec/sample_svg/mask_with_transform.svg +11 -0
- data/spec/sample_svg/multilingual.svg +7 -0
- metadata +23 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c524e2dc0d6754bcd60ca3d4e056f64069621b4412ff90b2f07c01123837e80
|
|
4
|
+
data.tar.gz: 3edb96730b5a94d0c5db17ac5cd47e6dd4ab4af4e93b409555fc94e69b12dde4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 344edee9e2156c94d4d82aa995d38df4a73cad64966979d70da1220a30875da3837dbc962d968347b52d1bc55ee843e141275f62e013de31362ea064fbc24f92
|
|
7
|
+
data.tar.gz: a65858effa7cabdff362876b3e877bed5d1e35a9b9dcc70fbc68d23849faea060f14f63fe710baf510e011e5f5433014b365146ac47acef4fd35f2ad02221fbd
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -77,6 +77,8 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
|
|
|
77
77
|
|
|
78
78
|
- `<clipPath>`
|
|
79
79
|
|
|
80
|
+
- `<mask>` with attributes `maskUnits` and `maskContentUnits`
|
|
81
|
+
|
|
80
82
|
- `<marker>`
|
|
81
83
|
|
|
82
84
|
- `<linearGradient>` and `<radialGradient>` are implemented on Prawn 2.2.0+ with attributes `gradientUnits` and
|
|
@@ -120,7 +122,7 @@ Pseudo-elements and the other pseudo-classes are not supported.
|
|
|
120
122
|
|
|
121
123
|
## Not supported
|
|
122
124
|
|
|
123
|
-
prawn-svg does not support
|
|
125
|
+
prawn-svg does not support patterns or filters.
|
|
124
126
|
|
|
125
127
|
It does not support text in the clip area, but you can clip shapes and text by any shape.
|
|
126
128
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Prawn::SVG::Attributes::Mask
|
|
2
|
+
def parse_mask_attribute_and_call
|
|
3
|
+
return unless (mask = properties.mask)
|
|
4
|
+
return if mask == 'none'
|
|
5
|
+
|
|
6
|
+
mask_element = extract_element_from_url_id_reference(mask, 'mask')
|
|
7
|
+
|
|
8
|
+
if mask_element.nil?
|
|
9
|
+
document.warnings << 'Could not resolve mask URI to a mask element'
|
|
10
|
+
else
|
|
11
|
+
add_call_and_enter 'save_graphics_state'
|
|
12
|
+
mask_calls = mask_element.build_mask_calls(self)
|
|
13
|
+
@calls << ['soft_mask', [], {}, mask_calls]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/prawn/svg/attributes.rb
CHANGED
data/lib/prawn/svg/document.rb
CHANGED
|
@@ -8,6 +8,7 @@ class Prawn::SVG::Elements::Base
|
|
|
8
8
|
include Prawn::SVG::Attributes::Transform
|
|
9
9
|
include Prawn::SVG::Attributes::Opacity
|
|
10
10
|
include Prawn::SVG::Attributes::ClipPath
|
|
11
|
+
include Prawn::SVG::Attributes::Mask
|
|
11
12
|
include Prawn::SVG::Attributes::Stroke
|
|
12
13
|
include Prawn::SVG::Attributes::Space
|
|
13
14
|
|
|
@@ -56,6 +57,10 @@ class Prawn::SVG::Elements::Base
|
|
|
56
57
|
apply_calls_from_standard_attributes
|
|
57
58
|
apply
|
|
58
59
|
|
|
60
|
+
if state.anchor_href && bounding_box
|
|
61
|
+
add_call('svg:add_link', state.anchor_href, bounding_box)
|
|
62
|
+
end
|
|
63
|
+
|
|
59
64
|
process_child_elements if container?
|
|
60
65
|
|
|
61
66
|
append_calls_to_parent unless computed_properties.display == 'none'
|
|
@@ -154,6 +159,7 @@ class Prawn::SVG::Elements::Base
|
|
|
154
159
|
parse_transform_attribute_and_call
|
|
155
160
|
parse_opacity_attributes_and_call
|
|
156
161
|
parse_clip_path_attribute_and_call
|
|
162
|
+
parse_mask_attribute_and_call
|
|
157
163
|
apply_colors
|
|
158
164
|
parse_stroke_attributes_and_call
|
|
159
165
|
apply_drawing_call
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
class Prawn::SVG::Elements::Mask < Prawn::SVG::Elements::Base
|
|
2
|
+
def parse
|
|
3
|
+
properties.display = 'none'
|
|
4
|
+
computed_properties.display = 'none'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def container?
|
|
8
|
+
true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def build_mask_calls(element)
|
|
12
|
+
bbox = element.bounding_box
|
|
13
|
+
mask_units = attributes['maskUnits'] || 'objectBoundingBox'
|
|
14
|
+
content_units = attributes['maskContentUnits'] || 'userSpaceOnUse'
|
|
15
|
+
|
|
16
|
+
if content_units == 'objectBoundingBox' && bbox.nil?
|
|
17
|
+
document.warnings << 'mask with maskContentUnits="objectBoundingBox" requires element to have a bounding box'
|
|
18
|
+
return []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
calls = []
|
|
22
|
+
|
|
23
|
+
calls.concat(build_clip_calls(bbox, mask_units)) if bbox || mask_units == 'userSpaceOnUse'
|
|
24
|
+
|
|
25
|
+
if content_units == 'objectBoundingBox'
|
|
26
|
+
calls.concat(build_object_bounding_box_calls(bbox))
|
|
27
|
+
else
|
|
28
|
+
calls.concat(duplicate_calls(base_calls))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
calls
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def build_clip_calls(bbox, mask_units)
|
|
37
|
+
if mask_units == 'objectBoundingBox'
|
|
38
|
+
mask_x = Float(attributes['x'] || '-0.1')
|
|
39
|
+
mask_y = Float(attributes['y'] || '-0.1')
|
|
40
|
+
mask_w = Float(attributes['width'] || '1.2')
|
|
41
|
+
mask_h = Float(attributes['height'] || '1.2')
|
|
42
|
+
|
|
43
|
+
bbox_left = bbox[0]
|
|
44
|
+
bbox_top = bbox[1]
|
|
45
|
+
bbox_right = bbox[2]
|
|
46
|
+
bbox_bottom = bbox[3]
|
|
47
|
+
bbox_w = bbox_right - bbox_left
|
|
48
|
+
bbox_h = bbox_top - bbox_bottom
|
|
49
|
+
|
|
50
|
+
clip_left = bbox_left + (mask_x * bbox_w)
|
|
51
|
+
clip_top = bbox_top - (mask_y * bbox_h)
|
|
52
|
+
clip_width = mask_w * bbox_w
|
|
53
|
+
clip_height = mask_h * bbox_h
|
|
54
|
+
else
|
|
55
|
+
clip_left = x_pixels(attributes['x'] || '-10%')
|
|
56
|
+
clip_top = y(attributes['y'] || '-10%')
|
|
57
|
+
clip_width = x_pixels(attributes['width'] || '120%')
|
|
58
|
+
clip_height = y_pixels(attributes['height'] || '120%')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[
|
|
62
|
+
['rectangle', [[clip_left, clip_top], clip_width, clip_height], {}, []],
|
|
63
|
+
['clip', [], {}, []]
|
|
64
|
+
]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_object_bounding_box_calls(bbox)
|
|
68
|
+
bbox_left = bbox[0]
|
|
69
|
+
bbox_top = bbox[1]
|
|
70
|
+
bbox_right = bbox[2]
|
|
71
|
+
bbox_bottom = bbox[3]
|
|
72
|
+
bbox_w = bbox_right - bbox_left
|
|
73
|
+
bbox_h = bbox_top - bbox_bottom
|
|
74
|
+
|
|
75
|
+
# Prawn's soft_mask doesn't support transformation_matrix inside the block,
|
|
76
|
+
# so we must produce calls with final Prawn coordinates.
|
|
77
|
+
#
|
|
78
|
+
# Set up sizing so that:
|
|
79
|
+
# - viewport = 1x1 (objectBoundingBox fractions resolve as-is for unitless values)
|
|
80
|
+
# - output_height = bbox_top, so y(frac) = bbox_top - frac = correct when bbox_h=1
|
|
81
|
+
#
|
|
82
|
+
# Then scale x by bbox_w, offset x by bbox_left, and scale y displacement by bbox_h.
|
|
83
|
+
|
|
84
|
+
unit_sizing = Prawn::SVG::Calculators::DocumentSizing.new([1, 1])
|
|
85
|
+
unit_sizing.document_width = 1
|
|
86
|
+
unit_sizing.document_height = 1
|
|
87
|
+
unit_sizing.calculate
|
|
88
|
+
|
|
89
|
+
result_calls = document.with_sizing(unit_sizing) do
|
|
90
|
+
new_state = state.dup
|
|
91
|
+
new_state.viewport_sizing = unit_sizing
|
|
92
|
+
new_state.inside_use = true
|
|
93
|
+
|
|
94
|
+
container = Prawn::SVG::Elements::Container.new(document, source, [], new_state)
|
|
95
|
+
container.process
|
|
96
|
+
|
|
97
|
+
container.base_calls
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
scale_calls_to_bbox(duplicate_calls(result_calls), bbox_left, bbox_top, bbox_w, bbox_h)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def scale_calls_to_bbox(calls, bbox_left, bbox_top, bbox_w, bbox_h)
|
|
104
|
+
calls.map do |name, args, kwargs, children|
|
|
105
|
+
new_args = case name
|
|
106
|
+
when 'rectangle'
|
|
107
|
+
point, width, height = args
|
|
108
|
+
[scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h), width * bbox_w, height * bbox_h]
|
|
109
|
+
when 'rounded_rectangle'
|
|
110
|
+
point, width, height, radius = args
|
|
111
|
+
[scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h), width * bbox_w, height * bbox_h, radius * bbox_w]
|
|
112
|
+
when 'move_to', 'line_to'
|
|
113
|
+
[scale_point(args[0], bbox_left, bbox_top, bbox_w, bbox_h)]
|
|
114
|
+
when 'circle'
|
|
115
|
+
point, radius = args
|
|
116
|
+
scaled_point = scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h)
|
|
117
|
+
if bbox_w == bbox_h
|
|
118
|
+
[scaled_point, radius * bbox_w]
|
|
119
|
+
else
|
|
120
|
+
new_children = children.any? ? scale_calls_to_bbox(children, bbox_left, bbox_top, bbox_w, bbox_h) : children
|
|
121
|
+
next ['ellipse', [scaled_point, radius * bbox_w, radius * bbox_h], kwargs, new_children]
|
|
122
|
+
end
|
|
123
|
+
when 'ellipse'
|
|
124
|
+
point, rx, ry = args
|
|
125
|
+
[scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h), rx * bbox_w, ry * bbox_h]
|
|
126
|
+
when 'curve_to'
|
|
127
|
+
dest = scale_point(args[0], bbox_left, bbox_top, bbox_w, bbox_h)
|
|
128
|
+
[dest]
|
|
129
|
+
else
|
|
130
|
+
args
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
new_kwargs = if name == 'curve_to' && kwargs[:bounds]
|
|
134
|
+
b = kwargs[:bounds]
|
|
135
|
+
{ bounds: b.map { |p| scale_point(p, bbox_left, bbox_top, bbox_w, bbox_h) } }
|
|
136
|
+
else
|
|
137
|
+
kwargs
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
new_children = children.any? ? scale_calls_to_bbox(children, bbox_left, bbox_top, bbox_w, bbox_h) : children
|
|
141
|
+
[name, new_args, new_kwargs, new_children]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def scale_point(point, bbox_left, bbox_top, bbox_w, bbox_h)
|
|
146
|
+
# point is [x, 1.0 - svg_y] from unit sizing where output_height = 1
|
|
147
|
+
# We need [bbox_left + svg_x * bbox_w, bbox_top - svg_y * bbox_h]
|
|
148
|
+
# svg_x = point[0], svg_y = 1.0 - point[1]
|
|
149
|
+
x = bbox_left + (point[0] * bbox_w)
|
|
150
|
+
y = bbox_top - ((1.0 - point[1]) * bbox_h)
|
|
151
|
+
[x, y]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -2,7 +2,7 @@ module Prawn::SVG
|
|
|
2
2
|
class Elements::TextComponent < Elements::DirectRenderBase
|
|
3
3
|
attr_reader :children, :parent_component
|
|
4
4
|
attr_reader :x_values, :y_values, :dx, :dy, :rotation, :text_length, :length_adjust
|
|
5
|
-
attr_reader :font
|
|
5
|
+
attr_reader :font, :fallback_fonts
|
|
6
6
|
|
|
7
7
|
def initialize(document, source, _calls, state, parent_component = nil)
|
|
8
8
|
if parent_component.nil? && source.name != 'text'
|
|
@@ -24,7 +24,7 @@ module Prawn::SVG
|
|
|
24
24
|
@text_length = normalize_length(attributes['textLength'])
|
|
25
25
|
@length_adjust = attributes['lengthAdjust']
|
|
26
26
|
|
|
27
|
-
@font =
|
|
27
|
+
@font, @fallback_fonts = select_fonts
|
|
28
28
|
|
|
29
29
|
@children = svg_text_children.flat_map do |child|
|
|
30
30
|
if child.node_type == :text
|
|
@@ -42,9 +42,8 @@ module Prawn::SVG
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def lay_out(prawn)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
prawn.font(font.name, style: font.subfamily) if font
|
|
45
|
+
with_svg_fonts(prawn) do
|
|
46
|
+
@children.each do |child|
|
|
48
47
|
child.lay_out(prawn)
|
|
49
48
|
end
|
|
50
49
|
end
|
|
@@ -73,9 +72,7 @@ module Prawn::SVG
|
|
|
73
72
|
y_offset = -height / 2.0
|
|
74
73
|
end
|
|
75
74
|
|
|
76
|
-
prawn
|
|
77
|
-
prawn.font(font.name, style: font.subfamily) if font
|
|
78
|
-
|
|
75
|
+
with_svg_fonts(prawn) do
|
|
79
76
|
children.each do |child|
|
|
80
77
|
case child
|
|
81
78
|
when Elements::TextNode
|
|
@@ -165,18 +162,42 @@ module Prawn::SVG
|
|
|
165
162
|
end
|
|
166
163
|
end
|
|
167
164
|
|
|
168
|
-
def
|
|
169
|
-
font_families = [computed_properties.font_family, document.fallback_font_name]
|
|
165
|
+
def select_fonts
|
|
170
166
|
font_style = :italic if computed_properties.font_style == 'italic'
|
|
171
167
|
font_weight = computed_properties.font_weight
|
|
168
|
+
fonts = []
|
|
172
169
|
|
|
173
|
-
|
|
170
|
+
font_family_names.each do |name|
|
|
174
171
|
font = document.font_registry.load(name, font_weight, font_style)
|
|
175
|
-
|
|
172
|
+
next unless font
|
|
173
|
+
next if fonts.any? { |existing| existing.name == font.name && existing.subfamily == font.subfamily }
|
|
174
|
+
|
|
175
|
+
fonts << font
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
+
_, *fallback_font_names = fonts.map(&:name).uniq
|
|
179
|
+
|
|
180
|
+
return [fonts.first, fallback_font_names] if fonts.any?
|
|
181
|
+
|
|
178
182
|
warnings << "Font family '#{computed_properties.font_family}' style '#{computed_properties.font_style}' is not a known font, and the fallback font could not be found."
|
|
179
|
-
|
|
183
|
+
[]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def with_svg_fonts(prawn)
|
|
187
|
+
prawn.save_font do
|
|
188
|
+
if font
|
|
189
|
+
prawn.font(font.name, style: font.subfamily)
|
|
190
|
+
prawn.fallback_fonts(fallback_fonts)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
yield
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def font_family_names
|
|
198
|
+
names = CSS::FontFamilyParser.parse(computed_properties.font_family.to_s)
|
|
199
|
+
names << document.fallback_font_name if document.fallback_font_name
|
|
200
|
+
names.map(&:strip).reject(&:empty?)
|
|
180
201
|
end
|
|
181
202
|
|
|
182
203
|
def total_flexible_and_fixed_width
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Prawn::SVG
|
|
2
2
|
class Elements::TextNode
|
|
3
|
-
Chunk = Struct.new(:text, :x, :y, :dx, :dy, :rotate, :base_width, :offset, :fixed_width)
|
|
3
|
+
Chunk = Struct.new(:text, :x, :y, :dx, :dy, :rotate, :base_width, :offset, :fixed_width, :font_runs)
|
|
4
4
|
|
|
5
5
|
attr_reader :component, :chunks
|
|
6
6
|
attr_accessor :text
|
|
@@ -75,12 +75,15 @@ module Prawn::SVG
|
|
|
75
75
|
|
|
76
76
|
opts = { size: component.computed_properties.numeric_font_size, kerning: true }
|
|
77
77
|
|
|
78
|
+
fallback_fonts = component.fallback_fonts
|
|
79
|
+
font_runs = fallback_fonts&.any? ? split_into_font_runs(prawn, text_to_draw, fallback_fonts) : nil
|
|
80
|
+
|
|
78
81
|
total_spacing = text_to_draw.length > 1 ? (component.letter_spacing_pixels || 0) * (text_to_draw.length - 1) : 0
|
|
79
|
-
base_width = prawn
|
|
82
|
+
base_width = width_of_text(prawn, text_to_draw, font_runs, opts) + total_spacing
|
|
80
83
|
|
|
81
84
|
offset = dx ? [0, dx].max : 0
|
|
82
85
|
|
|
83
|
-
@chunks << Chunk.new(text_to_draw, x, y, dx, dy, rotate, base_width, offset, nil)
|
|
86
|
+
@chunks << Chunk.new(text_to_draw, x, y, dx, dy, rotate, base_width, offset, nil, font_runs)
|
|
84
87
|
|
|
85
88
|
if remaining
|
|
86
89
|
remaining_text = remaining_text[1..]
|
|
@@ -110,9 +113,12 @@ module Prawn::SVG
|
|
|
110
113
|
cursor.y = chunk.y if chunk.y
|
|
111
114
|
cursor.y -= chunk.dy if chunk.dy
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
width = chunk.fixed_width || chunk.base_width
|
|
117
|
+
|
|
118
|
+
render_underline(prawn, size, cursor, y_offset, width) if component.computed_properties.text_decoration == 'underline'
|
|
119
|
+
render_link_annotation(prawn, size, cursor, y_offset, width)
|
|
114
120
|
|
|
115
|
-
opts = { size: size, at: [cursor.x, cursor.y + (y_offset || 0)] }
|
|
121
|
+
opts = { size: size, at: [cursor.x, cursor.y + (y_offset || 0)], kerning: true }
|
|
116
122
|
opts[:rotate] = chunk.rotate if chunk.rotate
|
|
117
123
|
|
|
118
124
|
scaling =
|
|
@@ -137,7 +143,7 @@ module Prawn::SVG
|
|
|
137
143
|
prawn.horizontal_text_scaling(scaling) do
|
|
138
144
|
prawn.character_spacing(spacing || component.letter_spacing_pixels || prawn.character_spacing) do
|
|
139
145
|
prawn.text_rendering_mode(calculate_text_rendering_mode) do
|
|
140
|
-
prawn
|
|
146
|
+
render_text_directly(prawn, chunk.text, chunk.font_runs, opts)
|
|
141
147
|
end
|
|
142
148
|
end
|
|
143
149
|
end
|
|
@@ -150,12 +156,97 @@ module Prawn::SVG
|
|
|
150
156
|
end
|
|
151
157
|
end
|
|
152
158
|
|
|
159
|
+
def width_of_text(prawn, text, font_runs, opts)
|
|
160
|
+
if font_runs.nil?
|
|
161
|
+
prawn.width_of(text, **opts)
|
|
162
|
+
else
|
|
163
|
+
font_runs.sum(0.0) do |font_name, run_text|
|
|
164
|
+
if font_name
|
|
165
|
+
width = nil
|
|
166
|
+
prawn.font(font_name) { width = prawn.width_of(run_text, **opts) }
|
|
167
|
+
width
|
|
168
|
+
else
|
|
169
|
+
prawn.width_of(run_text, **opts)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def render_text_directly(prawn, text, font_runs, opts)
|
|
176
|
+
if font_runs.nil?
|
|
177
|
+
prawn.draw_text(text, **opts)
|
|
178
|
+
else
|
|
179
|
+
x = opts[:at][0]
|
|
180
|
+
font_runs.each do |font_name, run_text|
|
|
181
|
+
run_opts = opts.merge(at: [x, opts[:at][1]])
|
|
182
|
+
if font_name
|
|
183
|
+
prawn.font(font_name) do
|
|
184
|
+
prawn.draw_text(run_text, **run_opts)
|
|
185
|
+
x += prawn.width_of(run_text, size: opts[:size], kerning: true)
|
|
186
|
+
end
|
|
187
|
+
else
|
|
188
|
+
prawn.draw_text(run_text, **run_opts)
|
|
189
|
+
x += prawn.width_of(run_text, size: opts[:size], kerning: true)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def split_into_font_runs(prawn, text, fallback_fonts)
|
|
196
|
+
original_font = prawn.font.family
|
|
197
|
+
runs = []
|
|
198
|
+
current_font = nil
|
|
199
|
+
current_text = +''
|
|
200
|
+
|
|
201
|
+
prawn.save_font do
|
|
202
|
+
text.each_char do |char|
|
|
203
|
+
font_for_char = font_for_glyph(prawn, char, original_font, fallback_fonts)
|
|
204
|
+
|
|
205
|
+
if font_for_char != current_font && !current_text.empty?
|
|
206
|
+
runs << [current_font, current_text]
|
|
207
|
+
current_text = +''
|
|
208
|
+
end
|
|
209
|
+
current_font = font_for_char
|
|
210
|
+
current_text << char
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
runs << [current_font, current_text] unless current_text.empty?
|
|
215
|
+
runs
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def font_for_glyph(prawn, char, original_font, fallback_fonts)
|
|
219
|
+
prawn.font(original_font)
|
|
220
|
+
return nil if prawn.font.glyph_present?(char)
|
|
221
|
+
|
|
222
|
+
fallback_fonts.each do |fb|
|
|
223
|
+
prawn.font(fb)
|
|
224
|
+
return fb if prawn.font.glyph_present?(char)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
|
|
153
230
|
def render_underline(prawn, size, cursor, y_offset, width)
|
|
154
231
|
offset, thickness = FontMetrics.underline_metrics(prawn, size)
|
|
155
232
|
|
|
156
233
|
prawn.fill_rectangle [cursor.x, cursor.y + (y_offset || 0) + offset + (thickness / 2.0)], width, thickness
|
|
157
234
|
end
|
|
158
235
|
|
|
236
|
+
def render_link_annotation(prawn, size, cursor, y_offset, width)
|
|
237
|
+
href = component.state.anchor_href
|
|
238
|
+
return unless href
|
|
239
|
+
|
|
240
|
+
text_bottom = cursor.y + (y_offset || 0) - scaled_font_size(prawn, :descender, size)
|
|
241
|
+
font_height = scaled_font_size(prawn, :height, size)
|
|
242
|
+
|
|
243
|
+
LinkRenderer.new(href, [cursor.x, text_bottom + font_height, cursor.x + width, text_bottom]).render(prawn)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def scaled_font_size(prawn, method_name, size)
|
|
247
|
+
(prawn.font.public_send(method_name) / prawn.font_size) * size
|
|
248
|
+
end
|
|
249
|
+
|
|
159
250
|
def calculate_text_rendering_mode
|
|
160
251
|
fill = !component.computed_properties.fill.none? # rubocop:disable Style/InverseMethods
|
|
161
252
|
stroke = !component.computed_properties.stroke.none? # rubocop:disable Style/InverseMethods
|
data/lib/prawn/svg/elements.rb
CHANGED
|
@@ -4,7 +4,7 @@ end
|
|
|
4
4
|
|
|
5
5
|
require 'prawn/svg/elements/call_duplicator'
|
|
6
6
|
|
|
7
|
-
%w[base direct_render_base root container clip_path viewport text text_component text_node line polyline polygon circle ellipse
|
|
7
|
+
%w[base direct_render_base root anchor container clip_path mask viewport text text_component text_node line polyline polygon circle ellipse
|
|
8
8
|
rect path use image gradient marker ignored].each do |filename|
|
|
9
9
|
require "prawn/svg/elements/#{filename}"
|
|
10
10
|
end
|
|
@@ -14,7 +14,7 @@ module Prawn::SVG::Elements
|
|
|
14
14
|
g: Prawn::SVG::Elements::Container,
|
|
15
15
|
symbol: Prawn::SVG::Elements::Container,
|
|
16
16
|
defs: Prawn::SVG::Elements::Container,
|
|
17
|
-
a: Prawn::SVG::Elements::
|
|
17
|
+
a: Prawn::SVG::Elements::Anchor,
|
|
18
18
|
clipPath: Prawn::SVG::Elements::ClipPath,
|
|
19
19
|
switch: Prawn::SVG::Elements::Container,
|
|
20
20
|
svg: Prawn::SVG::Elements::Viewport,
|
|
@@ -38,6 +38,6 @@ module Prawn::SVG::Elements
|
|
|
38
38
|
foreignObject: Prawn::SVG::Elements::Ignored,
|
|
39
39
|
'font-face': Prawn::SVG::Elements::Ignored,
|
|
40
40
|
filter: Prawn::SVG::Elements::Ignored, # unsupported
|
|
41
|
-
mask: Prawn::SVG::Elements::
|
|
41
|
+
mask: Prawn::SVG::Elements::Mask
|
|
42
42
|
}.freeze
|
|
43
43
|
end
|
|
@@ -4,33 +4,44 @@ class Prawn::SVG::FontMetrics
|
|
|
4
4
|
DEFAULT_X_HEIGHT_RATIO = 0.5
|
|
5
5
|
|
|
6
6
|
def x_height_in_points(pdf, font_size)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
cache_key = cache_key_for(pdf)
|
|
10
|
-
|
|
11
|
-
@x_height_cache[cache_key] ||= calculate_x_height_ratio(pdf)
|
|
12
|
-
@x_height_cache[cache_key] * font_size
|
|
7
|
+
x_height = cache(:x_height, pdf.font) { calculate_x_height_ratio(pdf) }
|
|
8
|
+
x_height * font_size
|
|
13
9
|
end
|
|
14
10
|
|
|
15
11
|
def underline_metrics(pdf, size)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@underline_metrics_cache[cache_key] ||= fetch_underline_metrics(pdf, size)
|
|
21
|
-
@underline_metrics_cache[cache_key]
|
|
12
|
+
cache(:underline, pdf.font, size) do
|
|
13
|
+
fetch_underline_metrics(pdf, size)
|
|
14
|
+
end
|
|
22
15
|
end
|
|
23
16
|
|
|
24
17
|
private
|
|
25
18
|
|
|
26
|
-
def
|
|
27
|
-
|
|
19
|
+
def cache(name, *args)
|
|
20
|
+
@font_metrics_cache ||= {}
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
cache_key = generate_cache_key([name, *args])
|
|
23
|
+
return @font_metrics_cache[cache_key] if @font_metrics_cache.key?(cache_key)
|
|
24
|
+
|
|
25
|
+
@font_metrics_cache[cache_key] = yield
|
|
26
|
+
@font_metrics_cache[cache_key]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def generate_cache_key(key)
|
|
30
|
+
case
|
|
31
|
+
when key.is_a?(Prawn::Fonts::TTF) then font_cache_key(key)
|
|
32
|
+
when key.is_a?(Prawn::Font) then 'default'
|
|
33
|
+
when key.is_a?(Array) then key.map { |element| generate_cache_key(element) }.join('/')
|
|
34
|
+
when key.respond_to?(:to_a) then generate_cache_key(key.to_a)
|
|
35
|
+
else key.to_s
|
|
36
|
+
end.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def font_cache_key(font)
|
|
40
|
+
ttf = font.ttf
|
|
30
41
|
return 'default' unless ttf
|
|
31
42
|
|
|
32
43
|
# Use font family name from TTF metadata, which doesn't include size
|
|
33
|
-
ttf.name&.font_family&.first ||
|
|
44
|
+
ttf.name&.font_family&.first || font&.name || 'default'
|
|
34
45
|
end
|
|
35
46
|
|
|
36
47
|
def calculate_x_height_ratio(pdf)
|
|
@@ -194,9 +194,17 @@ class Prawn::SVG::GradientRenderer
|
|
|
194
194
|
end
|
|
195
195
|
|
|
196
196
|
def current_pdf_transform
|
|
197
|
-
@current_pdf_transform ||=
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
@current_pdf_transform ||= if prawn.state.page.in_stamp_stream?
|
|
198
|
+
# Inside a soft_mask Form XObject, the CTM resets to identity but Prawn's
|
|
199
|
+
# internal tracking still reflects the page CTM. The geometry coordinates
|
|
200
|
+
# only have the bounds offset applied (no scaling), so the pattern matrix
|
|
201
|
+
# should match by using only the bounds translation.
|
|
202
|
+
current_pdf_translation
|
|
203
|
+
else
|
|
204
|
+
load_matrix(
|
|
205
|
+
prawn.current_transformation_matrix_with_translation(*prawn.bounds.anchor)
|
|
206
|
+
)
|
|
207
|
+
end
|
|
200
208
|
end
|
|
201
209
|
|
|
202
210
|
def current_pdf_translation
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Prawn::SVG::LinkRenderer
|
|
2
|
+
include Prawn::SVG::PDFMatrix
|
|
3
|
+
|
|
4
|
+
def initialize(href, bounding_box)
|
|
5
|
+
@href = href
|
|
6
|
+
@bounding_box = bounding_box
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def render(prawn)
|
|
10
|
+
prawn.link_annotation(transformed_bounding_box(prawn), {
|
|
11
|
+
Border: [0, 0, 0],
|
|
12
|
+
A: { Type: :Action, S: :URI, URI: PDF::Core::LiteralString.new(href) }
|
|
13
|
+
})
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :href, :bounding_box
|
|
19
|
+
|
|
20
|
+
def transformed_bounding_box(prawn)
|
|
21
|
+
x0, y0, x1, y1 = bounding_box
|
|
22
|
+
|
|
23
|
+
matrix = load_matrix(prawn.current_transformation_matrix_with_translation(*prawn.bounds.anchor))
|
|
24
|
+
|
|
25
|
+
corners = [
|
|
26
|
+
matrix * Vector[x0, y0, 1.0],
|
|
27
|
+
matrix * Vector[x0, y1, 1.0],
|
|
28
|
+
matrix * Vector[x1, y0, 1.0],
|
|
29
|
+
matrix * Vector[x1, y1, 1.0]
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
xs = corners.map { |c| c[0] }
|
|
33
|
+
ys = corners.map { |c| c[1] }
|
|
34
|
+
|
|
35
|
+
tx0, tx1 = xs.minmax
|
|
36
|
+
ty0, ty1 = ys.minmax
|
|
37
|
+
|
|
38
|
+
[tx0, ty0, tx1, ty1]
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/prawn/svg/properties.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Prawn::SVG
|
|
|
32
32
|
'marker-end' => Config.new('none', true, [:funciri, 'none']),
|
|
33
33
|
'marker-mid' => Config.new('none', true, [:funciri, 'none']),
|
|
34
34
|
'marker-start' => Config.new('none', true, [:funciri, 'none']),
|
|
35
|
+
'mask' => Config.new('none', false, [:funciri, 'none']),
|
|
35
36
|
'opacity' => Config.new(1.0, false, [:number]),
|
|
36
37
|
'overflow' => Config.new('visible', false, %w[visible hidden scroll auto]),
|
|
37
38
|
'stop-color' => Config.new(Color.black, false, [:color_with_icc, 'currentcolor']),
|