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,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Type1
|
|
5
|
+
# AFM (Adobe Font Metrics) file parser
|
|
6
|
+
#
|
|
7
|
+
# [`AFMParser`](lib/fontisan/type1/afm_parser.rb) parses Adobe Font Metrics
|
|
8
|
+
# files which contain font metric information for Type 1 fonts.
|
|
9
|
+
#
|
|
10
|
+
# AFM files include:
|
|
11
|
+
# - Character widths
|
|
12
|
+
# - Kerning pairs
|
|
13
|
+
# - Character bounding boxes
|
|
14
|
+
# - Font metadata (name, version, copyright, etc.)
|
|
15
|
+
#
|
|
16
|
+
# @example Parse an AFM file
|
|
17
|
+
# afm = Fontisan::Type1::AFMParser.parse_file("font.afm")
|
|
18
|
+
# puts afm.font_name
|
|
19
|
+
# puts afm.character_widths['A']
|
|
20
|
+
# puts afm.kerning_pairs[['A', 'V']]
|
|
21
|
+
#
|
|
22
|
+
# @see https://www.adobe.com/devnet/font/pdfs/5004.AFM_Spec.pdf
|
|
23
|
+
class AFMParser
|
|
24
|
+
# @return [String] Font name
|
|
25
|
+
attr_reader :font_name
|
|
26
|
+
|
|
27
|
+
# @return [String] Full name
|
|
28
|
+
attr_reader :full_name
|
|
29
|
+
|
|
30
|
+
# @return [String] Family name
|
|
31
|
+
attr_reader :family_name
|
|
32
|
+
|
|
33
|
+
# @return [String] Weight
|
|
34
|
+
attr_reader :weight
|
|
35
|
+
|
|
36
|
+
# @return [String] Version
|
|
37
|
+
attr_reader :version
|
|
38
|
+
|
|
39
|
+
# @return [String] Copyright notice
|
|
40
|
+
attr_reader :copyright
|
|
41
|
+
|
|
42
|
+
# @return [Hash<String, Integer>] Character widths (glyph name => width)
|
|
43
|
+
attr_reader :character_widths
|
|
44
|
+
|
|
45
|
+
# @return [Hash<Array(String>, Integer>] Kerning pairs ([left, right] => adjustment)
|
|
46
|
+
attr_reader :kerning_pairs
|
|
47
|
+
|
|
48
|
+
# @return [Hash] Character bounding boxes (glyph name => {llx, lly, urx, ury})
|
|
49
|
+
attr_reader :character_bboxes
|
|
50
|
+
|
|
51
|
+
# @return [Integer] Font bounding box [llx, lly, urx, ury]
|
|
52
|
+
attr_reader :font_bbox
|
|
53
|
+
|
|
54
|
+
# @return [Hash] Raw AFM data
|
|
55
|
+
attr_reader :raw_data
|
|
56
|
+
|
|
57
|
+
# Parse AFM file
|
|
58
|
+
#
|
|
59
|
+
# @param path [String] Path to AFM file
|
|
60
|
+
# @return [AFMParser] Parsed AFM data
|
|
61
|
+
# @raise [ArgumentError] If path is nil
|
|
62
|
+
# @raise [Fontisan::Error] If file cannot be read or parsed
|
|
63
|
+
def self.parse_file(path)
|
|
64
|
+
raise ArgumentError, "Path cannot be nil" if path.nil?
|
|
65
|
+
|
|
66
|
+
unless File.exist?(path)
|
|
67
|
+
raise Fontisan::Error, "AFM file not found: #{path}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
content = File.read(path, encoding: "ISO-8859-1")
|
|
71
|
+
parse(content)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Parse AFM content
|
|
75
|
+
#
|
|
76
|
+
# @param content [String] AFM file content
|
|
77
|
+
# @return [AFMParser] Parsed AFM data
|
|
78
|
+
def self.parse(content)
|
|
79
|
+
new.parse(content)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Alias for parse method
|
|
83
|
+
def self.parse_string(content)
|
|
84
|
+
parse(content)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Initialize a new AFMParser
|
|
88
|
+
def initialize
|
|
89
|
+
@character_widths = {}
|
|
90
|
+
@kerning_pairs = {}
|
|
91
|
+
@character_bboxes = {}
|
|
92
|
+
@raw_data = {}
|
|
93
|
+
@font_bbox = nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Parse AFM content
|
|
97
|
+
#
|
|
98
|
+
# @param content [String] AFM file content
|
|
99
|
+
# @return [AFMParser] Self for method chaining
|
|
100
|
+
def parse(content)
|
|
101
|
+
parse_global_metrics(content)
|
|
102
|
+
parse_character_metrics(content)
|
|
103
|
+
parse_kerning_data(content)
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get character width for glyph
|
|
108
|
+
#
|
|
109
|
+
# @param glyph_name [String] Glyph name
|
|
110
|
+
# @return [Integer, nil] Character width or nil if not found
|
|
111
|
+
def width(glyph_name)
|
|
112
|
+
@character_widths[glyph_name]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get kerning adjustment for character pair
|
|
116
|
+
#
|
|
117
|
+
# @param left [String] Left glyph name
|
|
118
|
+
# @param right [String] Right glyph name
|
|
119
|
+
# @return [Integer, nil] Kerning adjustment or nil if not found
|
|
120
|
+
def kerning(left, right)
|
|
121
|
+
@kerning_pairs[[left, right]]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if character exists
|
|
125
|
+
#
|
|
126
|
+
# @param glyph_name [String] Glyph name
|
|
127
|
+
# @return [Boolean] True if character exists
|
|
128
|
+
def has_character?(glyph_name)
|
|
129
|
+
@character_widths.key?(glyph_name)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Parse global font metrics
|
|
135
|
+
#
|
|
136
|
+
# @param content [String] AFM content
|
|
137
|
+
def parse_global_metrics(content)
|
|
138
|
+
# Split into lines for safer processing (limits input size for regex)
|
|
139
|
+
lines = content.lines
|
|
140
|
+
|
|
141
|
+
# Parse all metrics in a single loop to satisfy RuboCop
|
|
142
|
+
lines.each do |line|
|
|
143
|
+
if @font_name.nil? && (match = line.match(/^FontName\s+(\S+)/i))
|
|
144
|
+
@font_name = match[1]
|
|
145
|
+
elsif @full_name.nil? && (match = line.match(/^FullName\s+(\S.*)/i))
|
|
146
|
+
@full_name = match[1].strip
|
|
147
|
+
elsif @family_name.nil? && (match = line.match(/^FamilyName\s+(\S.*)/i))
|
|
148
|
+
@family_name = match[1].strip
|
|
149
|
+
elsif @weight.nil? && (match = line.match(/^Weight\s+(\S.*)/i))
|
|
150
|
+
@weight = match[1].strip
|
|
151
|
+
elsif @version.nil? && (match = line.match(/^Version\s+(\S.*)/i))
|
|
152
|
+
@version = match[1].strip
|
|
153
|
+
elsif @copyright.nil? && (match = line.match(/^Notice\s+(\S.*)/i))
|
|
154
|
+
@copyright = match[1].strip
|
|
155
|
+
elsif @font_bbox.nil? && (match = line.match(/^FontBBox\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$/i))
|
|
156
|
+
@font_bbox = [match[1].to_i, match[2].to_i, match[3].to_i, match[4].to_i]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Break early if all metrics found
|
|
160
|
+
break if @font_name && @full_name && @family_name &&
|
|
161
|
+
@weight && @version && @copyright && @font_bbox
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Parse character metrics
|
|
166
|
+
#
|
|
167
|
+
# @param content [String] AFM content
|
|
168
|
+
def parse_character_metrics(content)
|
|
169
|
+
# Find StartCharMetrics section - use safer pattern
|
|
170
|
+
char_metrics_section = content.match(/^StartCharMetrics\s+(\d+)/i)
|
|
171
|
+
return unless char_metrics_section
|
|
172
|
+
|
|
173
|
+
# Parse character metrics until EndCharMetrics
|
|
174
|
+
in_char_metrics = false
|
|
175
|
+
content.each_line do |line|
|
|
176
|
+
if /^StartCharMetrics/i.match?(line)
|
|
177
|
+
in_char_metrics = true
|
|
178
|
+
next
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
break if /^EndCharMetrics/i.match?(line)
|
|
182
|
+
next unless in_char_metrics
|
|
183
|
+
|
|
184
|
+
# Parse character metric line
|
|
185
|
+
# Format: C name ; WX width ; B llx lly urx ury ...
|
|
186
|
+
parse_char_metric_line(line)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Parse a single character metric line
|
|
191
|
+
#
|
|
192
|
+
# @param line [String] Character metric line
|
|
193
|
+
def parse_char_metric_line(line)
|
|
194
|
+
return if line.strip.empty?
|
|
195
|
+
|
|
196
|
+
glyph_name = nil
|
|
197
|
+
char_width = nil
|
|
198
|
+
char_bbox = nil
|
|
199
|
+
|
|
200
|
+
# Parse character metric line
|
|
201
|
+
# Format: C code ; WX width ; N name ; B llx lly urx ury ...
|
|
202
|
+
# Or: C code ; WX width ; B llx lly urx ury ... (no N field)
|
|
203
|
+
|
|
204
|
+
# Extract glyph name from N field if present
|
|
205
|
+
# Format: N name ; or N;name;
|
|
206
|
+
if (name_match = line.match(/;\s+N\s+([^\s;]+)/))
|
|
207
|
+
glyph_name = name_match[1]
|
|
208
|
+
# Remove quotes if present
|
|
209
|
+
glyph_name = glyph_name.gsub(/['"]/, "")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Extract width (WX)
|
|
213
|
+
if (width_match = line.match(/WX\s+(\d+)/))
|
|
214
|
+
char_width = width_match[1].to_i
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Extract bounding box (B)
|
|
218
|
+
if (bbox_match = line.match(/B\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)/))
|
|
219
|
+
char_bbox = {
|
|
220
|
+
llx: bbox_match[1].to_i,
|
|
221
|
+
lly: bbox_match[2].to_i,
|
|
222
|
+
urx: bbox_match[3].to_i,
|
|
223
|
+
ury: bbox_match[4].to_i,
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Store if we have a name and width
|
|
228
|
+
if glyph_name && char_width
|
|
229
|
+
@character_widths[glyph_name] = char_width
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Store bounding box if we have a name and bbox
|
|
233
|
+
if glyph_name && char_bbox
|
|
234
|
+
@character_bboxes[glyph_name] = char_bbox
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Parse kerning data
|
|
239
|
+
#
|
|
240
|
+
# @param content [String] AFM content
|
|
241
|
+
def parse_kerning_data(content)
|
|
242
|
+
# Find StartKernData section
|
|
243
|
+
in_kern_data = false
|
|
244
|
+
in_kern_pairs = false
|
|
245
|
+
|
|
246
|
+
content.each_line do |line|
|
|
247
|
+
if /^StartKernData/i.match?(line)
|
|
248
|
+
in_kern_data = true
|
|
249
|
+
next
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
if /^EndKernData/i.match?(line)
|
|
253
|
+
in_kern_data = false
|
|
254
|
+
next
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if /^StartKernPairs\s+/i.match?(line)
|
|
258
|
+
in_kern_pairs = true
|
|
259
|
+
next
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
if /^EndKernPairs/i.match?(line)
|
|
263
|
+
in_kern_pairs = false
|
|
264
|
+
next
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
next unless in_kern_data && in_kern_pairs
|
|
268
|
+
|
|
269
|
+
# Parse kerning pair line
|
|
270
|
+
# Format: KPX left right adjustment
|
|
271
|
+
parse_kern_pair_line(line)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Parse a kerning pair line
|
|
276
|
+
#
|
|
277
|
+
# @param line [String] Kerning pair line
|
|
278
|
+
def parse_kern_pair_line(line)
|
|
279
|
+
return if line.strip.empty?
|
|
280
|
+
|
|
281
|
+
# KPX format: KPX left right adjustment
|
|
282
|
+
# Use specific character class for glyph names to prevent ReDoS
|
|
283
|
+
# AFM glyph names contain letters, digits, period, underscore
|
|
284
|
+
if (match = line.match(/^KPX\s+([A-Za-z0-9._]{1,64})\s+([A-Za-z0-9._]{1,64})\s+(-?\d+)/i))
|
|
285
|
+
left = match[1]
|
|
286
|
+
right = match[2]
|
|
287
|
+
adjustment = match[3].to_i
|
|
288
|
+
|
|
289
|
+
# Remove quotes if present (some AFM files quote glyph names)
|
|
290
|
+
left = left.gsub(/['"]/, "")
|
|
291
|
+
right = right.gsub(/['"]/, "")
|
|
292
|
+
|
|
293
|
+
@kerning_pairs[[left, right]] = adjustment
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|