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,20 @@
|
|
|
1
|
+
module Prawn::SVG
|
|
2
|
+
class Font
|
|
3
|
+
attr_reader :name, :weight, :style, :stretch
|
|
4
|
+
|
|
5
|
+
def initialize(name, weight, style, stretch = nil)
|
|
6
|
+
@name = name
|
|
7
|
+
@weight = weight
|
|
8
|
+
@style = style
|
|
9
|
+
@stretch = stretch
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def subfamily
|
|
13
|
+
parts = []
|
|
14
|
+
parts << stretch if stretch && stretch != :normal
|
|
15
|
+
parts << weight if weight && weight != :normal
|
|
16
|
+
parts << style if style
|
|
17
|
+
parts.empty? ? :normal : parts.join('_').to_sym
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
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(:x_height, pdf.font) { calculate_x_height_ratio(pdf) }
|
|
8
|
+
x_height * font_size
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def underline_metrics(pdf, size)
|
|
12
|
+
cache(:underline, pdf.font, size) do
|
|
13
|
+
fetch_underline_metrics(pdf, size)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def overline_metrics(pdf, size)
|
|
18
|
+
cache(:overline, pdf.font, size) do
|
|
19
|
+
fetch_overline_metrics(pdf, size)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def strikethrough_metrics(pdf, size)
|
|
24
|
+
cache(:strikethrough, pdf.font, size) do
|
|
25
|
+
fetch_strikethrough_metrics(pdf, size)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def cache(name, *args)
|
|
32
|
+
@font_metrics_cache ||= {}
|
|
33
|
+
|
|
34
|
+
cache_key = generate_cache_key([name, *args])
|
|
35
|
+
return @font_metrics_cache[cache_key] if @font_metrics_cache.key?(cache_key)
|
|
36
|
+
|
|
37
|
+
@font_metrics_cache[cache_key] = yield
|
|
38
|
+
@font_metrics_cache[cache_key]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def generate_cache_key(key)
|
|
42
|
+
case
|
|
43
|
+
when key.is_a?(Prawn::Fonts::TTF) then font_cache_key(key)
|
|
44
|
+
when key.is_a?(Prawn::Font) then 'default'
|
|
45
|
+
when key.is_a?(Array) then key.map { |element| generate_cache_key(element) }.join('/')
|
|
46
|
+
when key.respond_to?(:to_a) then generate_cache_key(key.to_a)
|
|
47
|
+
else key.to_s
|
|
48
|
+
end.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def font_cache_key(font)
|
|
52
|
+
ttf = font.ttf
|
|
53
|
+
return 'default' unless ttf
|
|
54
|
+
|
|
55
|
+
# Use font family name from TTF metadata, which doesn't include size
|
|
56
|
+
ttf.family_name || font&.name || 'default'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def calculate_x_height_ratio(pdf)
|
|
60
|
+
return DEFAULT_X_HEIGHT_RATIO unless pdf && pdf.font.is_a?(Prawn::Fonts::TTF)
|
|
61
|
+
|
|
62
|
+
ttf = pdf.font.ttf
|
|
63
|
+
return DEFAULT_X_HEIGHT_RATIO unless ttf
|
|
64
|
+
|
|
65
|
+
units_per_em = ttf.header.units_per_em.to_f
|
|
66
|
+
return DEFAULT_X_HEIGHT_RATIO unless units_per_em.positive?
|
|
67
|
+
|
|
68
|
+
xid = ttf.glyph_by_code['x'.ord]
|
|
69
|
+
return DEFAULT_X_HEIGHT_RATIO unless xid&.positive?
|
|
70
|
+
|
|
71
|
+
y_min, y_max = ttf.glyph_vertical_bounds(xid)
|
|
72
|
+
return DEFAULT_X_HEIGHT_RATIO unless y_max && y_min
|
|
73
|
+
|
|
74
|
+
glyph_height_units = y_max - y_min
|
|
75
|
+
return DEFAULT_X_HEIGHT_RATIO if glyph_height_units <= 0
|
|
76
|
+
|
|
77
|
+
glyph_height_units / units_per_em
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def fetch_underline_metrics(pdf, size)
|
|
81
|
+
units_per_em = nil
|
|
82
|
+
pos_units = thick_units = nil
|
|
83
|
+
if pdf.font.is_a?(Prawn::Font::TTF)
|
|
84
|
+
ttf = begin
|
|
85
|
+
pdf.font.ttf
|
|
86
|
+
rescue StandardError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
if ttf&.table?('post')
|
|
90
|
+
units_per_em = ttf.header.units_per_em.to_f
|
|
91
|
+
pos_units, thick_units = ttf.underline_metrics.map(&:to_f)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
offset =
|
|
96
|
+
if units_per_em && pos_units
|
|
97
|
+
(pos_units / units_per_em) * size
|
|
98
|
+
else
|
|
99
|
+
-0.12 * size
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
thick =
|
|
103
|
+
if units_per_em && thick_units&.positive?
|
|
104
|
+
[(thick_units / units_per_em) * size, 1.0].max
|
|
105
|
+
else
|
|
106
|
+
[size * 0.07, 1.0].max
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
[offset, thick]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fetch_overline_metrics(pdf, size)
|
|
113
|
+
_, underline_thickness = fetch_underline_metrics(pdf, size)
|
|
114
|
+
[size, underline_thickness]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def fetch_strikethrough_metrics(pdf, size)
|
|
118
|
+
_, underline_thickness = fetch_underline_metrics(pdf, size)
|
|
119
|
+
|
|
120
|
+
units_per_em = nil
|
|
121
|
+
strikeout_pos = nil
|
|
122
|
+
if pdf.font.is_a?(Prawn::Font::TTF)
|
|
123
|
+
ttf = begin
|
|
124
|
+
pdf.font.ttf
|
|
125
|
+
rescue StandardError
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
if ttf&.table?('OS/2')
|
|
129
|
+
units_per_em = ttf.header.units_per_em.to_f
|
|
130
|
+
strikeout_pos = ttf.strikeout_position.to_f
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
offset =
|
|
135
|
+
if units_per_em && strikeout_pos
|
|
136
|
+
(strikeout_pos / units_per_em) * size
|
|
137
|
+
else
|
|
138
|
+
0.3 * size
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
[offset, underline_thickness]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,315 @@
|
|
|
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
|
|
10
|
+
|
|
11
|
+
FONT_WEIGHT_FALLBACKS = {
|
|
12
|
+
thin: :extralight,
|
|
13
|
+
extralight: :light,
|
|
14
|
+
light: :normal,
|
|
15
|
+
normal: nil,
|
|
16
|
+
medium: :normal,
|
|
17
|
+
semibold: :bold,
|
|
18
|
+
bold: :normal,
|
|
19
|
+
extrabold: :bold,
|
|
20
|
+
black: :extrabold
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
FONT_WEIGHTS = FONT_WEIGHT_FALLBACKS.keys.freeze
|
|
24
|
+
|
|
25
|
+
FONT_WEIGHT_VALUES = {
|
|
26
|
+
thin: 100, extralight: 200, light: 300, normal: 400, medium: 500,
|
|
27
|
+
semibold: 600, bold: 700, extrabold: 800, black: 900
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Font files use various names for the same weight. Map common alternatives
|
|
31
|
+
# to the canonical weight names used in FONT_WEIGHTS.
|
|
32
|
+
FONT_WEIGHT_ALIASES = {
|
|
33
|
+
thin: %i[hairline],
|
|
34
|
+
extralight: %i[ultralight ultra_light extra_light],
|
|
35
|
+
semibold: %i[semi_bold demi_bold demibold],
|
|
36
|
+
extrabold: %i[extra_bold ultra_bold ultrabold],
|
|
37
|
+
black: %i[heavy]
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
FONT_STRETCH_MAPPING = {
|
|
41
|
+
'ultra-condensed' => :ultra_condensed,
|
|
42
|
+
'extra-condensed' => :extra_condensed,
|
|
43
|
+
'condensed' => :condensed,
|
|
44
|
+
'semi-condensed' => :semi_condensed,
|
|
45
|
+
'normal' => :normal,
|
|
46
|
+
'semi-expanded' => :semi_expanded,
|
|
47
|
+
'expanded' => :expanded,
|
|
48
|
+
'extra-expanded' => :extra_expanded,
|
|
49
|
+
'ultra-expanded' => :ultra_expanded
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
FONT_STRETCHES = %i[
|
|
53
|
+
ultra_condensed extra_condensed condensed semi_condensed normal
|
|
54
|
+
semi_expanded expanded extra_expanded ultra_expanded
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
DEFAULT_FONT_PATHS = [
|
|
58
|
+
'/Library/Fonts',
|
|
59
|
+
'/System/Library/Fonts',
|
|
60
|
+
"#{Dir.home}/Library/Fonts",
|
|
61
|
+
'/usr/share/fonts/truetype',
|
|
62
|
+
'/mnt/c/Windows/Fonts' # Bash on Ubuntu on Windows
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
65
|
+
@font_path = DEFAULT_FONT_PATHS.select { |path| Dir.exist?(path) }
|
|
66
|
+
|
|
67
|
+
def initialize(font_families)
|
|
68
|
+
@font_families = font_families
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def installed_fonts
|
|
72
|
+
merge_external_fonts
|
|
73
|
+
@font_families
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def correctly_cased_font_name(name)
|
|
77
|
+
merge_external_fonts
|
|
78
|
+
@font_case_mapping[name.downcase]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def load(family, weight = nil, style = nil, stretch = nil)
|
|
82
|
+
weight = weight_for_css_font_weight(weight) unless FONT_WEIGHTS.include?(weight)
|
|
83
|
+
stretch = stretch_for_css_font_stretch(stretch)
|
|
84
|
+
|
|
85
|
+
CSS::FontFamilyParser.parse(family).detect do |name|
|
|
86
|
+
name = name.gsub(/\s{2,}/, ' ')
|
|
87
|
+
|
|
88
|
+
font = find_suitable_font(name, weight, style, stretch)
|
|
89
|
+
break font if font
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def register_font_face(family_name, css_weight, css_style, css_stretch, font_data)
|
|
94
|
+
merge_external_fonts
|
|
95
|
+
|
|
96
|
+
weight = css_weight ? weight_for_css_font_weight(css_weight) : :normal
|
|
97
|
+
style = %w[italic oblique].include?(css_style) ? :italic : nil
|
|
98
|
+
stretch = stretch_for_css_font_stretch(css_stretch)
|
|
99
|
+
effective_stretch = stretch == :normal ? nil : stretch
|
|
100
|
+
|
|
101
|
+
font = Font.new(family_name, weight, style, effective_stretch)
|
|
102
|
+
family = (@font_families[family_name] ||= {})
|
|
103
|
+
family[font.subfamily] = font_data
|
|
104
|
+
@font_case_mapping[family_name.downcase] = family_name
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def find_local_font_data(local_name, css_weight, css_style, css_stretch)
|
|
108
|
+
merge_external_fonts
|
|
109
|
+
|
|
110
|
+
name = correctly_cased_font_name(local_name) || local_name
|
|
111
|
+
name = GENERIC_CSS_FONT_MAPPING[name] if GENERIC_CSS_FONT_MAPPING.key?(name) && !@font_families.key?(name)
|
|
112
|
+
|
|
113
|
+
subfamilies = @font_families[name]
|
|
114
|
+
return unless subfamilies&.any?
|
|
115
|
+
|
|
116
|
+
weight = css_weight ? weight_for_css_font_weight(css_weight) : :normal
|
|
117
|
+
style = %w[italic oblique].include?(css_style) ? :italic : nil
|
|
118
|
+
stretch = stretch_for_css_font_stretch(css_stretch)
|
|
119
|
+
effective_stretch = stretch == :normal ? nil : stretch
|
|
120
|
+
|
|
121
|
+
font = find_suitable_font(name, weight, style, effective_stretch)
|
|
122
|
+
return unless font
|
|
123
|
+
|
|
124
|
+
subfamilies[font.subfamily]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def find_suitable_font(name, weight, style, stretch)
|
|
130
|
+
name = correctly_cased_font_name(name) || name
|
|
131
|
+
|
|
132
|
+
name = GENERIC_CSS_FONT_MAPPING[name] if GENERIC_CSS_FONT_MAPPING.key?(name) && !installed_fonts.key?(name)
|
|
133
|
+
|
|
134
|
+
return unless (subfamilies = installed_fonts[name])
|
|
135
|
+
return if subfamilies.empty?
|
|
136
|
+
|
|
137
|
+
css_stretch_matching_order(stretch).each do |s|
|
|
138
|
+
effective_stretch = s == :normal ? nil : s
|
|
139
|
+
|
|
140
|
+
font = if effective_stretch
|
|
141
|
+
find_with_css_weight_matching(name, weight, style, effective_stretch)
|
|
142
|
+
else
|
|
143
|
+
find_with_weight_fallback(name, weight, style)
|
|
144
|
+
end
|
|
145
|
+
return font if font
|
|
146
|
+
|
|
147
|
+
next unless style
|
|
148
|
+
|
|
149
|
+
font = if effective_stretch
|
|
150
|
+
find_with_css_weight_matching(name, weight, nil, effective_stretch)
|
|
151
|
+
else
|
|
152
|
+
find_with_weight_fallback(name, weight, nil)
|
|
153
|
+
end
|
|
154
|
+
return font if font
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
Font.new(name, subfamilies.keys.first, nil)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def find_with_weight_fallback(name, weight, style, stretch = nil)
|
|
161
|
+
current_weight = weight
|
|
162
|
+
while current_weight
|
|
163
|
+
font = Font.new(name, current_weight, style, stretch)
|
|
164
|
+
return font if installed?(font)
|
|
165
|
+
|
|
166
|
+
FONT_WEIGHT_ALIASES.fetch(current_weight, []).each do |alias_weight|
|
|
167
|
+
font = Font.new(name, alias_weight, style, stretch)
|
|
168
|
+
return font if installed?(font)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
current_weight = FONT_WEIGHT_FALLBACKS[current_weight]
|
|
172
|
+
end
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def find_with_css_weight_matching(name, weight, style, stretch)
|
|
177
|
+
css_weight_matching_order(weight).each do |w|
|
|
178
|
+
font = Font.new(name, w, style, stretch)
|
|
179
|
+
return font if installed?(font)
|
|
180
|
+
|
|
181
|
+
FONT_WEIGHT_ALIASES.fetch(w, []).each do |alias_weight|
|
|
182
|
+
font = Font.new(name, alias_weight, style, stretch)
|
|
183
|
+
return font if installed?(font)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def css_stretch_matching_order(stretch)
|
|
190
|
+
return [:normal] if stretch.nil? || stretch == :normal
|
|
191
|
+
|
|
192
|
+
idx = FONT_STRETCHES.index(stretch) || FONT_STRETCHES.index(:normal)
|
|
193
|
+
normal_idx = FONT_STRETCHES.index(:normal)
|
|
194
|
+
|
|
195
|
+
narrower = FONT_STRETCHES[0...idx].reverse
|
|
196
|
+
wider = FONT_STRETCHES[(idx + 1)..]
|
|
197
|
+
|
|
198
|
+
if idx <= normal_idx
|
|
199
|
+
[stretch] + narrower + wider
|
|
200
|
+
else
|
|
201
|
+
[stretch] + wider + narrower
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def css_weight_matching_order(desired)
|
|
206
|
+
value = FONT_WEIGHT_VALUES[desired] || 400
|
|
207
|
+
below = FONT_WEIGHTS.select { |w| FONT_WEIGHT_VALUES[w] < value }.sort_by { |w| -FONT_WEIGHT_VALUES[w] }
|
|
208
|
+
above = FONT_WEIGHTS.select { |w| FONT_WEIGHT_VALUES[w] > value }.sort_by { |w| FONT_WEIGHT_VALUES[w] }
|
|
209
|
+
|
|
210
|
+
if value <= 500
|
|
211
|
+
[desired] + below + above
|
|
212
|
+
else
|
|
213
|
+
[desired] + above + below
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def installed?(font)
|
|
218
|
+
subfamilies = installed_fonts[font.name]
|
|
219
|
+
!subfamilies.nil? && subfamilies.key?(font.subfamily)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def weight_for_css_font_weight(weight)
|
|
223
|
+
case weight
|
|
224
|
+
when '100' then :thin
|
|
225
|
+
when '200' then :extralight
|
|
226
|
+
when '300' then :light
|
|
227
|
+
when '400', 'normal' then :normal
|
|
228
|
+
when '500' then :medium
|
|
229
|
+
when '600' then :semibold
|
|
230
|
+
when '700', 'bold' then :bold
|
|
231
|
+
when '800' then :extrabold
|
|
232
|
+
when '900' then :black
|
|
233
|
+
else :normal # rubocop:disable Lint/DuplicateBranch
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def stretch_for_css_font_stretch(stretch)
|
|
238
|
+
return nil if stretch.nil?
|
|
239
|
+
|
|
240
|
+
FONT_STRETCH_MAPPING[stretch] || :normal
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def merge_external_fonts
|
|
244
|
+
if @font_case_mapping.nil?
|
|
245
|
+
self.class.load_external_fonts unless self.class.external_font_families
|
|
246
|
+
@font_families.merge!(self.class.external_font_families) do |_key, v1, _v2|
|
|
247
|
+
v1
|
|
248
|
+
end
|
|
249
|
+
@font_case_mapping = @font_families.keys.each.with_object({}) do |key, result|
|
|
250
|
+
result[key.downcase] = key
|
|
251
|
+
end
|
|
252
|
+
GENERIC_CSS_FONT_MAPPING.each_key do |generic|
|
|
253
|
+
@font_case_mapping[generic] = generic
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
class << self
|
|
259
|
+
attr_reader :external_font_families, :font_path
|
|
260
|
+
|
|
261
|
+
def load_external_fonts
|
|
262
|
+
@external_font_families = {}
|
|
263
|
+
|
|
264
|
+
external_font_paths.each do |filename|
|
|
265
|
+
ttc = TTC.new(filename)
|
|
266
|
+
if ttc.fonts.any?
|
|
267
|
+
ttc.fonts.each do |font|
|
|
268
|
+
subfamily = (font[:subfamily] || 'normal').gsub(/\s+/, '_').downcase.to_sym
|
|
269
|
+
subfamily = :normal if subfamily == :regular
|
|
270
|
+
family_hash = (external_font_families[font[:family]] ||= {})
|
|
271
|
+
font_data = { file: filename, font: font[:index] }
|
|
272
|
+
family_hash[subfamily] ||= font_data
|
|
273
|
+
register_by_weight_class(family_hash, font[:weight_class], subfamily, font_data)
|
|
274
|
+
end
|
|
275
|
+
else
|
|
276
|
+
ttf = TTF.new(filename)
|
|
277
|
+
next unless ttf.family
|
|
278
|
+
|
|
279
|
+
subfamily = (ttf.subfamily || 'normal').gsub(/\s+/, '_').downcase.to_sym
|
|
280
|
+
subfamily = :normal if subfamily == :regular
|
|
281
|
+
family_hash = (external_font_families[ttf.family] ||= {})
|
|
282
|
+
family_hash[subfamily] ||= filename
|
|
283
|
+
register_by_weight_class(family_hash, ttf.weight_class, subfamily, family_hash[subfamily])
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
WEIGHT_CLASS_TO_SYMBOL = {
|
|
291
|
+
100 => :thin, 200 => :extralight, 300 => :light, 400 => :normal, 500 => :medium,
|
|
292
|
+
600 => :semibold, 700 => :bold, 800 => :extrabold, 900 => :black
|
|
293
|
+
}.freeze
|
|
294
|
+
|
|
295
|
+
def register_by_weight_class(family_hash, weight_class, subfamily, font_data)
|
|
296
|
+
return unless weight_class
|
|
297
|
+
|
|
298
|
+
subfamily_str = subfamily.to_s
|
|
299
|
+
return if subfamily_str.include?('italic') || subfamily_str.include?('oblique') ||
|
|
300
|
+
subfamily_str.include?('condensed') || subfamily_str.include?('expanded')
|
|
301
|
+
|
|
302
|
+
canonical = WEIGHT_CLASS_TO_SYMBOL[weight_class]
|
|
303
|
+
family_hash[canonical] = font_data if canonical
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def external_font_paths
|
|
307
|
+
font_path
|
|
308
|
+
.uniq
|
|
309
|
+
.flat_map { |path| Dir["#{path}/**/*"] }
|
|
310
|
+
.uniq
|
|
311
|
+
.select { |path| File.file?(path) }
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|