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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +214 -51
  3. data/README.adoc +160 -0
  4. data/lib/fontisan/cli.rb +177 -6
  5. data/lib/fontisan/commands/convert_command.rb +32 -1
  6. data/lib/fontisan/config/conversion_matrix.yml +132 -4
  7. data/lib/fontisan/constants.rb +12 -0
  8. data/lib/fontisan/conversion_options.rb +378 -0
  9. data/lib/fontisan/converters/collection_converter.rb +45 -10
  10. data/lib/fontisan/converters/format_converter.rb +2 -0
  11. data/lib/fontisan/converters/outline_converter.rb +78 -4
  12. data/lib/fontisan/converters/type1_converter.rb +559 -0
  13. data/lib/fontisan/font_loader.rb +46 -3
  14. data/lib/fontisan/type1/afm_generator.rb +436 -0
  15. data/lib/fontisan/type1/afm_parser.rb +298 -0
  16. data/lib/fontisan/type1/agl.rb +456 -0
  17. data/lib/fontisan/type1/charstring_converter.rb +240 -0
  18. data/lib/fontisan/type1/charstrings.rb +408 -0
  19. data/lib/fontisan/type1/conversion_options.rb +243 -0
  20. data/lib/fontisan/type1/decryptor.rb +183 -0
  21. data/lib/fontisan/type1/encodings.rb +697 -0
  22. data/lib/fontisan/type1/font_dictionary.rb +514 -0
  23. data/lib/fontisan/type1/generator.rb +220 -0
  24. data/lib/fontisan/type1/inf_generator.rb +332 -0
  25. data/lib/fontisan/type1/pfa_generator.rb +343 -0
  26. data/lib/fontisan/type1/pfa_parser.rb +158 -0
  27. data/lib/fontisan/type1/pfb_generator.rb +291 -0
  28. data/lib/fontisan/type1/pfb_parser.rb +166 -0
  29. data/lib/fontisan/type1/pfm_generator.rb +610 -0
  30. data/lib/fontisan/type1/pfm_parser.rb +433 -0
  31. data/lib/fontisan/type1/private_dict.rb +285 -0
  32. data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
  33. data/lib/fontisan/type1/upm_scaler.rb +118 -0
  34. data/lib/fontisan/type1.rb +73 -0
  35. data/lib/fontisan/type1_font.rb +331 -0
  36. data/lib/fontisan/version.rb +1 -1
  37. data/lib/fontisan.rb +2 -0
  38. 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.2.11"
4
+ VERSION = "0.2.12"
5
5
  end
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.11
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-17 00:00:00.000000000 Z
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