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.
Files changed (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. 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
@@ -0,0 +1,14 @@
1
+ Prawn::SVG::FuncIRI = Struct.new(:url) do
2
+ def self.parse(value)
3
+ case Prawn::SVG::CSS::ValuesParser.parse(value)
4
+ in [['url', [url]]]
5
+ new(url.strip)
6
+ else
7
+ nil
8
+ end
9
+ end
10
+
11
+ def to_s
12
+ "url(#{url})"
13
+ end
14
+ end