fontisan 0.2.10 → 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +216 -42
  3. data/README.adoc +160 -0
  4. data/lib/fontisan/cli.rb +177 -6
  5. data/lib/fontisan/collection/table_analyzer.rb +88 -3
  6. data/lib/fontisan/commands/convert_command.rb +32 -1
  7. data/lib/fontisan/config/conversion_matrix.yml +132 -4
  8. data/lib/fontisan/constants.rb +12 -0
  9. data/lib/fontisan/conversion_options.rb +378 -0
  10. data/lib/fontisan/converters/cff_table_builder.rb +198 -0
  11. data/lib/fontisan/converters/collection_converter.rb +45 -10
  12. data/lib/fontisan/converters/format_converter.rb +2 -0
  13. data/lib/fontisan/converters/glyf_table_builder.rb +63 -0
  14. data/lib/fontisan/converters/outline_converter.rb +111 -374
  15. data/lib/fontisan/converters/outline_extraction.rb +93 -0
  16. data/lib/fontisan/converters/outline_optimizer.rb +89 -0
  17. data/lib/fontisan/converters/type1_converter.rb +559 -0
  18. data/lib/fontisan/font_loader.rb +46 -3
  19. data/lib/fontisan/glyph_accessor.rb +29 -1
  20. data/lib/fontisan/type1/afm_generator.rb +436 -0
  21. data/lib/fontisan/type1/afm_parser.rb +298 -0
  22. data/lib/fontisan/type1/agl.rb +456 -0
  23. data/lib/fontisan/type1/charstring_converter.rb +240 -0
  24. data/lib/fontisan/type1/charstrings.rb +408 -0
  25. data/lib/fontisan/type1/conversion_options.rb +243 -0
  26. data/lib/fontisan/type1/decryptor.rb +183 -0
  27. data/lib/fontisan/type1/encodings.rb +697 -0
  28. data/lib/fontisan/type1/font_dictionary.rb +514 -0
  29. data/lib/fontisan/type1/generator.rb +220 -0
  30. data/lib/fontisan/type1/inf_generator.rb +332 -0
  31. data/lib/fontisan/type1/pfa_generator.rb +343 -0
  32. data/lib/fontisan/type1/pfa_parser.rb +158 -0
  33. data/lib/fontisan/type1/pfb_generator.rb +291 -0
  34. data/lib/fontisan/type1/pfb_parser.rb +166 -0
  35. data/lib/fontisan/type1/pfm_generator.rb +610 -0
  36. data/lib/fontisan/type1/pfm_parser.rb +433 -0
  37. data/lib/fontisan/type1/private_dict.rb +285 -0
  38. data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
  39. data/lib/fontisan/type1/upm_scaler.rb +118 -0
  40. data/lib/fontisan/type1.rb +73 -0
  41. data/lib/fontisan/type1_font.rb +331 -0
  42. data/lib/fontisan/variation/cache.rb +1 -0
  43. data/lib/fontisan/version.rb +1 -1
  44. data/lib/fontisan/woff2_font.rb +3 -3
  45. data/lib/fontisan.rb +2 -0
  46. metadata +30 -2
@@ -0,0 +1,559 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../conversion_options"
4
+ require_relative "../type1/charstring_converter"
5
+ require_relative "../type1/font_dictionary"
6
+ require_relative "../type1/charstrings"
7
+ require_relative "../type1_font"
8
+ require_relative "cff_table_builder"
9
+
10
+ module Fontisan
11
+ module Converters
12
+ # Converter for Adobe Type 1 fonts to/from SFNT formats.
13
+ #
14
+ # [`Type1Converter`](lib/fontisan/converters/type1_converter.rb) handles
15
+ # bidirectional conversion between Type 1 fonts (PFB/PFA) and SFNT-based
16
+ # formats (TTF, OTF, WOFF, WOFF2).
17
+ #
18
+ # == Conversion Strategy
19
+ #
20
+ # Type 1 fonts use PostScript CharStrings that are similar to CFF CharStrings
21
+ # used in OpenType fonts. The conversion uses CharStringConverter for the
22
+ # CharString translation.
23
+ #
24
+ # * Type 1 → OTF: Convert Type 1 CharStrings to CFF format, build CFF table
25
+ # * OTF → Type 1: Convert CFF CharStrings to Type 1 format, build PFB/PFA
26
+ # * Type 1 → TTF: Type 1 → OTF → TTF (via OutlineConverter)
27
+ # * TTF → Type 1: TTF → OTF → Type 1
28
+ #
29
+ # == Conversion Options
30
+ #
31
+ # The converter accepts [`ConversionOptions`](../conversion_options) with
32
+ # opening and generating options:
33
+ #
34
+ # * Opening options: decompose_composites, generate_unicode, read_all_records
35
+ # * Generating options: decompose_on_output, hinting_mode, write_pfm, write_afm
36
+ #
37
+ # @example Convert Type 1 to OTF with options
38
+ # font = FontLoader.load("font.pfb")
39
+ # options = ConversionOptions.recommended(from: :type1, to: :otf)
40
+ # converter = Type1Converter.new
41
+ # tables = converter.convert(font, options: options)
42
+ #
43
+ # @example Convert with preset
44
+ # options = ConversionOptions.from_preset(:type1_to_modern)
45
+ # tables = converter.convert(font, options: options)
46
+ #
47
+ # @see https://www.adobe.com/devnet/font/pdfs/Type1.pdf
48
+ # @see CharStringConverter
49
+ class Type1Converter
50
+ include ConversionStrategy
51
+ include CffTableBuilder
52
+
53
+ # Initialize a new Type1Converter
54
+ #
55
+ # @param options [Hash] Converter options
56
+ # @option options [Boolean] :optimize_cff Enable CFF optimization (default: false)
57
+ # @option options [Boolean] :preserve_hints Preserve hinting (default: true)
58
+ # @option options [Symbol] :target_format Target format for conversion
59
+ def initialize(options = {})
60
+ @optimize_cff = options.fetch(:optimize_cff, false)
61
+ @preserve_hints = options.fetch(:preserve_hints, true)
62
+ @target_format = options[:target_format]
63
+ end
64
+
65
+ # Convert font to target format
66
+ #
67
+ # @param font [Type1Font, OpenTypeFont, TrueTypeFont] Source font
68
+ # @param options [Hash, ConversionOptions] Conversion options
69
+ # @option options [Symbol] :target_format Target format override
70
+ # @option options [ConversionOptions] :options ConversionOptions object
71
+ # @return [Hash<String, String>] Map of table tags to binary data
72
+ def convert(font, options = {})
73
+ # Extract ConversionOptions if provided
74
+ conv_options = extract_conversion_options(options)
75
+
76
+ target_format = options[:target_format] || conv_options&.to || @target_format ||
77
+ detect_target_format(font)
78
+ validate(font, target_format)
79
+
80
+ # Apply opening options to source font
81
+ apply_opening_options(font, conv_options) if conv_options
82
+
83
+ source_format = detect_format(font)
84
+
85
+ case [source_format, target_format]
86
+ when %i[type1 otf]
87
+ convert_type1_to_otf(font, conv_options)
88
+ when %i[otf type1]
89
+ convert_otf_to_type1(font, conv_options)
90
+ when %i[type1 ttf]
91
+ convert_type1_to_ttf(font, conv_options)
92
+ when %i[ttf type1]
93
+ convert_ttf_to_type1(font, conv_options)
94
+ else
95
+ raise Fontisan::Error,
96
+ "Unsupported conversion: #{source_format} → #{target_format}"
97
+ end
98
+ end
99
+
100
+ # Get supported conversions
101
+ #
102
+ # @return [Array<Array<Symbol>>] Supported conversion pairs
103
+ def supported_conversions
104
+ [
105
+ %i[type1 otf],
106
+ %i[otf type1],
107
+ %i[type1 ttf],
108
+ %i[ttf type1],
109
+ ]
110
+ end
111
+
112
+ # Validate font for conversion
113
+ #
114
+ # @param font [Type1Font, OpenTypeFont, TrueTypeFont] Font to validate
115
+ # @param target_format [Symbol] Target format
116
+ # @return [Boolean] True if valid
117
+ # @raise [ArgumentError] If font is invalid
118
+ # @raise [Error] If conversion is not supported
119
+ def validate(font, target_format)
120
+ raise ArgumentError, "Font cannot be nil" if font.nil?
121
+
122
+ unless font.respond_to?(:font_dictionary) || font.respond_to?(:tables)
123
+ raise ArgumentError,
124
+ "Font must be Type1Font or have :tables method"
125
+ end
126
+
127
+ source_format = detect_format(font)
128
+ unless supports?(source_format, target_format)
129
+ raise Fontisan::Error,
130
+ "Conversion #{source_format} → #{target_format} not supported"
131
+ end
132
+
133
+ true
134
+ end
135
+
136
+ private
137
+
138
+ # Extract ConversionOptions from options hash
139
+ #
140
+ # @param options [Hash, ConversionOptions] Options or hash containing :options key
141
+ # @return [ConversionOptions, nil] Extracted ConversionOptions or nil
142
+ def extract_conversion_options(options)
143
+ return options if options.is_a?(ConversionOptions)
144
+
145
+ options[:options] if options.is_a?(Hash)
146
+ end
147
+
148
+ # Apply opening options to source font
149
+ #
150
+ # @param font [Type1Font] Source font
151
+ # @param conv_options [ConversionOptions] Conversion options with opening options
152
+ def apply_opening_options(font, conv_options)
153
+ return unless font.is_a?(Type1Font)
154
+ return unless conv_options
155
+
156
+ # Generate Unicode codepoints if requested
157
+ if conv_options.opening_option?(:generate_unicode)
158
+ generate_unicode_mappings(font)
159
+ end
160
+
161
+ # Decompose seac composites if requested
162
+ if conv_options.opening_option?(:decompose_composites)
163
+ decompose_seac_glyphs(font)
164
+ end
165
+
166
+ # Read all font dictionary records if requested
167
+ if conv_options.opening_option?(:read_all_records) && font.font_dictionary.respond_to?(:reload)
168
+ # Ensure full font dictionary is loaded
169
+ font.font_dictionary.reload
170
+ end
171
+ end
172
+
173
+ # Generate Unicode codepoints from glyph names/encoding
174
+ #
175
+ # @param font [Type1Font] Source Type 1 font
176
+ def generate_unicode_mappings(_font)
177
+ # Placeholder: Generate Unicode mappings from glyph names
178
+ # A full implementation would:
179
+ # 1. Parse the Adobe Glyph List
180
+ # 2. Map glyph names to Unicode codepoints
181
+ # 3. Update the charstrings encoding
182
+ #
183
+ # For now, this is a no-op placeholder
184
+ nil
185
+ end
186
+
187
+ # Decompose seac composite glyphs to base glyphs
188
+ #
189
+ # @param font [Type1Font] Source Type 1 font
190
+ def decompose_seac_glyphs(_font)
191
+ # Placeholder: Decompose seac composites
192
+ # A full implementation would:
193
+ # 1. Identify glyphs using seac operator
194
+ # 2. Resolve the accent character from encoding
195
+ # 3. Extract component outlines recursively
196
+ # 4. Merge into single glyph
197
+ #
198
+ # For now, this is a no-op placeholder
199
+ nil
200
+ end
201
+
202
+ # Detect font format
203
+ #
204
+ # @param font [Type1Font, OpenTypeFont, TrueTypeFont] Font to detect
205
+ # @return [Symbol] Font format (:type1, :ttf, :otf)
206
+ def detect_format(font)
207
+ case font
208
+ when Type1Font
209
+ :type1
210
+ when TrueTypeFont
211
+ :ttf
212
+ when OpenTypeFont
213
+ :otf
214
+ else
215
+ # Try to detect from tables
216
+ if font.respond_to?(:tables)
217
+ if font.tables.key?("glyf")
218
+ :ttf
219
+ elsif font.tables.key?("CFF ") || font.tables.key?("CFF2")
220
+ :otf
221
+ else
222
+ raise Fontisan::Error, "Cannot detect font format"
223
+ end
224
+ else
225
+ raise Fontisan::Error, "Unknown font type: #{font.class}"
226
+ end
227
+ end
228
+ end
229
+
230
+ # Detect target format from font class or options
231
+ #
232
+ # @param font [Type1Font, OpenTypeFont, TrueTypeFont] Source font
233
+ # @return [Symbol] Target format
234
+ def detect_target_format(font)
235
+ case font
236
+ when Type1Font
237
+ :otf # Default: Type 1 → OTF
238
+ when TrueTypeFont
239
+ :type1 # TTF → Type 1
240
+ when OpenTypeFont
241
+ :type1 # OTF → Type 1
242
+ else
243
+ :otf
244
+ end
245
+ end
246
+
247
+ # Convert Type 1 font to OpenType/CFF
248
+ #
249
+ # @param font [Type1Font] Source Type 1 font
250
+ # @param options [Hash] Conversion options
251
+ # @return [Hash<String, String>] Target tables including CFF table
252
+ def convert_type1_to_otf(font, _options = {})
253
+ # Convert Type 1 CharStrings to CFF format
254
+ converter = Type1::CharStringConverter.new(font.charstrings)
255
+ cff_charstrings = {}
256
+
257
+ font.charstrings.each_charstring do |glyph_name, charstring|
258
+ cff_charstrings[glyph_name] = converter.convert(charstring)
259
+ end
260
+
261
+ # Build font dictionary for CFF
262
+ font_dict = build_cff_font_dict(font)
263
+
264
+ # Build private dictionary for CFF
265
+ private_dict = build_cff_private_dict(font)
266
+
267
+ # Build CFF table
268
+ # Note: This is a simplified implementation
269
+ # A full implementation would build proper CFF INDEX structures
270
+ cff_data = build_cff_table_data(font, cff_charstrings, font_dict,
271
+ private_dict)
272
+
273
+ # Build other required SFNT tables
274
+ tables = {}
275
+
276
+ # Build head table
277
+ tables["head"] = build_head_table(font)
278
+
279
+ # Build hhea table
280
+ tables["hhea"] = build_hhea_table(font)
281
+
282
+ # Build maxp table
283
+ tables["maxp"] = build_maxp_table(font)
284
+
285
+ # Build name table
286
+ tables["name"] = build_name_table(font)
287
+
288
+ # Build OS/2 table
289
+ tables["OS/2"] = build_os2_table(font)
290
+
291
+ # Build post table
292
+ tables["post"] = build_post_table(font)
293
+
294
+ # Build cmap table
295
+ tables["cmap"] = build_cmap_table(font)
296
+
297
+ # Add CFF table
298
+ tables["CFF "] = cff_data
299
+
300
+ tables
301
+ end
302
+
303
+ # Convert OpenType/CFF font to Type 1
304
+ #
305
+ # @param font [OpenTypeFont] Source OpenType font
306
+ # @return [Hash<String, String>] Type 1 font data as PFB
307
+ def convert_otf_to_type1(font)
308
+ # Extract CFF table
309
+ cff_table = font.table("CFF ")
310
+ raise Fontisan::Error, "CFF table not found" unless cff_table
311
+
312
+ # Parse CFF table to extract CharStrings
313
+ # Note: This is a simplified implementation
314
+ # A full implementation would parse CFF INDEX structures
315
+
316
+ # Convert CFF CharStrings to Type 1 format
317
+ type1_charstrings = {}
318
+ Type1::CharStringConverter.new
319
+
320
+ # Extract glyph outlines from CFF
321
+ # For each glyph, convert CFF CharString to Type 1
322
+ font.outlines.each_with_index do |outline, index|
323
+ glyph_name = font.glyph_name(index) || "glyph#{index}"
324
+ # Reverse conversion: CFF → Type 1
325
+ # This is a placeholder - full implementation requires CFF parser
326
+ type1_charstrings[glyph_name] = convert_cff_to_type1(outline)
327
+ end
328
+
329
+ # Build Type 1 font data
330
+ build_type1_data(font, type1_charstrings)
331
+ end
332
+
333
+ # Convert Type 1 font to TrueType (via OTF)
334
+ #
335
+ # @param font [Type1Font] Source Type 1 font
336
+ # @param options [Hash] Conversion options
337
+ # @return [Hash<String, String>] Target tables including glyf table
338
+ def convert_type1_to_ttf(font, options = {})
339
+ # First convert to OTF
340
+ otf_tables = convert_type1_to_otf(font, options)
341
+
342
+ # Then use OutlineConverter to convert OTF to TTF
343
+ # Create a temporary OTF font object
344
+ temp_otf = OpenTypeFont.new
345
+ otf_tables.each do |tag, data|
346
+ temp_otf.tables[tag] = data
347
+ end
348
+
349
+ # Use OutlineConverter for OTF → TTF
350
+ outline_converter = OutlineConverter.new(
351
+ optimize_cff: @optimize_cff,
352
+ preserve_hints: @preserve_hints,
353
+ target_format: :ttf,
354
+ )
355
+
356
+ outline_converter.convert(temp_otf, target_format: :ttf)
357
+ end
358
+
359
+ # Convert TrueType font to Type 1 (via OTF)
360
+ #
361
+ # @param font [TrueTypeFont] Source TrueType font
362
+ # @return [Hash<String, String>] Type 1 font data as PFB
363
+ def convert_ttf_to_type1(font)
364
+ # First use OutlineConverter to convert TTF to OTF
365
+ outline_converter = OutlineConverter.new(
366
+ optimize_cff: @optimize_cff,
367
+ preserve_hints: @preserve_hints,
368
+ target_format: :otf,
369
+ )
370
+
371
+ otf_tables = outline_converter.convert(font, target_format: :otf)
372
+
373
+ # Create a temporary OTF font object
374
+ temp_otf = OpenTypeFont.new
375
+ otf_tables.each do |tag, data|
376
+ temp_otf.tables[tag] = data
377
+ end
378
+
379
+ # Then convert OTF to Type 1
380
+ convert_otf_to_type1(temp_otf)
381
+ end
382
+
383
+ # Build CFF font dictionary from Type 1 font
384
+ #
385
+ # @param font [Type1Font] Source Type 1 font
386
+ # @return [Hash] CFF font dictionary data
387
+ def build_cff_font_dict(font)
388
+ {
389
+ version: font.font_dictionary.version || "001.000",
390
+ notice: font.font_dictionary.notice || "",
391
+ copyright: font.font_dictionary.copyright || "",
392
+ full_name: font.font_dictionary.full_name || font.font_name,
393
+ family_name: font.font_dictionary.family_name || font.font_name,
394
+ weight: font.font_dictionary.weight || "Medium",
395
+ font_b_box: font.font_dictionary.font_bbox || [0, 0, 0, 0],
396
+ font_matrix: font.font_dictionary.font_matrix || [0.001, 0, 0, 0.001,
397
+ 0, 0],
398
+ charset: font.charstrings.encoding.keys,
399
+ encoding: font.charstrings.encoding,
400
+ }
401
+ end
402
+
403
+ # Build CFF private dictionary from Type 1 font
404
+ #
405
+ # @param font [Type1Font] Source Type 1 font
406
+ # @return [Hash] CFF private dictionary data
407
+ def build_cff_private_dict(font)
408
+ private_dict = font.private_dict
409
+ {
410
+ blue_values: private_dict.blue_values || [],
411
+ other_blues: private_dict.other_blues || [],
412
+ family_blues: private_dict.family_blues || [],
413
+ family_other_blues: private_dict.family_other_blues || [],
414
+ blue_scale: private_dict.blue_scale || 0.039625,
415
+ blue_shift: private_dict.blue_shift || 7,
416
+ blue_fuzz: private_dict.blue_fuzz || 1,
417
+ std_hw: private_dict.std_hw || 0,
418
+ std_vw: private_dict.std_vw || 0,
419
+ stem_snap_h: private_dict.stem_snap_h || [],
420
+ stem_snap_v: private_dict.stem_snap_v || [],
421
+ force_bold: private_dict.force_bold || false,
422
+ language_group: private_dict.language_group || 0,
423
+ expansion_factor: private_dict.expansion_factor || 0.06,
424
+ initial_random_seed: private_dict.initial_random_seed || 0,
425
+ }
426
+ end
427
+
428
+ # Build CFF table data
429
+ #
430
+ # @param font [Type1Font] Source Type 1 font
431
+ # @param charstrings [Hash] CFF CharStrings (glyph_name => data)
432
+ # @param font_dict [Hash] CFF font dictionary (not used, kept for compatibility)
433
+ # @param private_dict [Hash] CFF private dictionary (not used, kept for compatibility)
434
+ # @return [String] CFF table binary data
435
+ def build_cff_table_data(font, charstrings, _font_dict, _private_dict)
436
+ # Convert charstrings hash to array (build_cff_table expects array)
437
+ charstrings_array = charstrings.values
438
+
439
+ # Build CFF table using CffTableBuilder
440
+ # We need to pass the Type1Font as-is for metadata extraction
441
+ build_cff_table(charstrings_array, [], font)
442
+ end
443
+
444
+ # Override extract_font_name to handle Type1Font
445
+ #
446
+ # @param font [Type1Font, TrueTypeFont, OpenTypeFont] Font
447
+ # @return [String] Font name
448
+ def extract_font_name(font)
449
+ if font.is_a?(Type1Font)
450
+ # Get font name from Type1Font
451
+ name = font.font_name || font.font_dictionary&.font_name
452
+ return name.dup.force_encoding("ASCII-8BIT") if name
453
+ end
454
+
455
+ # Fall back to original implementation for TrueTypeFont/OpenTypeFont
456
+ super
457
+ end
458
+
459
+ # Convert CFF outline to Type 1 CharString
460
+ #
461
+ # @param outline [Outline] Glyph outline
462
+ # @return [String] Type 1 CharString bytecode
463
+ def convert_cff_to_type1(_outline)
464
+ # Reverse conversion from CFF to Type 1
465
+ # This is a placeholder implementation
466
+ # Full implementation requires:
467
+ # 1. Parse CFF CharString to commands
468
+ # 2. Map CFF operators to Type 1 operators
469
+ # 3. Encode numbers in Type 1 format
470
+ # 4. Handle hints and subroutines
471
+
472
+ String.new(encoding: Encoding::ASCII_8BIT)
473
+ end
474
+
475
+ # Build Type 1 font data
476
+ #
477
+ # @param font [OpenTypeFont] Source OpenType font
478
+ # @param charstrings [Hash] Type 1 CharStrings
479
+ # @return [Hash] Type 1 font data with :pfb key
480
+ def build_type1_data(_font, _charstrings)
481
+ # Build PFB format
482
+ # This is a placeholder implementation
483
+ # Full implementation requires:
484
+ # 1. Build Font Dictionary
485
+ # 2. Build Private Dictionary
486
+ # 3. Build CharStrings
487
+ # 4. Encrypt with eexec
488
+ # 5. Format as PFB chunks
489
+
490
+ pfb_data = String.new(encoding: Encoding::ASCII_8BIT)
491
+
492
+ { pfb: pfb_data }
493
+ end
494
+
495
+ # Build head table from Type 1 font
496
+ #
497
+ # @param font [Type1Font] Source Type 1 font
498
+ # @return [String] head table binary data
499
+ def build_head_table(_font)
500
+ # Placeholder: Build actual head table
501
+ String.new(encoding: Encoding::ASCII_8BIT)
502
+ end
503
+
504
+ # Build hhea table from Type 1 font
505
+ #
506
+ # @param font [Type1Font] Source Type 1 font
507
+ # @return [String] hhea table binary data
508
+ def build_hhea_table(_font)
509
+ # Placeholder: Build actual hhea table
510
+ String.new(encoding: Encoding::ASCII_8BIT)
511
+ end
512
+
513
+ # Build maxp table from Type 1 font
514
+ #
515
+ # @param font [Type1Font] Source Type 1 font
516
+ # @return [String] maxp table binary data
517
+ def build_maxp_table(_font)
518
+ # Placeholder: Build actual maxp table
519
+ String.new(encoding: Encoding::ASCII_8BIT)
520
+ end
521
+
522
+ # Build name table from Type 1 font
523
+ #
524
+ # @param font [Type1Font] Source Type 1 font
525
+ # @return [String] name table binary data
526
+ def build_name_table(_font)
527
+ # Placeholder: Build actual name table
528
+ String.new(encoding: Encoding::ASCII_8BIT)
529
+ end
530
+
531
+ # Build OS/2 table from Type 1 font
532
+ #
533
+ # @param font [Type1Font] Source Type 1 font
534
+ # @return [String] OS/2 table binary data
535
+ def build_os2_table(_font)
536
+ # Placeholder: Build actual OS/2 table
537
+ String.new(encoding: Encoding::ASCII_8BIT)
538
+ end
539
+
540
+ # Build post table from Type 1 font
541
+ #
542
+ # @param font [Type1Font] Source Type 1 font
543
+ # @return [String] post table binary data
544
+ def build_post_table(_font)
545
+ # Placeholder: Build actual post table
546
+ String.new(encoding: Encoding::ASCII_8BIT)
547
+ end
548
+
549
+ # Build cmap table from Type 1 font
550
+ #
551
+ # @param font [Type1Font] Source Type 1 font
552
+ # @return [String] cmap table binary data
553
+ def build_cmap_table(_font)
554
+ # Placeholder: Build actual cmap table
555
+ String.new(encoding: Encoding::ASCII_8BIT)
556
+ end
557
+ end
558
+ end
559
+ end
@@ -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)
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 WOFF2 file."
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