fontisan 0.2.11 → 0.2.12
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/.rubocop_todo.yml +214 -51
- data/README.adoc +160 -0
- data/lib/fontisan/cli.rb +177 -6
- data/lib/fontisan/commands/convert_command.rb +32 -1
- data/lib/fontisan/config/conversion_matrix.yml +132 -4
- data/lib/fontisan/constants.rb +12 -0
- data/lib/fontisan/conversion_options.rb +378 -0
- data/lib/fontisan/converters/collection_converter.rb +45 -10
- data/lib/fontisan/converters/format_converter.rb +2 -0
- data/lib/fontisan/converters/outline_converter.rb +78 -4
- data/lib/fontisan/converters/type1_converter.rb +559 -0
- data/lib/fontisan/font_loader.rb +46 -3
- data/lib/fontisan/type1/afm_generator.rb +436 -0
- data/lib/fontisan/type1/afm_parser.rb +298 -0
- data/lib/fontisan/type1/agl.rb +456 -0
- data/lib/fontisan/type1/charstring_converter.rb +240 -0
- data/lib/fontisan/type1/charstrings.rb +408 -0
- data/lib/fontisan/type1/conversion_options.rb +243 -0
- data/lib/fontisan/type1/decryptor.rb +183 -0
- data/lib/fontisan/type1/encodings.rb +697 -0
- data/lib/fontisan/type1/font_dictionary.rb +514 -0
- data/lib/fontisan/type1/generator.rb +220 -0
- data/lib/fontisan/type1/inf_generator.rb +332 -0
- data/lib/fontisan/type1/pfa_generator.rb +343 -0
- data/lib/fontisan/type1/pfa_parser.rb +158 -0
- data/lib/fontisan/type1/pfb_generator.rb +291 -0
- data/lib/fontisan/type1/pfb_parser.rb +166 -0
- data/lib/fontisan/type1/pfm_generator.rb +610 -0
- data/lib/fontisan/type1/pfm_parser.rb +433 -0
- data/lib/fontisan/type1/private_dict.rb +285 -0
- data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
- data/lib/fontisan/type1/upm_scaler.rb +118 -0
- data/lib/fontisan/type1.rb +73 -0
- data/lib/fontisan/type1_font.rb +331 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -0
- metadata +26 -2
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "upm_scaler"
|
|
4
|
+
require_relative "encodings"
|
|
5
|
+
require_relative "agl"
|
|
6
|
+
|
|
7
|
+
module Fontisan
|
|
8
|
+
module Type1
|
|
9
|
+
# AFM (Adobe Font Metrics) file generator
|
|
10
|
+
#
|
|
11
|
+
# [`AFMGenerator`](lib/fontisan/type1/afm_generator.rb) generates Adobe Font Metrics
|
|
12
|
+
# files from TTF/OTF fonts.
|
|
13
|
+
#
|
|
14
|
+
# AFM files include:
|
|
15
|
+
# - Character widths
|
|
16
|
+
# - Kerning pairs
|
|
17
|
+
# - Character bounding boxes
|
|
18
|
+
# - Font metadata (name, version, copyright, etc.)
|
|
19
|
+
#
|
|
20
|
+
# @example Generate AFM from TTF
|
|
21
|
+
# font = Fontisan::FontLoader.load("font.ttf")
|
|
22
|
+
# afm = Fontisan::Type1::AFMGenerator.generate(font)
|
|
23
|
+
# File.write("font.afm", afm)
|
|
24
|
+
#
|
|
25
|
+
# @example Generate AFM with 1000 UPM scaling
|
|
26
|
+
# afm = Fontisan::Type1::AFMGenerator.generate(font, upm_scale: 1000)
|
|
27
|
+
#
|
|
28
|
+
# @example Generate AFM with Unicode encoding
|
|
29
|
+
# afm = Fontisan::Type1::AFMGenerator.generate(font, encoding: Fontisan::Type1::Encodings::Unicode)
|
|
30
|
+
#
|
|
31
|
+
# @see https://www.adobe.com/devnet/font/pdfs/5004.AFM_Spec.pdf
|
|
32
|
+
class AFMGenerator
|
|
33
|
+
class << self
|
|
34
|
+
# Generate AFM content from a font
|
|
35
|
+
#
|
|
36
|
+
# @param font [Fontisan::TrueTypeFont, Fontisan::OpenTypeFont] The font to generate AFM from
|
|
37
|
+
# @param options [Hash] Generation options
|
|
38
|
+
# @option options [Integer, :native] :upm_scale Target UPM (1000 for Type 1, :native for no scaling)
|
|
39
|
+
# @option options [Class] :encoding Encoding class (default: AdobeStandard)
|
|
40
|
+
# @return [String] AFM file content
|
|
41
|
+
def generate(font, options = {})
|
|
42
|
+
new(font, options).generate_afm
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generate AFM file from a font and write to file
|
|
46
|
+
#
|
|
47
|
+
# @param font [Fontisan::TrueTypeFont, Fontisan::OpenTypeFont] The font to generate AFM from
|
|
48
|
+
# @param path [String] Path to write AFM file
|
|
49
|
+
# @param options [Hash] Generation options
|
|
50
|
+
# @return [void
|
|
51
|
+
def generate_to_file(font, path, options = {})
|
|
52
|
+
afm_content = generate(font, options)
|
|
53
|
+
File.write(path, afm_content, encoding: "ISO-8859-1")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get Adobe glyph name from Unicode codepoint
|
|
57
|
+
#
|
|
58
|
+
# @param codepoint [Integer] Unicode codepoint
|
|
59
|
+
# @param encoding [Class] Encoding class to use (default: nil for direct AGL lookup)
|
|
60
|
+
# @return [String] Adobe glyph name
|
|
61
|
+
def adobe_glyph_name(codepoint, encoding: nil)
|
|
62
|
+
if encoding
|
|
63
|
+
encoding.glyph_name_for_code(codepoint)
|
|
64
|
+
else
|
|
65
|
+
AGL.glyph_name_for_unicode(codepoint)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Initialize a new AFMGenerator
|
|
71
|
+
#
|
|
72
|
+
# @param font [Fontisan::TrueTypeFont, Fontisan::OpenTypeFont] The font to generate AFM from
|
|
73
|
+
# @param options [Hash] Generation options
|
|
74
|
+
def initialize(font, options = {})
|
|
75
|
+
@font = font
|
|
76
|
+
@metrics = MetricsCalculator.new(font)
|
|
77
|
+
|
|
78
|
+
# Set up scaler
|
|
79
|
+
upm_scale = options[:upm_scale] || 1000
|
|
80
|
+
@scaler = if upm_scale == :native
|
|
81
|
+
UPMScaler.native(font)
|
|
82
|
+
else
|
|
83
|
+
UPMScaler.new(font, target_upm: upm_scale)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Set up encoding
|
|
87
|
+
@encoding = options[:encoding] || Encodings::AdobeStandard
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Generate AFM content
|
|
91
|
+
#
|
|
92
|
+
# @return [String] AFM file content
|
|
93
|
+
def generate_afm
|
|
94
|
+
afm_lines = []
|
|
95
|
+
|
|
96
|
+
# Header
|
|
97
|
+
afm_lines << "StartFontMetrics 4.1"
|
|
98
|
+
|
|
99
|
+
# Font metadata
|
|
100
|
+
add_font_metadata(afm_lines)
|
|
101
|
+
|
|
102
|
+
# Font bounding box
|
|
103
|
+
add_font_bounding_box(afm_lines)
|
|
104
|
+
|
|
105
|
+
# Character metrics
|
|
106
|
+
add_character_metrics(afm_lines)
|
|
107
|
+
|
|
108
|
+
# Kerning data
|
|
109
|
+
add_kerning_data(afm_lines)
|
|
110
|
+
|
|
111
|
+
# Footer
|
|
112
|
+
afm_lines << "EndFontMetrics"
|
|
113
|
+
|
|
114
|
+
afm_lines.join("\n")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Add font metadata to AFM
|
|
120
|
+
#
|
|
121
|
+
# @param afm_lines [Array<String>] AFM lines array
|
|
122
|
+
def add_font_metadata(afm_lines)
|
|
123
|
+
# Font name
|
|
124
|
+
font_name = @font.post_script_name
|
|
125
|
+
afm_lines << "FontName #{font_name}" if font_name
|
|
126
|
+
|
|
127
|
+
# Full name
|
|
128
|
+
full_name = @font.full_name
|
|
129
|
+
afm_lines << "FullName #{full_name}" if full_name
|
|
130
|
+
|
|
131
|
+
# Family name
|
|
132
|
+
family_name = @font.family_name
|
|
133
|
+
afm_lines << "FamilyName #{family_name}" if family_name
|
|
134
|
+
|
|
135
|
+
# Weight
|
|
136
|
+
weight = extract_weight
|
|
137
|
+
afm_lines << "Weight #{weight}" if weight
|
|
138
|
+
|
|
139
|
+
# Italic angle
|
|
140
|
+
italic_angle = extract_italic_angle
|
|
141
|
+
afm_lines << "ItalicAngle #{italic_angle}" if italic_angle
|
|
142
|
+
|
|
143
|
+
# IsFixedPitch
|
|
144
|
+
is_fixed_pitch = is_monospace? ? "true" : "false"
|
|
145
|
+
afm_lines << "IsFixedPitch #{is_fixed_pitch}"
|
|
146
|
+
|
|
147
|
+
# Character direction
|
|
148
|
+
afm_lines << "CharacterDirection 0"
|
|
149
|
+
|
|
150
|
+
# Version
|
|
151
|
+
version = extract_version
|
|
152
|
+
afm_lines << "Version #{version}" if version
|
|
153
|
+
|
|
154
|
+
# Notice (copyright)
|
|
155
|
+
notice = extract_copyright
|
|
156
|
+
afm_lines << "Notice #{notice}" if notice
|
|
157
|
+
|
|
158
|
+
# Encoding scheme
|
|
159
|
+
afm_lines << "EncodingScheme AdobeStandardEncoding"
|
|
160
|
+
|
|
161
|
+
# Mapping scheme
|
|
162
|
+
afm_lines << "MappingScheme 0"
|
|
163
|
+
|
|
164
|
+
# Ascender
|
|
165
|
+
ascender = @metrics.ascent
|
|
166
|
+
afm_lines << "Ascender #{ascender}" if ascender
|
|
167
|
+
|
|
168
|
+
# Descender
|
|
169
|
+
descender = @metrics.descent
|
|
170
|
+
afm_lines << "Descender #{descender}" if descender
|
|
171
|
+
|
|
172
|
+
# Underline properties
|
|
173
|
+
post = @font.table(Constants::POST_TAG)
|
|
174
|
+
underline_position = post&.underline_position if post.respond_to?(:underline_position)
|
|
175
|
+
afm_lines << "UnderlinePosition #{underline_position}" if underline_position
|
|
176
|
+
|
|
177
|
+
underline_thickness = post&.underline_thickness if post.respond_to?(:underline_thickness)
|
|
178
|
+
afm_lines << "UnderlineThickness #{underline_thickness}" if underline_thickness
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Add font bounding box to AFM
|
|
182
|
+
#
|
|
183
|
+
# @param afm_lines [Array<String>] AFM lines array
|
|
184
|
+
def add_font_bounding_box(afm_lines)
|
|
185
|
+
bbox = extract_font_bounding_box
|
|
186
|
+
return unless bbox && bbox.length == 4
|
|
187
|
+
|
|
188
|
+
# Scale bounding box
|
|
189
|
+
scaled_bbox = @scaler.scale_bbox(bbox)
|
|
190
|
+
afm_lines << "FontBBox #{scaled_bbox[0]} #{scaled_bbox[1]} #{scaled_bbox[2]} #{scaled_bbox[3]}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Add character metrics to AFM
|
|
194
|
+
#
|
|
195
|
+
# @param afm_lines [Array<String>] AFM lines array
|
|
196
|
+
def add_character_metrics(afm_lines)
|
|
197
|
+
# Get character mappings from cmap
|
|
198
|
+
char_mappings = extract_character_mappings
|
|
199
|
+
return if char_mappings.empty?
|
|
200
|
+
|
|
201
|
+
afm_lines << "StartCharMetrics #{char_mappings.length}"
|
|
202
|
+
|
|
203
|
+
char_mappings.each do |unicode, glyph_id|
|
|
204
|
+
next unless unicode && unicode >= 32 && unicode <= 255
|
|
205
|
+
|
|
206
|
+
# Get glyph name from encoding
|
|
207
|
+
glyph_name = @encoding.glyph_name_for_code(unicode)
|
|
208
|
+
glyph_name ||= AGL.glyph_name_for_unicode(unicode)
|
|
209
|
+
next unless glyph_name
|
|
210
|
+
|
|
211
|
+
# Get and scale width
|
|
212
|
+
width = @metrics.glyph_width(glyph_id)
|
|
213
|
+
next unless width
|
|
214
|
+
|
|
215
|
+
scaled_width = @scaler.scale_width(width)
|
|
216
|
+
|
|
217
|
+
# Get and scale bounding box if available
|
|
218
|
+
bbox = extract_glyph_bounding_box(glyph_id)
|
|
219
|
+
scaled_bbox = bbox ? @scaler.scale_bbox(bbox) : nil
|
|
220
|
+
|
|
221
|
+
# Format: C code ; WX width ; N name ; B llx lly urx ury ;
|
|
222
|
+
metric_line = "C #{unicode} ; WX #{scaled_width} ; N #{glyph_name}"
|
|
223
|
+
if scaled_bbox && scaled_bbox.length == 4
|
|
224
|
+
metric_line += " ; B #{scaled_bbox[0]} #{scaled_bbox[1]} #{scaled_bbox[2]} #{scaled_bbox[3]}"
|
|
225
|
+
end
|
|
226
|
+
afm_lines << metric_line
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
afm_lines << "EndCharMetrics"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Add kerning data to AFM
|
|
233
|
+
#
|
|
234
|
+
# @param afm_lines [Array<String>] AFM lines array
|
|
235
|
+
def add_kerning_data(afm_lines)
|
|
236
|
+
kerning_pairs = extract_kerning_pairs
|
|
237
|
+
return if kerning_pairs.empty?
|
|
238
|
+
|
|
239
|
+
afm_lines << "StartKernData"
|
|
240
|
+
afm_lines << "StartKernPairs #{kerning_pairs.length}"
|
|
241
|
+
|
|
242
|
+
kerning_pairs.each do |left, right, adjustment|
|
|
243
|
+
left_name = self.class.adobe_glyph_name(left)
|
|
244
|
+
right_name = self.class.adobe_glyph_name(right)
|
|
245
|
+
afm_lines << "KPX #{left_name} #{right_name} #{adjustment}"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
afm_lines << "EndKernPairs"
|
|
249
|
+
afm_lines << "EndKernData"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Extract weight from OS/2 table
|
|
253
|
+
#
|
|
254
|
+
# @return [String] Weight
|
|
255
|
+
def extract_weight
|
|
256
|
+
os2 = @font.table(Constants::OS2_TAG)
|
|
257
|
+
return "Regular" unless os2
|
|
258
|
+
|
|
259
|
+
weight_class = if os2.respond_to?(:us_weight_class)
|
|
260
|
+
os2.us_weight_class
|
|
261
|
+
elsif os2.respond_to?(:weight_class)
|
|
262
|
+
os2.weight_class
|
|
263
|
+
end
|
|
264
|
+
return "Regular" unless weight_class
|
|
265
|
+
|
|
266
|
+
case weight_class
|
|
267
|
+
when 100..200 then "Thin"
|
|
268
|
+
when 200..300 then "ExtraLight"
|
|
269
|
+
when 300..400 then "Light"
|
|
270
|
+
when 400..500 then "Regular"
|
|
271
|
+
when 500..600 then "Medium"
|
|
272
|
+
when 600..700 then "SemiBold"
|
|
273
|
+
when 700..800 then "Bold"
|
|
274
|
+
when 800..900 then "ExtraBold"
|
|
275
|
+
when 900..1000 then "Black"
|
|
276
|
+
else "Regular"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Extract italic angle from post table
|
|
281
|
+
#
|
|
282
|
+
# @return [Float] Italic angle
|
|
283
|
+
def extract_italic_angle
|
|
284
|
+
post = @font.table(Constants::POST_TAG)
|
|
285
|
+
return 0.0 unless post
|
|
286
|
+
|
|
287
|
+
if post.respond_to?(:italic_angle)
|
|
288
|
+
post.italic_angle
|
|
289
|
+
else
|
|
290
|
+
0.0
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Check if font is monospace
|
|
295
|
+
#
|
|
296
|
+
# @return [Boolean] True if monospace
|
|
297
|
+
def is_monospace?
|
|
298
|
+
post = @font.table(Constants::POST_TAG)
|
|
299
|
+
return false unless post
|
|
300
|
+
|
|
301
|
+
if post.respond_to?(:is_fixed_pitch)
|
|
302
|
+
post.is_fixed_pitch
|
|
303
|
+
else
|
|
304
|
+
false
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Extract version from name table
|
|
309
|
+
#
|
|
310
|
+
# @return [String, nil] Version string
|
|
311
|
+
def extract_version
|
|
312
|
+
name_table = @font.table(Constants::NAME_TAG)
|
|
313
|
+
return nil unless name_table
|
|
314
|
+
|
|
315
|
+
if name_table.respond_to?(:version_string)
|
|
316
|
+
name_table.version_string(1) || name_table.version_string(3)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Extract copyright from name table
|
|
321
|
+
#
|
|
322
|
+
# @return [String, nil] Copyright notice
|
|
323
|
+
def extract_copyright
|
|
324
|
+
name_table = @font.table(Constants::NAME_TAG)
|
|
325
|
+
return nil unless name_table
|
|
326
|
+
|
|
327
|
+
if name_table.respond_to?(:copyright)
|
|
328
|
+
name_table.copyright(1) || name_table.copyright(3)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Extract character mappings from cmap table
|
|
333
|
+
#
|
|
334
|
+
# @return [Hash<Integer, Integer>] Unicode to glyph ID mappings
|
|
335
|
+
def extract_character_mappings
|
|
336
|
+
cmap = @font.table(Constants::CMAP_TAG)
|
|
337
|
+
return {} unless cmap
|
|
338
|
+
|
|
339
|
+
@extract_character_mappings ||= begin
|
|
340
|
+
mappings = {}
|
|
341
|
+
|
|
342
|
+
# Try to get Unicode mappings (most reliable method)
|
|
343
|
+
if cmap.respond_to?(:unicode_mappings)
|
|
344
|
+
mappings = cmap.unicode_mappings || {}
|
|
345
|
+
elsif cmap.respond_to?(:unicode_bmp_mapping)
|
|
346
|
+
mappings = cmap.unicode_bmp_mapping || {}
|
|
347
|
+
elsif cmap.respond_to?(:subtables)
|
|
348
|
+
# Look for Unicode BMP subtable
|
|
349
|
+
unicode_subtable = cmap.subtables.find do |subtable|
|
|
350
|
+
subtable.respond_to?(:platform_id) &&
|
|
351
|
+
subtable.platform_id == 3 &&
|
|
352
|
+
subtable.respond_to?(:encoding_id) &&
|
|
353
|
+
subtable.encoding_id == 1
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
if unicode_subtable.respond_to?(:glyph_index_map)
|
|
357
|
+
mappings = unicode_subtable.glyph_index_map
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
mappings
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Extract font bounding box
|
|
366
|
+
#
|
|
367
|
+
# @return [Array<Integer>, nil] Bounding box [llx, lly, urx, ury]
|
|
368
|
+
def extract_font_bounding_box
|
|
369
|
+
head = @font.table(Constants::HEAD_TAG)
|
|
370
|
+
return nil unless head
|
|
371
|
+
|
|
372
|
+
if head.respond_to?(:font_bounding_box)
|
|
373
|
+
head.font_bounding_box
|
|
374
|
+
elsif head.respond_to?(:x_min) && head.respond_to?(:y_min) &&
|
|
375
|
+
head.respond_to?(:x_max) && head.respond_to?(:y_max)
|
|
376
|
+
[head.x_min, head.y_min, head.x_max, head.y_max]
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Extract glyph bounding box
|
|
381
|
+
#
|
|
382
|
+
# @param glyph_id [Integer] Glyph ID
|
|
383
|
+
# @return [Array<Integer>, nil] Bounding box [llx, lly, urx, ury]
|
|
384
|
+
def extract_glyph_bounding_box(glyph_id)
|
|
385
|
+
return nil unless @font.truetype?
|
|
386
|
+
|
|
387
|
+
glyf_table = @font.table(Constants::GLYF_TAG)
|
|
388
|
+
return nil unless glyf_table
|
|
389
|
+
|
|
390
|
+
loca_table = @font.table(Constants::LOCA_TAG)
|
|
391
|
+
return nil unless loca_table
|
|
392
|
+
|
|
393
|
+
head_table = @font.table(Constants::HEAD_TAG)
|
|
394
|
+
return nil unless head_table
|
|
395
|
+
|
|
396
|
+
# Ensure loca is parsed with context
|
|
397
|
+
if loca_table.respond_to?(:parse_with_context) && !loca_table.parsed?
|
|
398
|
+
maxp = @font.table(Constants::MAXP_TAG)
|
|
399
|
+
if maxp
|
|
400
|
+
loca_table.parse_with_context(head_table.index_to_loc_format,
|
|
401
|
+
maxp.num_glyphs)
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
if glyf_table.respond_to?(:glyph_for)
|
|
406
|
+
glyph = glyf_table.glyph_for(glyph_id, loca_table, head_table)
|
|
407
|
+
return nil unless glyph
|
|
408
|
+
|
|
409
|
+
if glyph.respond_to?(:bounding_box)
|
|
410
|
+
glyph.bounding_box
|
|
411
|
+
elsif glyph.respond_to?(:x_min) && glyph.respond_to?(:y_min) &&
|
|
412
|
+
glyph.respond_to?(:x_max) && glyph.respond_to?(:y_max)
|
|
413
|
+
[glyph.x_min, glyph.y_min, glyph.x_max, glyph.y_max]
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Extract kerning pairs from GPOS table
|
|
419
|
+
#
|
|
420
|
+
# @return [Array<Array>] Array of [left_unicode, right_unicode, adjustment]
|
|
421
|
+
def extract_kerning_pairs
|
|
422
|
+
gpos = @font.table(Constants::GPOS_TAG)
|
|
423
|
+
return [] unless gpos
|
|
424
|
+
|
|
425
|
+
@extract_kerning_pairs ||= begin
|
|
426
|
+
pairs = []
|
|
427
|
+
|
|
428
|
+
# This is a simplified implementation
|
|
429
|
+
# Full implementation would parse GPOS lookup type 2 (Pair positioning)
|
|
430
|
+
# For now, return empty array
|
|
431
|
+
pairs
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|