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,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "type1"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
# Adobe Type 1 Font handler
|
|
7
|
+
#
|
|
8
|
+
# [`Type1Font`](lib/fontisan/type1_font.rb) provides parsing and conversion
|
|
9
|
+
# capabilities for Adobe Type 1 fonts in PFB (Printer Font Binary) and
|
|
10
|
+
# PFA (Printer Font ASCII) formats.
|
|
11
|
+
#
|
|
12
|
+
# Type 1 fonts were the standard for digital typography in the 1980s-1990s
|
|
13
|
+
# and consist of:
|
|
14
|
+
# - Font dictionary with metadata (FontInfo, FontName, Encoding, etc.)
|
|
15
|
+
# - Private dictionary with hinting and spacing information
|
|
16
|
+
# - CharStrings (glyph outline descriptions)
|
|
17
|
+
# - eexec encryption for protection
|
|
18
|
+
#
|
|
19
|
+
# @example Load a PFB file
|
|
20
|
+
# font = Fontisan::Type1Font.from_file('font.pfb')
|
|
21
|
+
# puts font.font_name
|
|
22
|
+
# puts font.version
|
|
23
|
+
#
|
|
24
|
+
# @example Load a PFA file
|
|
25
|
+
# font = Fontisan::Type1Font.from_file('font.pfa')
|
|
26
|
+
# puts font.full_name
|
|
27
|
+
#
|
|
28
|
+
# @example Access decrypted font data
|
|
29
|
+
# font = Fontisan::Type1Font.from_file('font.pfb')
|
|
30
|
+
# puts font.decrypted_data
|
|
31
|
+
#
|
|
32
|
+
# @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
|
|
33
|
+
class Type1Font
|
|
34
|
+
# @return [String, nil] File path if loaded from file
|
|
35
|
+
attr_reader :file_path
|
|
36
|
+
|
|
37
|
+
# @return [Symbol] Format type (:pfb or :pfa)
|
|
38
|
+
attr_reader :format
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] Decrypted font data
|
|
41
|
+
attr_reader :decrypted_data
|
|
42
|
+
|
|
43
|
+
# @return [FontDictionary, nil] Font dictionary
|
|
44
|
+
attr_reader :font_dictionary
|
|
45
|
+
|
|
46
|
+
# @return [PrivateDict, nil] Private dictionary
|
|
47
|
+
attr_reader :private_dict
|
|
48
|
+
|
|
49
|
+
# @return [CharStrings, nil] CharStrings dictionary
|
|
50
|
+
attr_reader :charstrings
|
|
51
|
+
|
|
52
|
+
# Initialize a new Type1Font instance
|
|
53
|
+
#
|
|
54
|
+
# @param data [String] Font file data (binary or text)
|
|
55
|
+
# @param format [Symbol] Format type (:pfb or :pfa, auto-detected if nil)
|
|
56
|
+
# @param file_path [String, nil] Optional file path for reference
|
|
57
|
+
def initialize(data, format: nil, file_path: nil)
|
|
58
|
+
@file_path = file_path
|
|
59
|
+
@format = format || detect_format(data)
|
|
60
|
+
@data = data
|
|
61
|
+
|
|
62
|
+
parse_font_data
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Load Type 1 font from file
|
|
66
|
+
#
|
|
67
|
+
# @param file_path [String] Path to PFB or PFA file
|
|
68
|
+
# @return [Type1Font] Loaded font instance
|
|
69
|
+
# @raise [ArgumentError] If file_path is nil
|
|
70
|
+
# @raise [Fontisan::Error] If file cannot be read or parsed
|
|
71
|
+
#
|
|
72
|
+
# @example Load PFB file
|
|
73
|
+
# font = Fontisan::Type1Font.from_file('font.pfb')
|
|
74
|
+
#
|
|
75
|
+
# @example Load PFA file
|
|
76
|
+
# font = Fontisan::Type1Font.from_file('font.pfa')
|
|
77
|
+
def self.from_file(file_path)
|
|
78
|
+
raise ArgumentError, "File path cannot be nil" if file_path.nil?
|
|
79
|
+
|
|
80
|
+
unless File.exist?(file_path)
|
|
81
|
+
raise Fontisan::Error, "File not found: #{file_path}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Read file
|
|
85
|
+
data = File.binread(file_path)
|
|
86
|
+
|
|
87
|
+
new(data, file_path: file_path)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get clear text portion (before eexec)
|
|
91
|
+
#
|
|
92
|
+
# @return [String] Clear text font dictionary
|
|
93
|
+
def clear_text
|
|
94
|
+
@clear_text ||= ""
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get encrypted portion (as hex string for PFA)
|
|
98
|
+
#
|
|
99
|
+
# @return [String] Encrypted portion
|
|
100
|
+
def encrypted_portion
|
|
101
|
+
@encrypted_portion ||= ""
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get font name from font dictionary
|
|
105
|
+
#
|
|
106
|
+
# @return [String, nil] Font name or nil if not found
|
|
107
|
+
def font_name
|
|
108
|
+
extract_dictionary_value("/FontName")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get full name from FontInfo
|
|
112
|
+
#
|
|
113
|
+
# @return [String, nil] Full name or nil if not found
|
|
114
|
+
def full_name
|
|
115
|
+
extract_fontinfo_value("FullName")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get family name from FontInfo
|
|
119
|
+
#
|
|
120
|
+
# @return [String, nil] Family name or nil if not found
|
|
121
|
+
def family_name
|
|
122
|
+
extract_fontinfo_value("FamilyName")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get version from FontInfo
|
|
126
|
+
#
|
|
127
|
+
# @return [String, nil] Version or nil if not found
|
|
128
|
+
def version
|
|
129
|
+
extract_fontinfo_value("version")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check if font has been decrypted
|
|
133
|
+
#
|
|
134
|
+
# @return [Boolean] True if font data has been decrypted
|
|
135
|
+
def decrypted?
|
|
136
|
+
!@decrypted_data.nil?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Check if font is encrypted
|
|
140
|
+
#
|
|
141
|
+
# @return [Boolean] True if font has eexec encrypted portion
|
|
142
|
+
def encrypted?
|
|
143
|
+
!@encrypted_portion.nil? && !@encrypted_portion.empty?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Decrypt the font if not already decrypted
|
|
147
|
+
#
|
|
148
|
+
# @return [String] Decrypted font data
|
|
149
|
+
def decrypt!
|
|
150
|
+
return @decrypted_data if decrypted?
|
|
151
|
+
|
|
152
|
+
if @encrypted_portion.nil? || @encrypted_portion.empty?
|
|
153
|
+
@decrypted_data = @clear_text
|
|
154
|
+
else
|
|
155
|
+
encrypted_binary = if @format == :pfa
|
|
156
|
+
# Convert hex string to binary
|
|
157
|
+
[@encrypted_portion.gsub(/\s/, "")].pack("H*")
|
|
158
|
+
else
|
|
159
|
+
@encrypted_portion
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
@decrypted_data = @clear_text +
|
|
163
|
+
Type1::Decryptor.eexec_decrypt(encrypted_binary)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
@decrypted_data
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Parse font dictionaries from decrypted data
|
|
170
|
+
#
|
|
171
|
+
# Parses the font dictionary, private dictionary, and CharStrings
|
|
172
|
+
# from the decrypted font data.
|
|
173
|
+
#
|
|
174
|
+
# @return [void]
|
|
175
|
+
def parse_dictionaries!
|
|
176
|
+
decrypt! unless decrypted?
|
|
177
|
+
|
|
178
|
+
# Parse font dictionary
|
|
179
|
+
@font_dictionary = Type1::FontDictionary.parse(@decrypted_data)
|
|
180
|
+
|
|
181
|
+
# Parse private dictionary
|
|
182
|
+
@private_dict = Type1::PrivateDict.parse(@decrypted_data)
|
|
183
|
+
|
|
184
|
+
# Parse CharStrings
|
|
185
|
+
@charstrings = Type1::CharStrings.parse(@decrypted_data, @private_dict)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Get font name from font dictionary
|
|
189
|
+
#
|
|
190
|
+
# @return [String, nil] Font name or nil if not found
|
|
191
|
+
def font_name
|
|
192
|
+
return @font_dictionary&.font_name if @font_dictionary
|
|
193
|
+
|
|
194
|
+
extract_dictionary_value("/FontName")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Get full name from FontInfo
|
|
198
|
+
#
|
|
199
|
+
# @return [String, nil] Full name or nil if not found
|
|
200
|
+
def full_name
|
|
201
|
+
return @font_dictionary&.font_info&.full_name if @font_dictionary
|
|
202
|
+
|
|
203
|
+
extract_fontinfo_value("FullName")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Get family name from FontInfo
|
|
207
|
+
#
|
|
208
|
+
# @return [String, nil] Family name or nil if not found
|
|
209
|
+
def family_name
|
|
210
|
+
return @font_dictionary&.font_info&.family_name if @font_dictionary
|
|
211
|
+
|
|
212
|
+
extract_fontinfo_value("FamilyName")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Get version from FontInfo
|
|
216
|
+
#
|
|
217
|
+
# @return [String, nil] Version or nil if not found
|
|
218
|
+
def version
|
|
219
|
+
return @font_dictionary&.font_info&.version if @font_dictionary
|
|
220
|
+
|
|
221
|
+
extract_fontinfo_value("version")
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Get list of glyph names
|
|
225
|
+
#
|
|
226
|
+
# @return [Array<String>] Glyph names
|
|
227
|
+
def glyph_names
|
|
228
|
+
return [] unless @charstrings
|
|
229
|
+
|
|
230
|
+
@charstrings.glyph_names
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Check if dictionaries have been parsed
|
|
234
|
+
#
|
|
235
|
+
# @return [Boolean] True if dictionaries have been parsed
|
|
236
|
+
def parsed_dictionaries?
|
|
237
|
+
!@font_dictionary.nil?
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
# Parse font data based on format
|
|
243
|
+
def parse_font_data
|
|
244
|
+
case @format
|
|
245
|
+
when :pfb
|
|
246
|
+
parse_pfb
|
|
247
|
+
when :pfa
|
|
248
|
+
parse_pfa
|
|
249
|
+
else
|
|
250
|
+
raise Fontisan::Error, "Unknown format: #{@format}"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Parse PFB format
|
|
255
|
+
def parse_pfb
|
|
256
|
+
parser = Type1::PFBParser.new
|
|
257
|
+
parser.parse(@data)
|
|
258
|
+
|
|
259
|
+
# PFB has alternating ASCII and binary parts
|
|
260
|
+
# ASCII parts contain font dictionary
|
|
261
|
+
# Binary parts contain encrypted CharStrings
|
|
262
|
+
@clear_text = parser.ascii_text
|
|
263
|
+
@encrypted_portion = parser.binary_data
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Parse PFA format
|
|
267
|
+
def parse_pfa
|
|
268
|
+
parser = Type1::PFAParser.new
|
|
269
|
+
parser.parse(@data)
|
|
270
|
+
|
|
271
|
+
@clear_text = parser.clear_text
|
|
272
|
+
@encrypted_portion = parser.encrypted_hex
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Detect format from data
|
|
276
|
+
#
|
|
277
|
+
# @param data [String] Font data
|
|
278
|
+
# @return [Symbol] Detected format (:pfb or :pfa)
|
|
279
|
+
def detect_format(data)
|
|
280
|
+
if Type1::PFBParser.pfb_file?(data)
|
|
281
|
+
:pfb
|
|
282
|
+
elsif Type1::PFAParser.pfa_file?(data)
|
|
283
|
+
:pfa
|
|
284
|
+
else
|
|
285
|
+
raise Fontisan::Error,
|
|
286
|
+
"Cannot detect Type 1 format: not a valid PFB or PFA file"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Extract value from font dictionary
|
|
291
|
+
#
|
|
292
|
+
# @param key [String] Dictionary key (e.g., "/FontName")
|
|
293
|
+
# @return [String, nil] Value or nil if not found
|
|
294
|
+
def extract_dictionary_value(key)
|
|
295
|
+
text = decrypted? ? @decrypted_data : @clear_text
|
|
296
|
+
|
|
297
|
+
# Look for /FontName /name def pattern
|
|
298
|
+
pattern = /#{Regexp.escape(key)}\s+\/([^\s]+)\s+def/
|
|
299
|
+
match = text.match(pattern)
|
|
300
|
+
return nil unless match
|
|
301
|
+
|
|
302
|
+
match[1]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Extract value from FontInfo dictionary
|
|
306
|
+
#
|
|
307
|
+
# @param key [String] FontInfo key (e.g., "FullName")
|
|
308
|
+
# @return [String, nil] Value or nil if not found
|
|
309
|
+
def extract_fontinfo_value(key)
|
|
310
|
+
text = decrypted? ? @decrypted_data : @clear_text
|
|
311
|
+
|
|
312
|
+
# Look for (FullName) readonly (value) readonly pattern
|
|
313
|
+
# This pattern handles nested parentheses in values
|
|
314
|
+
pattern = /\(#{Regexp.escape(key)}\)\s+readonly\s+(\([^()]*\)|\((?:[^()]*\([^()]*\)[^()]*)*\))\s+readonly/
|
|
315
|
+
match = text.match(pattern)
|
|
316
|
+
return match[1].gsub(/^\(|\)$/, "") if match
|
|
317
|
+
|
|
318
|
+
# Look for /FullName (value) def pattern
|
|
319
|
+
pattern = /\/#{Regexp.escape(key)}\s+\(([^)]+)\)\s+def/
|
|
320
|
+
match = text.match(pattern)
|
|
321
|
+
return match[1] if match
|
|
322
|
+
|
|
323
|
+
# Look for /FullName (value) readonly readonly pattern
|
|
324
|
+
pattern = /\/#{Regexp.escape(key)}\s+(\([^()]*\)|\((?:[^()]*\([^()]*\)[^()]*)*\))\s+readonly\s+readonly/
|
|
325
|
+
match = text.match(pattern)
|
|
326
|
+
return match[1].gsub(/^\(|\)$/, "") if match
|
|
327
|
+
|
|
328
|
+
nil
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
data/lib/fontisan/version.rb
CHANGED
data/lib/fontisan.rb
CHANGED
|
@@ -83,6 +83,7 @@ require_relative "fontisan/true_type_collection"
|
|
|
83
83
|
require_relative "fontisan/open_type_collection"
|
|
84
84
|
require_relative "fontisan/woff_font"
|
|
85
85
|
require_relative "fontisan/woff2_font"
|
|
86
|
+
require_relative "fontisan/type1_font"
|
|
86
87
|
|
|
87
88
|
# Font extensions for table-based construction
|
|
88
89
|
require_relative "fontisan/true_type_font_extensions"
|
|
@@ -166,6 +167,7 @@ require_relative "fontisan/collection/writer"
|
|
|
166
167
|
require_relative "fontisan/collection/builder"
|
|
167
168
|
|
|
168
169
|
# Format conversion infrastructure
|
|
170
|
+
require_relative "fontisan/conversion_options"
|
|
169
171
|
require_relative "fontisan/converters/conversion_strategy"
|
|
170
172
|
require_relative "fontisan/converters/table_copier"
|
|
171
173
|
require_relative "fontisan/converters/outline_converter"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -168,6 +168,7 @@ files:
|
|
|
168
168
|
- lib/fontisan/config/variable_settings.yml
|
|
169
169
|
- lib/fontisan/config/woff2_settings.yml
|
|
170
170
|
- lib/fontisan/constants.rb
|
|
171
|
+
- lib/fontisan/conversion_options.rb
|
|
171
172
|
- lib/fontisan/converters/cff_table_builder.rb
|
|
172
173
|
- lib/fontisan/converters/collection_converter.rb
|
|
173
174
|
- lib/fontisan/converters/conversion_strategy.rb
|
|
@@ -178,6 +179,7 @@ files:
|
|
|
178
179
|
- lib/fontisan/converters/outline_optimizer.rb
|
|
179
180
|
- lib/fontisan/converters/svg_generator.rb
|
|
180
181
|
- lib/fontisan/converters/table_copier.rb
|
|
182
|
+
- lib/fontisan/converters/type1_converter.rb
|
|
181
183
|
- lib/fontisan/converters/woff2_encoder.rb
|
|
182
184
|
- lib/fontisan/converters/woff_writer.rb
|
|
183
185
|
- lib/fontisan/dfont_collection.rb
|
|
@@ -348,6 +350,28 @@ files:
|
|
|
348
350
|
- lib/fontisan/true_type_collection.rb
|
|
349
351
|
- lib/fontisan/true_type_font.rb
|
|
350
352
|
- lib/fontisan/true_type_font_extensions.rb
|
|
353
|
+
- lib/fontisan/type1.rb
|
|
354
|
+
- lib/fontisan/type1/afm_generator.rb
|
|
355
|
+
- lib/fontisan/type1/afm_parser.rb
|
|
356
|
+
- lib/fontisan/type1/agl.rb
|
|
357
|
+
- lib/fontisan/type1/charstring_converter.rb
|
|
358
|
+
- lib/fontisan/type1/charstrings.rb
|
|
359
|
+
- lib/fontisan/type1/conversion_options.rb
|
|
360
|
+
- lib/fontisan/type1/decryptor.rb
|
|
361
|
+
- lib/fontisan/type1/encodings.rb
|
|
362
|
+
- lib/fontisan/type1/font_dictionary.rb
|
|
363
|
+
- lib/fontisan/type1/generator.rb
|
|
364
|
+
- lib/fontisan/type1/inf_generator.rb
|
|
365
|
+
- lib/fontisan/type1/pfa_generator.rb
|
|
366
|
+
- lib/fontisan/type1/pfa_parser.rb
|
|
367
|
+
- lib/fontisan/type1/pfb_generator.rb
|
|
368
|
+
- lib/fontisan/type1/pfb_parser.rb
|
|
369
|
+
- lib/fontisan/type1/pfm_generator.rb
|
|
370
|
+
- lib/fontisan/type1/pfm_parser.rb
|
|
371
|
+
- lib/fontisan/type1/private_dict.rb
|
|
372
|
+
- lib/fontisan/type1/ttf_to_type1_converter.rb
|
|
373
|
+
- lib/fontisan/type1/upm_scaler.rb
|
|
374
|
+
- lib/fontisan/type1_font.rb
|
|
351
375
|
- lib/fontisan/utilities/brotli_wrapper.rb
|
|
352
376
|
- lib/fontisan/utilities/checksum_calculator.rb
|
|
353
377
|
- lib/fontisan/utils/thread_pool.rb
|