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
data/lib/fontisan/cli.rb CHANGED
@@ -203,10 +203,10 @@ module Fontisan
203
203
 
204
204
  desc "convert FONT_FILE", "Convert font to different format"
205
205
  option :to, type: :string, required: true,
206
- desc: "Target format (ttf, otf, woff, woff2)",
206
+ desc: "Target format (ttf, otf, type1, t1, woff, woff2)",
207
207
  aliases: "-t"
208
- option :output, type: :string, required: true,
209
- desc: "Output file path",
208
+ option :output, type: :string,
209
+ desc: "Output file path (required unless --show-options)",
210
210
  aliases: "-o"
211
211
  option :coordinates, type: :string,
212
212
  desc: "Instance coordinates (e.g., wght=700,wdth=100)",
@@ -232,10 +232,34 @@ module Fontisan
232
232
  desc: "Italic axis value (alternative to --coordinates)"
233
233
  option :opsz, type: :numeric,
234
234
  desc: "Optical size axis value (alternative to --coordinates)"
235
+ # Conversion options
236
+ option :preset, type: :string,
237
+ desc: "Use named preset (type1_to_modern, modern_to_type1, web_optimized, archive_to_modern)"
238
+ option :show_options, type: :boolean, default: false,
239
+ desc: "Show recommended options for the conversion and exit"
240
+ option :decompose, type: :boolean,
241
+ desc: "Decompose composite glyphs (opening option)"
242
+ option :convert_curves, type: :boolean,
243
+ desc: "Convert curves during conversion (opening option)"
244
+ option :scale_to_1000, type: :boolean,
245
+ desc: "Scale to 1000 units per em (opening option)"
246
+ option :autohint, type: :boolean,
247
+ desc: "Auto-hint the font (opening option)"
248
+ option :generate_unicode, type: :boolean,
249
+ desc: "Generate Unicode mappings (Type 1 opening option)"
250
+ option :hinting_mode, type: :string,
251
+ desc: "Hinting mode: preserve, auto, none, or full"
252
+ option :optimize_cff, type: :boolean,
253
+ desc: "Enable CFF subroutine optimization"
254
+ option :optimize_tables, type: :boolean,
255
+ desc: "Enable table optimization"
256
+ option :decompose_on_output, type: :boolean,
257
+ desc: "Decompose on output (generating option)"
235
258
  # Convert a font to a different format using the universal transformation pipeline.
236
259
  #
237
260
  # Supported conversions:
238
261
  # - TTF ↔ OTF: Outline format conversion
262
+ # - Type 1 ↔ TTF/OTF: Adobe Type 1 font conversion
239
263
  # - WOFF/WOFF2: Web font packaging
240
264
  # - Variable fonts: Automatic variation preservation or instance generation
241
265
  # - Collections (TTC/OTC/dfont): Preserve mixed TTF+OTF by default, or standardize with --target-format
@@ -261,6 +285,12 @@ module Fontisan
261
285
  # @example Convert TTF to OTF
262
286
  # fontisan convert font.ttf --to otf --output font.otf
263
287
  #
288
+ # @example Convert Type 1 to OTF
289
+ # fontisan convert font.pfb --to otf --output font.otf
290
+ #
291
+ # @example Convert OTF to Type 1
292
+ # fontisan convert font.otf --to type1 --output font.pfb
293
+ #
264
294
  # @example Convert TTC to OTC (preserves mixed formats by default)
265
295
  # fontisan convert family.ttc --to otc --output family.otc
266
296
  #
@@ -284,17 +314,46 @@ module Fontisan
284
314
  #
285
315
  # @example Convert without validation
286
316
  # fontisan convert font.ttf --to otf --output font.otf --no-validate
317
+ #
318
+ # @example Use named preset
319
+ # fontisan convert font.pfb --to otf --output font.otf --preset type1_to_modern
320
+ #
321
+ # @example Show recommended options for conversion
322
+ # fontisan convert font.ttf --to otf --show-options
323
+ #
324
+ # @example Convert with custom options
325
+ # fontisan convert font.ttf --to otf --output font.otf --autohint --hinting-mode auto
287
326
  def convert(font_file)
327
+ # Detect source format from file
328
+ source_format = detect_source_format(font_file)
329
+
330
+ # Handle --show-options
331
+ if options[:show_options]
332
+ show_recommended_options(source_format, options[:to])
333
+ return
334
+ end
335
+
336
+ # Validate output is provided when not using --show-options
337
+ unless options[:output]
338
+ raise Thor::Error, "Output path is required. Use --output option."
339
+ end
340
+
341
+ # Build ConversionOptions
342
+ conv_options = build_conversion_options(source_format, options[:to],
343
+ options)
344
+
288
345
  # Build instance coordinates from axis options
289
346
  instance_coords = build_instance_coordinates(options)
290
347
 
291
- # Merge coordinates into options
348
+ # Merge coordinates and ConversionOptions into convert_options
292
349
  convert_options = options.to_h.dup
293
350
  if instance_coords.any?
294
- convert_options[:instance_coordinates] =
295
- instance_coords
351
+ convert_options[:instance_coordinates] = instance_coords
296
352
  end
297
353
 
354
+ # Add ConversionOptions if built
355
+ convert_options[:options] = conv_options if conv_options
356
+
298
357
  command = Commands::ConvertCommand.new(font_file, convert_options)
299
358
  command.run
300
359
  rescue Errno::ENOENT, Error => e
@@ -668,5 +727,117 @@ module Fontisan
668
727
  puts " #{profile_name.to_s.ljust(20)} - #{config[:description]}"
669
728
  end
670
729
  end
730
+
731
+ # Detect source format from file extension
732
+ #
733
+ # @param font_file [String] Path to the font file
734
+ # @return [Symbol] Detected format symbol
735
+ def detect_source_format(font_file)
736
+ ext = File.extname(font_file).downcase
737
+ case ext
738
+ when ".ttf"
739
+ :ttf
740
+ when ".otf"
741
+ :otf
742
+ when ".pfb", ".pfa"
743
+ :type1
744
+ when ".ttc"
745
+ :ttc
746
+ when ".otc"
747
+ :otc
748
+ when ".dfont"
749
+ :dfont
750
+ when ".woff"
751
+ :woff
752
+ when ".woff2"
753
+ :woff2
754
+ when ".svg"
755
+ :svg
756
+ else
757
+ # Default to TTF for unknown extensions
758
+ :ttf
759
+ end
760
+ end
761
+
762
+ # Show recommended options for a conversion
763
+ #
764
+ # @param source_format [Symbol] Source format
765
+ # @param target_format_str [String] Target format string
766
+ # @return [void]
767
+ def show_recommended_options(source_format, target_format_str)
768
+ target_format = Fontisan::ConversionOptions.normalize_format(target_format_str)
769
+
770
+ puts "\nRecommended options for #{source_format.to_s.upcase} → #{target_format.to_s.upcase} conversion:"
771
+ puts "=" * 70
772
+
773
+ # Show recommended options
774
+ recommended = Fontisan::ConversionOptions.recommended(from: source_format,
775
+ to: target_format)
776
+ puts "\nOpening options:"
777
+ if recommended.opening.any?
778
+ recommended.opening.each do |key, value|
779
+ puts " --#{key.to_s.gsub('_', '-')}: #{value}"
780
+ end
781
+ else
782
+ puts " (none)"
783
+ end
784
+
785
+ puts "\nGenerating options:"
786
+ if recommended.generating.any?
787
+ recommended.generating.each do |key, value|
788
+ puts " --#{key.to_s.gsub('_', '-')}: #{value}"
789
+ end
790
+ else
791
+ puts " (none)"
792
+ end
793
+
794
+ puts "\nAvailable presets:"
795
+ Fontisan::ConversionOptions.available_presets.each do |preset|
796
+ puts " #{preset}"
797
+ end
798
+
799
+ puts "\nTo use preset:"
800
+ puts " fontisan convert #{source_format} --to #{target_format} --preset <name> --output output.ext"
801
+ puts "\n"
802
+ end
803
+
804
+ # Build ConversionOptions from CLI options
805
+ #
806
+ # @param source_format [Symbol] Source format
807
+ # @param target_format_str [String] Target format string
808
+ # @param opts [Hash] CLI options
809
+ # @return [ConversionOptions, nil] Built ConversionOptions or nil
810
+ def build_conversion_options(source_format, target_format_str, opts)
811
+ target_format = Fontisan::ConversionOptions.normalize_format(target_format_str)
812
+
813
+ # Use preset if specified
814
+ if opts[:preset]
815
+ return Fontisan::ConversionOptions.from_preset(opts[:preset])
816
+ end
817
+
818
+ # Build opening options from CLI flags
819
+ opening = {}
820
+ opening[:decompose_composites] = true if opts[:decompose]
821
+ opening[:convert_curves] = true if opts[:convert_curves]
822
+ opening[:scale_to_1000] = true if opts[:scale_to_1000]
823
+ opening[:autohint] = true if opts[:autohint]
824
+ opening[:generate_unicode] = true if opts[:generate_unicode]
825
+
826
+ # Build generating options from CLI flags
827
+ generating = {}
828
+ generating[:hinting_mode] = opts[:hinting_mode] if opts[:hinting_mode]
829
+ generating[:decompose_on_output] = true if opts[:decompose_on_output]
830
+ generating[:optimize_tables] = true if opts[:optimize_tables]
831
+
832
+ # Only create ConversionOptions if any options were set
833
+ return nil if opening.empty? && generating.empty?
834
+
835
+ Fontisan::ConversionOptions.new(
836
+ from: source_format,
837
+ to: target_format,
838
+ opening: opening,
839
+ generating: generating,
840
+ )
841
+ end
671
842
  end
672
843
  end
@@ -3,6 +3,7 @@
3
3
  require_relative "base_command"
4
4
  require_relative "../pipeline/transformation_pipeline"
5
5
  require_relative "../converters/collection_converter"
6
+ require_relative "../conversion_options"
6
7
  require_relative "../font_loader"
7
8
 
8
9
  module Fontisan
@@ -36,6 +37,16 @@ module Fontisan
36
37
  # coordinates: 'wght=700,wdth=100'
37
38
  # )
38
39
  # command.run
40
+ #
41
+ # @example Convert with ConversionOptions
42
+ # options = ConversionOptions.recommended(from: :ttf, to: :otf)
43
+ # command = ConvertCommand.new(
44
+ # 'input.ttf',
45
+ # to: 'otf',
46
+ # output: 'output.otf',
47
+ # options: options
48
+ # )
49
+ # command.run
39
50
  class ConvertCommand < BaseCommand
40
51
  # Initialize convert command
41
52
  #
@@ -52,6 +63,7 @@ module Fontisan
52
63
  # @option options [String] :target_format Target outline format for collections: 'preserve' (default), 'ttf', or 'otf'
53
64
  # @option options [Boolean] :no_validate Skip output validation
54
65
  # @option options [Boolean] :verbose Verbose output
66
+ # @option options [ConversionOptions] :options ConversionOptions object
55
67
  def initialize(font_path, options = {})
56
68
  super(font_path, options)
57
69
 
@@ -63,6 +75,9 @@ module Fontisan
63
75
  # Parse target format
64
76
  @target_format = parse_target_format(opts[:to])
65
77
 
78
+ # Extract ConversionOptions if provided
79
+ @conv_options = extract_conversion_options(opts)
80
+
66
81
  # Parse coordinates if string provided
67
82
  @coordinates = if opts[:coordinates]
68
83
  parse_coordinates(opts[:coordinates])
@@ -123,6 +138,9 @@ module Fontisan
123
138
  verbose: @options[:verbose],
124
139
  }
125
140
 
141
+ # Add ConversionOptions if available
142
+ pipeline_options[:conversion_options] = @conv_options if @conv_options
143
+
126
144
  # Add variation options if specified
127
145
  pipeline_options[:coordinates] = @coordinates if @coordinates
128
146
  pipeline_options[:instance_index] = @instance_index if @instance_index
@@ -196,6 +214,7 @@ module Fontisan
196
214
  output: @output_path,
197
215
  target_format: @collection_target_format,
198
216
  verbose: @options[:verbose],
217
+ options: @conv_options, # Pass ConversionOptions
199
218
  },
200
219
  )
201
220
 
@@ -309,10 +328,12 @@ module Fontisan
309
328
  :woff
310
329
  when "woff2"
311
330
  :woff2
331
+ when "type1", "type-1", "t1", "pfb", "pfa"
332
+ :type1
312
333
  else
313
334
  raise ArgumentError,
314
335
  "Unknown target format: #{format}. " \
315
- "Supported: ttf, otf, ttc, otc, dfont, svg, woff, woff2"
336
+ "Supported: ttf, otf, type1, t1, ttc, otc, dfont, svg, woff, woff2"
316
337
  end
317
338
  end
318
339
 
@@ -329,6 +350,16 @@ module Fontisan
329
350
  "#{(bytes / (1024.0 * 1024)).round(1)} MB"
330
351
  end
331
352
  end
353
+
354
+ # Extract ConversionOptions from options hash
355
+ #
356
+ # @param options [Hash, ConversionOptions] Options or hash containing :options key
357
+ # @return [ConversionOptions, nil] Extracted ConversionOptions or nil
358
+ def extract_conversion_options(options)
359
+ return options if options.is_a?(ConversionOptions)
360
+
361
+ options[:options] if options.is_a?(Hash)
362
+ end
332
363
  end
333
364
  end
334
365
  end
@@ -6,9 +6,13 @@
6
6
  # Format identifiers:
7
7
  # - ttf: TrueType Font (with glyf/loca tables)
8
8
  # - otf: OpenType Font with CFF outlines (with CFF table)
9
- # - woff: Web Open Font Format (not yet implemented)
10
- # - woff2: Web Open Font Format 2 (implemented in Phase 2)
11
- # - svg: SVG Font (implemented in Phase 2)
9
+ # - type1: Adobe Type 1 Font (PFB/PFA format)
10
+ # - woff: Web Open Font Format (zlib compression)
11
+ # - woff2: Web Open Font Format 2 (Brotli compression)
12
+ # - svg: SVG Font (deprecated, for conversion/inspection)
13
+ # - ttc: TrueType Collection
14
+ # - otc: OpenType Collection
15
+ # - dfont: Apple dfont suitcase
12
16
  #
13
17
  # Phase 1 (Milestone 1.3): Basic conversions
14
18
  # - Same format (copy/optimize): ttf→ttf, otf→otf
@@ -20,8 +24,11 @@
20
24
  # Phase 2 (Milestone 2.2): SVG font generation
21
25
  # - SVG export: ttf to svg, otf to svg
22
26
  #
27
+ # Phase 3: Type 1 font support
28
+ # - Type 1 conversions: type1↔otf, type1↔ttf
29
+ #
23
30
  # Future phases:
24
- # - Phase 3: Variable font conversions
31
+ # - Phase 4: Variable font conversions
25
32
 
26
33
  conversions:
27
34
  # Same-format conversions (copy/optimize)
@@ -58,6 +65,47 @@ conversions:
58
65
  table generation including cubic-to-quadratic curve conversion and
59
66
  proper TrueType glyph structure encoding.
60
67
 
68
+ # Phase 3 conversions - Type 1 fonts
69
+ - from: type1
70
+ to: otf
71
+ strategy: type1_converter
72
+ description: "Convert Type 1 to OpenType/CFF format"
73
+ status: foundation
74
+ notes: >
75
+ Type 1 to OpenType conversion using CharStringConverter. Converts Type 1
76
+ CharStrings to CFF format and builds CFF table with proper DICT structures.
77
+ seac composites are expanded. Some hinting may not be preserved.
78
+
79
+ - from: otf
80
+ to: type1
81
+ strategy: type1_converter
82
+ description: "Convert OpenType/CFF to Type 1 format"
83
+ status: foundation
84
+ notes: >
85
+ OpenType to Type 1 conversion. Reverse conversion from CFF CharStrings to
86
+ Type 1 format. Builds PFB output with eexec encryption. Some modern
87
+ OpenType features may be lost in conversion.
88
+
89
+ - from: type1
90
+ to: ttf
91
+ strategy: type1_converter
92
+ description: "Convert Type 1 to TrueType format"
93
+ status: foundation
94
+ notes: >
95
+ Two-step conversion: Type 1 → OTF → TTF. First converts to OpenType/CFF,
96
+ then to TrueType with cubic-to-quadratic curve approximation. Hinting may
97
+ not be preserved in either step.
98
+
99
+ - from: ttf
100
+ to: type1
101
+ strategy: type1_converter
102
+ description: "Convert TrueType to Type 1 format"
103
+ status: foundation
104
+ notes: >
105
+ Two-step conversion: TTF → OTF → Type 1. First converts to OpenType/CFF
106
+ with quadratic-to-cubic curve conversion, then to Type 1. Significant
107
+ approximation artifacts may occur due to curve conversions.
108
+
61
109
  # Phase 2 conversions (Milestone 2.1) - WOFF2
62
110
  - from: ttf
63
111
  to: woff2
@@ -147,6 +195,35 @@ conversions:
147
195
  transformation. Note: SVG fonts are deprecated in favor of WOFF2, but
148
196
  useful for fallback, conversion, and inspection purposes.
149
197
 
198
+ # Type 1 to web formats (via OTF intermediate)
199
+ - from: type1
200
+ to: woff
201
+ strategy: type1_converter
202
+ description: "Convert Type 1 to WOFF format"
203
+ status: foundation
204
+ notes: >
205
+ Two-step conversion: Type 1 → OTF → WOFF. Converts Type 1 to OpenType/CFF,
206
+ then compresses with zlib. Useful for web delivery of legacy Type 1 fonts.
207
+
208
+ - from: type1
209
+ to: woff2
210
+ strategy: type1_converter
211
+ description: "Convert Type 1 to WOFF2 format"
212
+ status: foundation
213
+ notes: >
214
+ Two-step conversion: Type 1 → OTF → WOFF2. Converts Type 1 to OpenType/CFF,
215
+ then compresses with Brotli. Best for web delivery of legacy Type 1 fonts.
216
+
217
+ - from: type1
218
+ to: svg
219
+ strategy: type1_converter
220
+ description: "Generate SVG font from Type 1"
221
+ status: foundation
222
+ notes: >
223
+ Type 1 to SVG conversion. Extracts outlines from Type 1 CharStrings and
224
+ generates SVG font format. Note: SVG fonts are deprecated but useful for
225
+ inspection and conversion workflows.
226
+
150
227
  # WOFF2 decompression (reverse conversions)
151
228
  - from: woff2
152
229
  to: ttf
@@ -296,6 +373,57 @@ compatibility:
296
373
  - hinting_loss: >
297
374
  CFF hints are not converted to TrueType hinting instructions.
298
375
 
376
+ type1_to_otf:
377
+ preserves:
378
+ - glyph_outlines: Type 1 CharStrings are similar to CFF
379
+ - font_metrics: FontBBox, metrics preserved
380
+ - name_table: FontInfo converted to name table
381
+ - hints: Some hints preserved (BlueValues, OtherBlues)
382
+ limitations:
383
+ - seac_expansion: >
384
+ seac composite glyphs are expanded to base glyphs.
385
+ - hinting_approximation: >
386
+ Some Type 1 hints may not translate exactly to CFF hints.
387
+ - lost_features: >
388
+ Type 1-specific features (Flex, multiple master) not preserved.
389
+
390
+ otf_to_type1:
391
+ preserves:
392
+ - glyph_outlines: CFF outlines converted to Type 1
393
+ - font_metrics: Basic metrics preserved
394
+ - name_table: name table converted to FontInfo
395
+ limitations:
396
+ - reverse_conversion: >
397
+ CFF to Type 1 is reverse conversion with potential data loss.
398
+ - modern_features_lost: >
399
+ Modern OpenType features (GPOS, GSUB variations) lost in Type 1.
400
+ - hinting_approximation: >
401
+ CFF hints may not translate exactly to Type 1 hints.
402
+
403
+ type1_to_ttf:
404
+ preserves:
405
+ - glyph_outlines: Via OTF intermediate
406
+ - font_metrics: Basic metrics preserved
407
+ limitations:
408
+ - multi_step_conversion: >
409
+ Type 1 → OTF → TTF, compounding approximation errors.
410
+ - curve_approximation: >
411
+ CFF cubic to TrueType quadratic approximation.
412
+ - significant_data_loss: >
413
+ Most Type 1 and OpenType features lost.
414
+
415
+ ttf_to_type1:
416
+ preserves:
417
+ - glyph_outlines: Via OTF intermediate
418
+ - font_metrics: Basic metrics preserved
419
+ limitations:
420
+ - multi_step_conversion: >
421
+ TTF → OTF → Type 1, compounding approximation errors.
422
+ - curve_approximation: >
423
+ Quadratic to cubic, then back to Type 1 curves.
424
+ - extreme_data_loss: >
425
+ Nearly all TrueType and Type 1 features lost.
426
+
299
427
  same_format:
300
428
  preserves:
301
429
  - all_tables
@@ -95,6 +95,18 @@ module Fontisan
95
95
  # CFF2 table tag identifier (CFF version 2 with variations)
96
96
  CFF2_TAG = "CFF2"
97
97
 
98
+ # Adobe Type 1 font format constants
99
+ # PFB (Printer Font Binary) chunk markers
100
+ PFB_ASCII_CHUNK = 0x8001
101
+ PFB_BINARY_CHUNK = 0x8002
102
+
103
+ # PFA (Printer Font ASCII) file signatures
104
+ PFA_SIGNATURE_ADOBE_1_0 = "%!PS-AdobeFont-1.0"
105
+ PFA_SIGNATURE_ADOBE_3_0 = "%!PS-Adobe-3.0 Resource-Font"
106
+
107
+ # Type 1 CharString operators
108
+ TYPE1_SEAC_ESCAPE = 6 # seac operator is escape byte 12 + 6
109
+
98
110
  # TrueType hinting tables
99
111
  # Font Program table (TrueType bytecode executed once at font load)
100
112
  FPGM_TAG = "fpgm"