fontisan 0.2.11 → 0.2.13
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 +294 -52
- data/Gemfile +5 -0
- data/README.adoc +163 -2
- data/docs/CONVERSION_GUIDE.adoc +633 -0
- data/docs/TYPE1_FONTS.adoc +445 -0
- data/lib/fontisan/cli.rb +177 -6
- data/lib/fontisan/commands/convert_command.rb +32 -1
- data/lib/fontisan/commands/info_command.rb +83 -2
- 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 +17 -5
- data/lib/fontisan/converters/outline_converter.rb +78 -4
- data/lib/fontisan/converters/type1_converter.rb +1234 -0
- data/lib/fontisan/font_loader.rb +46 -3
- data/lib/fontisan/hints/hint_converter.rb +4 -1
- 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/cff_to_type1_converter.rb +302 -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 +576 -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 +369 -0
- data/lib/fontisan/type1/pfa_parser.rb +159 -0
- data/lib/fontisan/type1/pfb_generator.rb +314 -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 +342 -0
- data/lib/fontisan/type1/seac_expander.rb +501 -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 +75 -0
- data/lib/fontisan/type1_font.rb +318 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -0
- metadata +30 -3
- data/docs/DOCUMENTATION_SUMMARY.md +0 -141
data/lib/fontisan/font_loader.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "true_type_collection"
|
|
|
8
8
|
require_relative "open_type_collection"
|
|
9
9
|
require_relative "woff_font"
|
|
10
10
|
require_relative "woff2_font"
|
|
11
|
+
require_relative "type1_font"
|
|
11
12
|
require_relative "error"
|
|
12
13
|
|
|
13
14
|
module Fontisan
|
|
@@ -15,11 +16,13 @@ module Fontisan
|
|
|
15
16
|
#
|
|
16
17
|
# This class is the primary entry point for loading fonts in Fontisan.
|
|
17
18
|
# It automatically detects the font format and returns the appropriate
|
|
18
|
-
# domain object (TrueTypeFont, OpenTypeFont, TrueTypeCollection, or OpenTypeCollection).
|
|
19
|
+
# domain object (TrueTypeFont, OpenTypeFont, Type1Font, TrueTypeCollection, or OpenTypeCollection).
|
|
19
20
|
#
|
|
20
21
|
# @example Load any font type
|
|
21
22
|
# font = FontLoader.load("font.ttf") # => TrueTypeFont
|
|
22
23
|
# font = FontLoader.load("font.otf") # => OpenTypeFont
|
|
24
|
+
# font = FontLoader.load("font.pfb") # => Type1Font
|
|
25
|
+
# font = FontLoader.load("font.pfa") # => Type1Font
|
|
23
26
|
# font = FontLoader.load("fonts.ttc") # => TrueTypeFont (first in collection)
|
|
24
27
|
# font = FontLoader.load("fonts.ttc", font_index: 2) # => TrueTypeFont (third in collection)
|
|
25
28
|
#
|
|
@@ -37,7 +40,7 @@ module Fontisan
|
|
|
37
40
|
# @param font_index [Integer] Index of font in collection (0-based, default: 0)
|
|
38
41
|
# @param mode [Symbol] Loading mode (:metadata or :full, default: from ENV or :full)
|
|
39
42
|
# @param lazy [Boolean] If true, load tables on demand (default: false for eager loading)
|
|
40
|
-
# @return [TrueTypeFont, OpenTypeFont, WoffFont, Woff2Font] The loaded font object
|
|
43
|
+
# @return [TrueTypeFont, OpenTypeFont, Type1Font, WoffFont, Woff2Font] The loaded font object
|
|
41
44
|
# @raise [Errno::ENOENT] if file does not exist
|
|
42
45
|
# @raise [UnsupportedFormatError] for unsupported formats
|
|
43
46
|
# @raise [InvalidFontError] for corrupted or unknown formats
|
|
@@ -55,6 +58,11 @@ module Fontisan
|
|
|
55
58
|
# Validate mode
|
|
56
59
|
LoadingModes.validate_mode!(resolved_mode)
|
|
57
60
|
|
|
61
|
+
# Check for Type 1 format first (PFB/PFA have different signatures)
|
|
62
|
+
if type1_font?(path)
|
|
63
|
+
return Type1Font.from_file(path, mode: resolved_mode)
|
|
64
|
+
end
|
|
65
|
+
|
|
58
66
|
File.open(path, "rb") do |io|
|
|
59
67
|
signature = io.read(4)
|
|
60
68
|
io.rewind
|
|
@@ -76,7 +84,7 @@ module Fontisan
|
|
|
76
84
|
resolved_lazy)
|
|
77
85
|
else
|
|
78
86
|
raise InvalidFontError,
|
|
79
|
-
"Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, or
|
|
87
|
+
"Unknown font format. Expected TTF, OTF, TTC, OTC, WOFF, WOFF2, PFB, or PFA file."
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
end
|
|
@@ -387,5 +395,40 @@ mode: LoadingModes::FULL, lazy: true)
|
|
|
387
395
|
end
|
|
388
396
|
|
|
389
397
|
private_class_method :dfont_signature?
|
|
398
|
+
|
|
399
|
+
# Check if file is a Type 1 font (PFB or PFA)
|
|
400
|
+
#
|
|
401
|
+
# Type 1 fonts come in two formats:
|
|
402
|
+
# - PFB (Printer Font Binary): Binary format with chunk markers
|
|
403
|
+
# - PFA (Printer Font ASCII): ASCII text format with hex encoding
|
|
404
|
+
#
|
|
405
|
+
# @param path [String] Path to the font file
|
|
406
|
+
# @return [Boolean] true if Type 1 font
|
|
407
|
+
# @api private
|
|
408
|
+
def self.type1_font?(path)
|
|
409
|
+
# Check file extension first (quick check)
|
|
410
|
+
ext = File.extname(path).downcase
|
|
411
|
+
return true if [".pfb", ".pfa", ".ps"].include?(ext)
|
|
412
|
+
|
|
413
|
+
# Check PFB signature (first byte should be 0x80 or 0x81)
|
|
414
|
+
File.open(path, "rb") do |io|
|
|
415
|
+
first_byte = io.getbyte
|
|
416
|
+
return true if [Constants::PFB_ASCII_CHUNK, Constants::PFB_BINARY_CHUNK].include?(first_byte)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Check PFA signature (text file with Adobe header)
|
|
420
|
+
File.open(path, "rb") do |io|
|
|
421
|
+
# Read first 100 bytes to check for PFA signature
|
|
422
|
+
header = io.read(100)
|
|
423
|
+
return true if header.include?(Constants::PFA_SIGNATURE_ADOBE_1_0) ||
|
|
424
|
+
header.include?(Constants::PFA_SIGNATURE_ADOBE_3_0)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
false
|
|
428
|
+
rescue IOError, Errno::ENOENT
|
|
429
|
+
false
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
private_class_method :type1_font?
|
|
390
433
|
end
|
|
391
434
|
end
|
|
@@ -274,7 +274,10 @@ module Fontisan
|
|
|
274
274
|
end
|
|
275
275
|
|
|
276
276
|
# Merge all extracted hints (prep_hints and fpgm_hints override stem widths if present)
|
|
277
|
-
|
|
277
|
+
# Note: fpgm_hints contains metadata (fpgm_size, has_functions, complexity)
|
|
278
|
+
# which we must filter out before merging into PostScript dict hints
|
|
279
|
+
fpgm_dict_hints = fpgm_hints.reject { |k, _| %i[fpgm_size has_functions complexity].include?(k) }
|
|
280
|
+
hints.merge!(prep_hints).merge!(fpgm_dict_hints).merge!(blue_zones)
|
|
278
281
|
|
|
279
282
|
# Provide default blue_values if none were detected
|
|
280
283
|
# These are standard values that work for most Latin fonts
|
|
@@ -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
|