prawn-svg 0.37.0 → 0.38.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/Gemfile.lock +1 -1
- data/lib/prawn/svg/attributes/transform.rb +2 -0
- data/lib/prawn/svg/elements/base.rb +13 -1
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/polygon.rb +2 -2
- data/lib/prawn/svg/elements/text.rb +54 -52
- data/lib/prawn/svg/elements/text_component.rb +176 -198
- data/lib/prawn/svg/elements/text_node.rb +174 -0
- data/lib/prawn/svg/elements.rb +1 -1
- data/lib/prawn/svg/font.rb +13 -57
- data/lib/prawn/svg/font_metrics.rb +97 -0
- data/lib/prawn/svg/font_registry.rb +119 -55
- data/lib/prawn/svg/renderer.rb +20 -64
- data/lib/prawn/svg/version.rb +1 -1
- data/lib/prawn-svg.rb +1 -0
- data/scripts/compare_samples +25 -0
- data/spec/prawn/svg/attributes/transform_spec.rb +4 -0
- data/spec/prawn/svg/elements/text_spec.rb +210 -130
- data/spec/prawn/svg/font_registry_spec.rb +121 -10
- data/spec/prawn/svg/font_spec.rb +30 -8
- data/spec/sample_svg/bytes.svg +121 -0
- data/spec/sample_svg/positioning.svg +26 -0
- data/spec/sample_svg/subfamilies.svg +5 -1
- data/spec/sample_svg/text_use.svg +37 -0
- metadata +9 -3
- data/lib/prawn/svg/elements/depth_first_base.rb +0 -52
@@ -0,0 +1,174 @@
|
|
1
|
+
module Prawn::SVG
|
2
|
+
class Elements::TextNode
|
3
|
+
Chunk = Struct.new(:text, :x, :y, :dx, :dy, :rotate, :base_width, :offset, :fixed_width)
|
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
|
+
opts = { size: component.computed_properties.numeric_font_size, kerning: true }
|
77
|
+
|
78
|
+
total_spacing = text_to_draw.length > 1 ? (component.letter_spacing_pixels || 0) * (text_to_draw.length - 1) : 0
|
79
|
+
base_width = prawn.width_of(text_to_draw, opts) + total_spacing
|
80
|
+
|
81
|
+
offset = dx ? [0, dx].max : 0
|
82
|
+
|
83
|
+
@chunks << Chunk.new(text_to_draw, x, y, dx, dy, rotate, base_width, offset, nil)
|
84
|
+
|
85
|
+
if remaining
|
86
|
+
remaining_text = remaining_text[1..]
|
87
|
+
else
|
88
|
+
# we can get to this path with rotations still pending
|
89
|
+
# solve this by shifting them out by the number of
|
90
|
+
# characters we've just drawn
|
91
|
+
shift = remaining_text.length - 1
|
92
|
+
if rotation_remaining && shift.positive?
|
93
|
+
comp = component
|
94
|
+
while comp
|
95
|
+
count = [shift, comp.rotation.length - 1].min
|
96
|
+
comp.rotation.shift(count) if count.positive?
|
97
|
+
comp = comp.parent_component
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
break
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def render(prawn, size, cursor, y_offset)
|
107
|
+
chunks.each do |chunk|
|
108
|
+
cursor.x = chunk.x if chunk.x
|
109
|
+
cursor.x += chunk.dx if chunk.dx
|
110
|
+
cursor.y = chunk.y if chunk.y
|
111
|
+
cursor.y -= chunk.dy if chunk.dy
|
112
|
+
|
113
|
+
render_underline(prawn, size, cursor, y_offset, chunk.fixed_width || chunk.base_width) if component.computed_properties.text_decoration == 'underline'
|
114
|
+
|
115
|
+
opts = { size: size, at: [cursor.x, cursor.y + (y_offset || 0)] }
|
116
|
+
opts[:rotate] = chunk.rotate if chunk.rotate
|
117
|
+
|
118
|
+
scaling =
|
119
|
+
if chunk.fixed_width && component.current_length_adjust_is_scaling?
|
120
|
+
chunk.fixed_width * 100 / chunk.base_width
|
121
|
+
else
|
122
|
+
100
|
123
|
+
end
|
124
|
+
|
125
|
+
spacing_enabled = chunk.fixed_width && !component.current_length_adjust_is_scaling? && chunk.text.length > 1
|
126
|
+
|
127
|
+
# This isn't perfect. It assumes the parent component which started the textLength context
|
128
|
+
# has a character at the end of its text nodes. If it doesn't, the last character in its
|
129
|
+
# children should not take the space. This is possible but would involve a lot more work so
|
130
|
+
# I will park it for now.
|
131
|
+
parent_spacing = spacing_enabled && !component.text_length
|
132
|
+
spacing =
|
133
|
+
if spacing_enabled
|
134
|
+
((chunk.fixed_width - chunk.base_width) / (chunk.text.length - (parent_spacing ? 0 : 1))) + (component.letter_spacing_pixels || 0)
|
135
|
+
end
|
136
|
+
|
137
|
+
prawn.horizontal_text_scaling(scaling) do
|
138
|
+
prawn.character_spacing(spacing || component.letter_spacing_pixels || prawn.character_spacing) do
|
139
|
+
prawn.text_rendering_mode(calculate_text_rendering_mode) do
|
140
|
+
prawn.draw_text(chunk.text, **opts)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
cursor.x += chunk.fixed_width || chunk.base_width
|
146
|
+
|
147
|
+
# If we're in a textLength context for one of our parents, we'll need to add spacing
|
148
|
+
# to the end of our string. See comment above for why this isn't quite right.
|
149
|
+
cursor.x += spacing if parent_spacing
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def render_underline(prawn, size, cursor, y_offset, width)
|
154
|
+
offset, thickness = FontMetrics.underline_metrics(prawn, size)
|
155
|
+
|
156
|
+
prawn.fill_rectangle [cursor.x, cursor.y + (y_offset || 0) + offset + (thickness / 2.0)], width, thickness
|
157
|
+
end
|
158
|
+
|
159
|
+
def calculate_text_rendering_mode
|
160
|
+
fill = !component.computed_properties.fill.none? # rubocop:disable Style/InverseMethods
|
161
|
+
stroke = !component.computed_properties.stroke.none? # rubocop:disable Style/InverseMethods
|
162
|
+
|
163
|
+
if fill && stroke
|
164
|
+
:fill_stroke
|
165
|
+
elsif fill
|
166
|
+
:fill
|
167
|
+
elsif stroke
|
168
|
+
:stroke
|
169
|
+
else
|
170
|
+
:invisible
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
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
|
7
|
+
%w[base direct_render_base root container clip_path 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
|
data/lib/prawn/svg/font.rb
CHANGED
@@ -1,65 +1,21 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
'sans-serif' => 'Helvetica',
|
5
|
-
'cursive' => 'Times-Roman',
|
6
|
-
'fantasy' => 'Times-Roman',
|
7
|
-
'monospace' => 'Courier'
|
8
|
-
}.freeze
|
1
|
+
module Prawn::SVG
|
2
|
+
class Font
|
3
|
+
attr_reader :name, :weight, :style
|
9
4
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
when '100', '200', '300' then :light
|
15
|
-
when '400', '500', 'normal' then :normal
|
16
|
-
when '600' then :semibold
|
17
|
-
when '700', 'bold' then :bold
|
18
|
-
when '800' then :extrabold
|
19
|
-
when '900' then :black
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def initialize(name, weight, style, font_registry: nil)
|
24
|
-
@font_registry = font_registry
|
25
|
-
unless font_registry.installed_fonts.key?(name)
|
26
|
-
# map generic font name to one of the built-in PDF fonts if not already mapped
|
27
|
-
name = GENERIC_CSS_FONT_MAPPING[name] || name
|
5
|
+
def initialize(name, weight, style)
|
6
|
+
@name = name
|
7
|
+
@weight = weight
|
8
|
+
@style = style
|
28
9
|
end
|
29
|
-
@name = font_registry.correctly_cased_font_name(name) || name
|
30
|
-
@weight = weight
|
31
|
-
@style = style
|
32
|
-
end
|
33
|
-
|
34
|
-
def installed?
|
35
|
-
subfamilies = @font_registry.installed_fonts[name]
|
36
|
-
!subfamilies.nil? && subfamilies.key?(subfamily)
|
37
|
-
end
|
38
10
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
elsif subfamilies.key?(:normal)
|
45
|
-
:normal
|
11
|
+
def subfamily
|
12
|
+
if weight == :normal && style
|
13
|
+
style
|
14
|
+
elsif weight || style
|
15
|
+
[weight, style].compact.join('_').to_sym
|
46
16
|
else
|
47
|
-
|
17
|
+
:normal
|
48
18
|
end
|
49
19
|
end
|
50
20
|
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
# Construct a subfamily name from the weight and style information.
|
55
|
-
# Note that this name might not actually exist in the font.
|
56
|
-
def subfamily_name
|
57
|
-
if weight == :normal && style
|
58
|
-
style
|
59
|
-
elsif weight || style
|
60
|
-
[weight, style].compact.join('_').to_sym
|
61
|
-
else
|
62
|
-
:normal
|
63
|
-
end
|
64
|
-
end
|
65
21
|
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
class Prawn::SVG::FontMetrics
|
2
|
+
class << self
|
3
|
+
# Default x-height as a fraction of font size (typical for most fonts)
|
4
|
+
DEFAULT_X_HEIGHT_RATIO = 0.5
|
5
|
+
|
6
|
+
def x_height_in_points(pdf, font_size)
|
7
|
+
@x_height_cache ||= {}
|
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
|
13
|
+
end
|
14
|
+
|
15
|
+
def underline_metrics(pdf, size)
|
16
|
+
@underline_metrics_cache ||= {}
|
17
|
+
|
18
|
+
cache_key = cache_key_for(pdf)
|
19
|
+
|
20
|
+
@underline_metrics_cache[cache_key] ||= fetch_underline_metrics(pdf, size)
|
21
|
+
@underline_metrics_cache[cache_key]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def cache_key_for(pdf)
|
27
|
+
return 'default' unless pdf && pdf.font.is_a?(Prawn::Fonts::TTF)
|
28
|
+
|
29
|
+
ttf = pdf.font.ttf
|
30
|
+
return 'default' unless ttf
|
31
|
+
|
32
|
+
# Use font family name from TTF metadata, which doesn't include size
|
33
|
+
ttf.name&.font_family&.first || pdf&.font&.name || 'default'
|
34
|
+
end
|
35
|
+
|
36
|
+
def calculate_x_height_ratio(pdf)
|
37
|
+
return DEFAULT_X_HEIGHT_RATIO unless pdf && pdf.font.is_a?(Prawn::Fonts::TTF)
|
38
|
+
|
39
|
+
ttf = pdf.font.ttf
|
40
|
+
return DEFAULT_X_HEIGHT_RATIO unless ttf
|
41
|
+
|
42
|
+
units_per_em = ttf.header&.units_per_em&.to_f
|
43
|
+
return DEFAULT_X_HEIGHT_RATIO unless units_per_em&.positive?
|
44
|
+
|
45
|
+
cmap = ttf.cmap&.unicode&.first
|
46
|
+
return DEFAULT_X_HEIGHT_RATIO unless cmap
|
47
|
+
|
48
|
+
xid = cmap['x'.ord]
|
49
|
+
return DEFAULT_X_HEIGHT_RATIO unless xid
|
50
|
+
|
51
|
+
bbox = ttf.glyph_outlines&.for(xid)
|
52
|
+
return DEFAULT_X_HEIGHT_RATIO unless bbox
|
53
|
+
|
54
|
+
y_max = bbox.y_max
|
55
|
+
y_min = bbox.y_min
|
56
|
+
return DEFAULT_X_HEIGHT_RATIO unless y_max && y_min
|
57
|
+
|
58
|
+
glyph_height_units = y_max - y_min
|
59
|
+
return DEFAULT_X_HEIGHT_RATIO if glyph_height_units <= 0
|
60
|
+
|
61
|
+
glyph_height_units / units_per_em
|
62
|
+
end
|
63
|
+
|
64
|
+
def fetch_underline_metrics(pdf, size)
|
65
|
+
units_per_em = nil
|
66
|
+
pos_units = thick_units = nil
|
67
|
+
if pdf.font.is_a?(Prawn::Font::TTF)
|
68
|
+
ttf = begin
|
69
|
+
pdf.font.ttf
|
70
|
+
rescue StandardError
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
if ttf.respond_to?(:post) && ttf.post && ttf.respond_to?(:header) && ttf.header
|
74
|
+
units_per_em = ttf.header.units_per_em.to_f
|
75
|
+
pos_units = ttf.post.underline_position.to_f
|
76
|
+
thick_units = ttf.post.underline_thickness.to_f
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
offset =
|
81
|
+
if units_per_em && pos_units
|
82
|
+
(pos_units / units_per_em) * size
|
83
|
+
else
|
84
|
+
-0.12 * size
|
85
|
+
end
|
86
|
+
|
87
|
+
thick =
|
88
|
+
if units_per_em && thick_units&.positive?
|
89
|
+
[(thick_units / units_per_em) * size, 0.5].max
|
90
|
+
else
|
91
|
+
[size * 0.06, 0.5].max
|
92
|
+
end
|
93
|
+
|
94
|
+
[offset, thick]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -1,75 +1,139 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
'
|
7
|
-
'
|
8
|
-
|
9
|
-
|
10
|
-
@font_path = DEFAULT_FONT_PATHS.select { |path| Dir.exist?(path) }
|
11
|
-
|
12
|
-
def initialize(font_families)
|
13
|
-
@font_families = font_families
|
14
|
-
end
|
1
|
+
module Prawn::SVG
|
2
|
+
class FontRegistry
|
3
|
+
GENERIC_CSS_FONT_MAPPING = {
|
4
|
+
'serif' => 'Times-Roman',
|
5
|
+
'sans-serif' => 'Helvetica',
|
6
|
+
'cursive' => 'Times-Roman',
|
7
|
+
'fantasy' => 'Times-Roman',
|
8
|
+
'monospace' => 'Courier'
|
9
|
+
}.freeze
|
15
10
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
11
|
+
FONT_WEIGHT_FALLBACKS = {
|
12
|
+
light: :normal,
|
13
|
+
normal: nil,
|
14
|
+
semibold: :bold,
|
15
|
+
bold: :normal,
|
16
|
+
extrabold: :bold,
|
17
|
+
black: :extrabold
|
18
|
+
}.freeze
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
FONT_WEIGHTS = FONT_WEIGHT_FALLBACKS.keys.freeze
|
21
|
+
|
22
|
+
DEFAULT_FONT_PATHS = [
|
23
|
+
'/Library/Fonts',
|
24
|
+
'/System/Library/Fonts',
|
25
|
+
"#{Dir.home}/Library/Fonts",
|
26
|
+
'/usr/share/fonts/truetype',
|
27
|
+
'/mnt/c/Windows/Fonts' # Bash on Ubuntu on Windows
|
28
|
+
].freeze
|
25
29
|
|
26
|
-
|
27
|
-
Prawn::SVG::CSS::FontFamilyParser.parse(family).detect do |name|
|
28
|
-
name = name.gsub(/\s{2,}/, ' ').downcase
|
30
|
+
@font_path = DEFAULT_FONT_PATHS.select { |path| Dir.exist?(path) }
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
+
def initialize(font_families)
|
33
|
+
@font_families = font_families
|
32
34
|
end
|
33
|
-
end
|
34
35
|
|
35
|
-
|
36
|
+
def installed_fonts
|
37
|
+
merge_external_fonts
|
38
|
+
@font_families
|
39
|
+
end
|
36
40
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
def correctly_cased_font_name(name)
|
42
|
+
merge_external_fonts
|
43
|
+
@font_case_mapping[name.downcase]
|
44
|
+
end
|
45
|
+
|
46
|
+
def load(family, weight = nil, style = nil)
|
47
|
+
weight = weight_for_css_font_weight(weight) unless FONT_WEIGHTS.include?(weight)
|
48
|
+
|
49
|
+
CSS::FontFamilyParser.parse(family).detect do |name|
|
50
|
+
name = name.gsub(/\s{2,}/, ' ')
|
51
|
+
|
52
|
+
font = find_suitable_font(name, weight, style)
|
53
|
+
break font if font
|
45
54
|
end
|
46
55
|
end
|
47
|
-
end
|
48
56
|
|
49
|
-
|
50
|
-
|
57
|
+
private
|
58
|
+
|
59
|
+
def find_suitable_font(name, weight, style)
|
60
|
+
name = correctly_cased_font_name(name) || name
|
61
|
+
name = GENERIC_CSS_FONT_MAPPING[name] if GENERIC_CSS_FONT_MAPPING.key?(name)
|
51
62
|
|
52
|
-
|
53
|
-
|
63
|
+
return unless (subfamilies = installed_fonts[name])
|
64
|
+
return if subfamilies.empty?
|
54
65
|
|
55
|
-
|
56
|
-
|
57
|
-
|
66
|
+
while weight
|
67
|
+
font = Font.new(name, weight, style)
|
68
|
+
return font if installed?(font)
|
69
|
+
|
70
|
+
weight = FONT_WEIGHT_FALLBACKS[weight]
|
71
|
+
end
|
58
72
|
|
59
|
-
|
60
|
-
|
61
|
-
|
73
|
+
if style
|
74
|
+
find_suitable_font(name, weight, nil) unless style.nil?
|
75
|
+
else
|
76
|
+
Font.new(name, subfamilies.keys.first, nil)
|
62
77
|
end
|
63
78
|
end
|
64
79
|
|
65
|
-
|
80
|
+
def installed?(font)
|
81
|
+
subfamilies = installed_fonts[font.name]
|
82
|
+
!subfamilies.nil? && subfamilies.key?(font.subfamily)
|
83
|
+
end
|
84
|
+
|
85
|
+
def weight_for_css_font_weight(weight)
|
86
|
+
case weight
|
87
|
+
when '100', '200', '300' then :light
|
88
|
+
when '400', '500', 'normal' then :normal
|
89
|
+
when '600' then :semibold
|
90
|
+
when '700', 'bold' then :bold
|
91
|
+
when '800' then :extrabold
|
92
|
+
when '900' then :black
|
93
|
+
else :normal # rubocop:disable Lint/DuplicateBranch
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def merge_external_fonts
|
98
|
+
if @font_case_mapping.nil?
|
99
|
+
self.class.load_external_fonts unless self.class.external_font_families
|
100
|
+
@font_families.merge!(self.class.external_font_families) do |_key, v1, _v2|
|
101
|
+
v1
|
102
|
+
end
|
103
|
+
@font_case_mapping = @font_families.keys.each.with_object({}) do |key, result|
|
104
|
+
result[key.downcase] = key
|
105
|
+
end
|
106
|
+
GENERIC_CSS_FONT_MAPPING.each_key do |generic|
|
107
|
+
@font_case_mapping[generic] = generic
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
66
111
|
|
67
|
-
|
68
|
-
font_path
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
112
|
+
class << self
|
113
|
+
attr_reader :external_font_families, :font_path
|
114
|
+
|
115
|
+
def load_external_fonts
|
116
|
+
@external_font_families = {}
|
117
|
+
|
118
|
+
external_font_paths.each do |filename|
|
119
|
+
ttf = TTF.new(filename)
|
120
|
+
next unless ttf.family
|
121
|
+
|
122
|
+
subfamily = (ttf.subfamily || 'normal').gsub(/\s+/, '_').downcase.to_sym
|
123
|
+
subfamily = :normal if subfamily == :regular
|
124
|
+
(external_font_families[ttf.family] ||= {})[subfamily] ||= filename
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def external_font_paths
|
131
|
+
font_path
|
132
|
+
.uniq
|
133
|
+
.flat_map { |path| Dir["#{path}/**/*"] }
|
134
|
+
.uniq
|
135
|
+
.select { |path| File.file?(path) }
|
136
|
+
end
|
73
137
|
end
|
74
138
|
end
|
75
139
|
end
|
data/lib/prawn/svg/renderer.rb
CHANGED
@@ -50,6 +50,10 @@ module Prawn
|
|
50
50
|
options[:at] || [x_based_on_requested_alignment, y_based_on_requested_alignment]
|
51
51
|
end
|
52
52
|
|
53
|
+
def render_calls(prawn, calls)
|
54
|
+
issue_prawn_command(prawn, calls)
|
55
|
+
end
|
56
|
+
|
53
57
|
private
|
54
58
|
|
55
59
|
def x_based_on_requested_alignment
|
@@ -100,80 +104,32 @@ module Prawn
|
|
100
104
|
if skip
|
101
105
|
# the call has been overridden
|
102
106
|
elsif children.empty? && call != 'transparent' # some prawn calls complain if they aren't supplied a block
|
103
|
-
|
104
|
-
prawn.send(call, *arguments, **kwarguments)
|
105
|
-
else
|
106
|
-
prawn.send(call, *arguments)
|
107
|
-
end
|
108
|
-
elsif RUBY_VERSION >= '2.7' || !kwarguments.empty?
|
109
|
-
prawn.send(call, *arguments, **kwarguments, &proc_creator(prawn, children))
|
107
|
+
prawn.send(call, *arguments, **kwarguments)
|
110
108
|
else
|
111
|
-
prawn.send(call, *arguments, &proc_creator(prawn, children))
|
109
|
+
prawn.send(call, *arguments, **kwarguments, &proc_creator(prawn, children))
|
112
110
|
end
|
113
111
|
end
|
114
112
|
end
|
115
113
|
|
116
114
|
def rewrite_call_arguments(prawn, call, arguments, kwarguments)
|
117
115
|
case call
|
118
|
-
when '
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
at[0] = @cursor[0] if at[0] == :relative
|
129
|
-
at[1] = @cursor[1] if at[1] == :relative
|
130
|
-
|
131
|
-
case options.delete(:dominant_baseline)
|
132
|
-
when 'middle'
|
133
|
-
height = prawn.font.height
|
134
|
-
at[1] -= height / 2.0
|
135
|
-
@cursor = [at[0], at[1]]
|
136
|
-
end
|
137
|
-
|
138
|
-
if (offset = options.delete(:offset))
|
139
|
-
at[0] += offset[0]
|
140
|
-
at[1] -= offset[1]
|
116
|
+
when 'svg:render'
|
117
|
+
element = arguments.first
|
118
|
+
raise ArgumentError, "Expected a Prawn::SVG::Elements::DirectRenderBase, got #{element.class}" unless element.is_a?(Prawn::SVG::Elements::DirectRenderBase)
|
119
|
+
|
120
|
+
begin
|
121
|
+
element.render(prawn, self)
|
122
|
+
rescue Prawn::SVG::Elements::Base::SkipElementQuietly
|
123
|
+
rescue Prawn::SVG::Elements::Base::SkipElementError => e
|
124
|
+
@document.warnings << e.message
|
141
125
|
end
|
142
126
|
|
143
|
-
|
144
|
-
|
145
|
-
if (stretch_to_width = options.delete(:stretch_to_width))
|
146
|
-
factor = stretch_to_width.to_f * 100 / width.to_f
|
147
|
-
prawn.add_content "#{factor} Tz"
|
148
|
-
width = stretch_to_width.to_f
|
149
|
-
end
|
150
|
-
|
151
|
-
if (pad_to_width = options.delete(:pad_to_width))
|
152
|
-
padding_required = pad_to_width.to_f - width.to_f
|
153
|
-
padding_per_character = padding_required / text.length.to_f
|
154
|
-
prawn.add_content "#{padding_per_character} Tc"
|
155
|
-
width = pad_to_width.to_f
|
156
|
-
end
|
157
|
-
|
158
|
-
case options.delete(:text_anchor)
|
159
|
-
when 'middle'
|
160
|
-
at[0] -= width / 2
|
161
|
-
@cursor = [at[0] + (width / 2), at[1]]
|
162
|
-
when 'end'
|
163
|
-
at[0] -= width
|
164
|
-
@cursor = at.dup
|
165
|
-
else
|
166
|
-
@cursor = [at[0] + width, at[1]]
|
167
|
-
end
|
127
|
+
yield
|
168
128
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
prawn.line [at[0], at[1] - 1.25], [at[0] + width, at[1] - 1.25]
|
174
|
-
prawn.stroke
|
175
|
-
end
|
176
|
-
end
|
129
|
+
when 'svg:yield'
|
130
|
+
block = arguments.first
|
131
|
+
block.call
|
132
|
+
yield
|
177
133
|
|
178
134
|
when 'transformation_matrix'
|
179
135
|
left = prawn.bounds.absolute_left
|
data/lib/prawn/svg/version.rb
CHANGED
data/lib/prawn-svg.rb
CHANGED
@@ -6,6 +6,7 @@ require 'prawn/svg/version'
|
|
6
6
|
require 'css_parser'
|
7
7
|
|
8
8
|
require 'prawn/svg/font_registry'
|
9
|
+
require 'prawn/svg/font_metrics'
|
9
10
|
require 'prawn/svg/calculators/arc_to_bezier_curve'
|
10
11
|
require 'prawn/svg/calculators/aspect_ratio'
|
11
12
|
require 'prawn/svg/calculators/document_sizing'
|