fontisan 0.2.11 → 0.2.13

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 +294 -52
  3. data/Gemfile +5 -0
  4. data/README.adoc +163 -2
  5. data/docs/CONVERSION_GUIDE.adoc +633 -0
  6. data/docs/TYPE1_FONTS.adoc +445 -0
  7. data/lib/fontisan/cli.rb +177 -6
  8. data/lib/fontisan/commands/convert_command.rb +32 -1
  9. data/lib/fontisan/commands/info_command.rb +83 -2
  10. data/lib/fontisan/config/conversion_matrix.yml +132 -4
  11. data/lib/fontisan/constants.rb +12 -0
  12. data/lib/fontisan/conversion_options.rb +378 -0
  13. data/lib/fontisan/converters/collection_converter.rb +45 -10
  14. data/lib/fontisan/converters/format_converter.rb +17 -5
  15. data/lib/fontisan/converters/outline_converter.rb +78 -4
  16. data/lib/fontisan/converters/type1_converter.rb +1234 -0
  17. data/lib/fontisan/font_loader.rb +46 -3
  18. data/lib/fontisan/hints/hint_converter.rb +4 -1
  19. data/lib/fontisan/type1/afm_generator.rb +436 -0
  20. data/lib/fontisan/type1/afm_parser.rb +298 -0
  21. data/lib/fontisan/type1/agl.rb +456 -0
  22. data/lib/fontisan/type1/cff_to_type1_converter.rb +302 -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 +576 -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 +369 -0
  32. data/lib/fontisan/type1/pfa_parser.rb +159 -0
  33. data/lib/fontisan/type1/pfb_generator.rb +314 -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 +342 -0
  38. data/lib/fontisan/type1/seac_expander.rb +501 -0
  39. data/lib/fontisan/type1/ttf_to_type1_converter.rb +327 -0
  40. data/lib/fontisan/type1/upm_scaler.rb +118 -0
  41. data/lib/fontisan/type1.rb +75 -0
  42. data/lib/fontisan/type1_font.rb +318 -0
  43. data/lib/fontisan/version.rb +1 -1
  44. data/lib/fontisan.rb +2 -0
  45. metadata +30 -3
  46. data/docs/DOCUMENTATION_SUMMARY.md +0 -141
@@ -0,0 +1,445 @@
1
+ = Type 1 Font Support
2
+
3
+ Fontisan provides comprehensive support for Adobe Type 1 fonts (PFB/PFA), including reading, converting, and generating Type 1 format fonts.
4
+
5
+ == Implementation Status
6
+
7
+ *Complete* features:
8
+ * Reading PFB (binary) and PFA (ASCII) format files
9
+ * eexec decryption for encrypted font portions (key: 55665)
10
+ * CharString decryption with lenIV support (key: 4330)
11
+ * Font dictionary and private dictionary parsing
12
+ * CharString parsing with `seac` composite expansion
13
+ * Type 1 → OTF conversion with CFF CharString generation
14
+ * Type 1 → TTF conversion (via OTF intermediate)
15
+ * OTF → Type 1 conversion with CFF → Type 1 CharString conversion
16
+ * TTF → Type 1 conversion (via OTF intermediate)
17
+ * Adobe Glyph List (AGL) integration for Unicode mapping
18
+ * SFNT table generation (head, hhea, maxp, name, OS/2, post, cmap)
19
+ * PFM, AFM, INF file generation for Type 1 fonts
20
+
21
+ *Preview/Planned* features:
22
+ * Multiple master Type 1 fonts
23
+ * Subroutine optimization in CharStrings
24
+
25
+ == Overview
26
+
27
+ Type 1 is a font format developed by Adobe Systems that uses PostScript outline descriptions. Type 1 fonts were widely used in professional typography and desktop publishing before being largely superseded by OpenType.
28
+
29
+ Fontisan's Type 1 support includes:
30
+
31
+ * Reading PFB and PFA files
32
+ * Converting Type 1 to modern formats (OTF, TTF, WOFF, WOFF2)
33
+ * Converting modern formats back to Type 1
34
+ * Preserving Type 1 metadata and hinting information
35
+ * Generating Unicode mappings from glyph names
36
+
37
+ == Type 1 Font Structure
38
+
39
+ === Encryption
40
+
41
+ Type 1 fonts typically use two levels of encryption to protect font data:
42
+
43
+ * *eexec encryption* - Protects the private dictionary and CharStrings with key 55665
44
+ * *CharString encryption* - Encrypts individual CharStrings with key 4330
45
+
46
+ Fontisan automatically decrypts both levels when loading Type 1 fonts using the Rabin-Miller cipher with byte shuffle:
47
+
48
+ [source,ruby]
49
+ ----
50
+ # Automatic decryption on load
51
+ font = Fontisan::FontLoader.from_file('font.pfb')
52
+ puts font.decrypted? # => true
53
+
54
+ # Access decrypted data
55
+ decrypted = font.decrypted_data
56
+ ----
57
+
58
+ === PFB vs PFA Formats
59
+
60
+ *PFB (Printer Font Binary)*::
61
+ Binary format with segmented structure using chunk markers:
62
+ * `0x8001` - ASCII text chunk
63
+ * `0x8002` - Binary data chunk (encrypted)
64
+ * `0x8003` - End of file marker
65
+
66
+ Each chunk is prefixed with a 4-byte little-endian length. PFB is most common on Windows systems.
67
+
68
+ *PFA (Printer Font ASCII)*::
69
+ Pure ASCII text format with encrypted portions marked by `currentfile eexec`. The encrypted data is represented as hexadecimal strings, followed by 512 ASCII zeros as a separator marker. PFA is most common on Unix/Linux systems.
70
+
71
+ === Type 1 Font Structure
72
+
73
+ A Type 1 font consists of three main parts:
74
+
75
+ . *Font Dictionary* - Contains font-level metadata (font name, version, bounding box, etc.)
76
+ . *Private Dictionary* - Contains hinting parameters (blue values, stem snap, etc.)
77
+ . *CharStrings* - Contains glyph outline descriptions in PostScript format
78
+
79
+ === Font Dictionary
80
+
81
+ The font dictionary contains essential font information:
82
+
83
+ [source,ruby]
84
+ ----
85
+ {
86
+ version: "001.000",
87
+ notice: "Copyright notice",
88
+ copyright: "Copyright string",
89
+ full_name: "Font Full Name",
90
+ family_name: "Font Family",
91
+ weight: "Medium",
92
+ font_bbox: [x_min, y_min, x_max, y_max],
93
+ font_matrix: [0.001, 0, 0, 0.001, 0, 0]
94
+ }
95
+ ----
96
+
97
+ Where,
98
+
99
+ `version`:: Version string in the format "XXX.YYY"
100
+ `notice`:: Copyright and license information
101
+ `copyright`:: Copyright string
102
+ `full_name`:: Full font name including style
103
+ `family_name`:: Font family name
104
+ `weight`:: Font weight (e.g., "Medium", "Bold", "Light")
105
+ `font_bbox`:: Font bounding box as [x_min, y_min, x_max, y_max]
106
+ `font_matrix`:: Transformation matrix for scaling coordinates
107
+
108
+ === Private Dictionary
109
+
110
+ The private dictionary contains hinting parameters:
111
+
112
+ [source,ruby]
113
+ ----
114
+ {
115
+ blue_values: [-10, 0, 470, 480],
116
+ other_blues: [250, 260],
117
+ blue_scale: 0.039625,
118
+ blue_shift: 7,
119
+ blue_fuzz: 1,
120
+ std_hw: 50,
121
+ std_vw: 80,
122
+ stem_snap_h: [50, 52],
123
+ stem_snap_v: [80, 82],
124
+ force_bold: false
125
+ }
126
+ ----
127
+
128
+ Where,
129
+
130
+ `blue_values`:: Alignment zones for baseline and cap heights
131
+ `other_blues`:: Additional alignment zones
132
+ `blue_scale`:: Scaling factor for alignment zones
133
+ `blue_shift`:: Maximum deviation from alignment zone
134
+ `blue_fuzz`:: Fuzz factor for alignment
135
+ `std_hw`:: Standard horizontal stem width
136
+ `std_vw`:: Standard vertical stem width
137
+ `stem_snap_h`:: Array of horizontal stem widths
138
+ `stem_snap_v`:: Array of vertical stem widths
139
+ `force_bold`:: Whether to force bold rendering
140
+
141
+ === CharStrings
142
+
143
+ CharStrings contain glyph outline descriptions using PostScript commands:
144
+
145
+ [source]
146
+ ----
147
+ hsbw 0 0 # Horizontal sidebearings and width
148
+ rmoveto 100 0 # Relative move to
149
+ rlineto 50 0 # Relative line to
150
+ rlineto 0 50 # Relative line to
151
+ rlineto -50 0 # Relative line to (close path)
152
+ rlineto 0 -50 # Relative line to (close path)
153
+ endchar # End of glyph
154
+ ----
155
+
156
+ === Seac Composite Glyphs
157
+
158
+ Type 1 fonts include composite glyphs using the `seac` operator for accented characters. For example, "À" (A grave) might be defined as a base "A" with a combining grave accent.
159
+
160
+ Fontisan provides automatic `seac` expansion:
161
+
162
+ [source,ruby]
163
+ ----
164
+ # Enable seac decomposition
165
+ options = Fontisan::ConversionOptions.new(
166
+ from: :type1,
167
+ to: :otf,
168
+ opening: { decompose_composites: true }
169
+ )
170
+
171
+ converter = Fontisan::Converters::Type1Converter.new
172
+ tables = converter.convert(font, options: options)
173
+ ----
174
+
175
+ The `SeacExpander` resolves composite glyphs by:
176
+ 1. Extracting the base character and accent from the encoding
177
+ 2. Recursively retrieving component CharStrings
178
+ 3. Merging the outlines into a single decomposed glyph
179
+
180
+ This is necessary because CFF fonts (used in OTF) do not support the `seac` operator.
181
+
182
+ == Loading Type 1 Fonts
183
+
184
+ Use `FontLoader.load` to load Type 1 fonts:
185
+
186
+ [source,ruby]
187
+ ----
188
+ require 'fontisan'
189
+
190
+ # Load PFB file
191
+ font = Fontisan::FontLoader.load('font.pfb')
192
+
193
+ # Load PFA file
194
+ font = Fontisan::FontLoader.load('font.pfa')
195
+
196
+ # Access font dictionary
197
+ dict = font.font_dictionary
198
+ puts "Font: #{dict.font_name}"
199
+ puts "Family: #{dict.family_name}"
200
+ ----
201
+
202
+ == Converting Type 1 Fonts
203
+
204
+ === Type 1 to OpenType (OTF)
205
+
206
+ Convert Type 1 fonts to modern OpenType format with CFF outlines:
207
+
208
+ [source,ruby]
209
+ ----
210
+ converter = Fontisan::Converters::Type1Converter.new
211
+ tables = converter.convert(font, target_format: :otf)
212
+
213
+ # Write to file
214
+ Fontisan::FontWriter.write(tables, 'output.otf')
215
+ ----
216
+
217
+ Using the CLI:
218
+
219
+ [source,shell]
220
+ ----
221
+ fontisan convert font.pfb --to otf --output font.otf
222
+ ----
223
+
224
+ === Type 1 to TrueType (TTF)
225
+
226
+ Convert Type 1 fonts to TrueType format with quadratic curves:
227
+
228
+ [source,ruby]
229
+ ----
230
+ converter = Fontisan::Converters::Type1Converter.new
231
+ tables = converter.convert(font, target_format: :ttf)
232
+
233
+ # Write to file
234
+ Fontisan::FontWriter.write(tables, 'output.ttf')
235
+ ----
236
+
237
+ Using the CLI:
238
+
239
+ [source,shell]
240
+ ----
241
+ fontisan convert font.pfb --to ttf --output font.ttf
242
+ ----
243
+
244
+ === Type 1 to Web Fonts
245
+
246
+ Convert Type 1 fonts directly to web font formats:
247
+
248
+ [source,shell]
249
+ ----
250
+ # Convert to WOFF
251
+ fontisan convert font.pfb --to woff --output font.woff
252
+
253
+ # Convert to WOFF2
254
+ fontisan convert font.pfb --to woff2 --output font.woff2
255
+ ----
256
+
257
+ === Conversion with Options
258
+
259
+ Use ConversionOptions for advanced control:
260
+
261
+ [source,ruby]
262
+ ----
263
+ options = Fontisan::ConversionOptions.recommended(from: :type1, to: :otf)
264
+ converter = Fontisan::Converters::Type1Converter.new
265
+ tables = converter.convert(font, options: options)
266
+ ----
267
+
268
+ Using presets:
269
+
270
+ [source,ruby]
271
+ ----
272
+ options = Fontisan::ConversionOptions.from_preset(:type1_to_modern)
273
+ tables = converter.convert(font, options: options)
274
+ ----
275
+
276
+ Using the CLI:
277
+
278
+ [source,shell]
279
+ ----
280
+ fontisan convert font.pfb --to otf --output font.otf --preset type1_to_modern
281
+ ----
282
+
283
+ == Converting to Type 1
284
+
285
+ === OpenType to Type 1
286
+
287
+ Convert OpenType/CFF fonts back to Type 1 format:
288
+
289
+ [source,ruby]
290
+ ----
291
+ converter = Fontisan::Converters::Type1Converter.new
292
+ type1_data = converter.convert(otf_font, target_format: :type1)
293
+
294
+ # Write PFB file
295
+ File.write('output.pfb', type1_data[:pfb])
296
+ ----
297
+
298
+ Using the CLI:
299
+
300
+ [source,shell]
301
+ ----
302
+ fontisan convert font.otf --to type1 --output font.pfb --preset modern_to_type1
303
+ ----
304
+
305
+ === TrueType to Type 1
306
+
307
+ Convert TrueType fonts to Type 1 (via OpenType intermediate):
308
+
309
+ [source,shell]
310
+ ----
311
+ fontisan convert font.ttf --to type1 --output font.pfb
312
+ ----
313
+
314
+ == Conversion Options
315
+
316
+ === Opening Options
317
+
318
+ Options applied when reading Type 1 fonts:
319
+
320
+ `generate_unicode`:: Generate Unicode codepoints from glyph names using the Adobe Glyph List
321
+ `decompose_composites`:: Decompose seac composite glyphs into base glyphs
322
+ `read_all_records`:: Force loading of all font dictionary records
323
+
324
+ === Generating Options
325
+
326
+ Options applied when writing Type 1 fonts:
327
+
328
+ `write_pfm`:: Generate PFM (Printer Font Metrics) file (default: true)
329
+ `write_afm`:: Generate AFM (Adobe Font Metrics) file (default: true)
330
+ `write_inf`:: Generate INF file for installation (default: true)
331
+ `select_encoding_automatically`:: Automatically select encoding (default: true)
332
+ `hinting_mode`:: Hint preservation mode: preserve, auto, or none
333
+ `decompose_on_output`:: Decompose composite glyphs on output
334
+
335
+ === Presets
336
+
337
+ Fontisan includes predefined presets for common Type 1 workflows:
338
+
339
+ * `type1_to_modern` - Optimize Type 1 fonts for modern use (generates Unicode, preserves hints)
340
+ * `modern_to_type1` - Convert modern fonts back to Type 1 format with proper metrics
341
+
342
+ [source,ruby]
343
+ ----
344
+ # Using preset programmatically
345
+ options = Fontisan::ConversionOptions.from_preset(:type1_to_modern)
346
+
347
+ # Using preset via CLI
348
+ fontisan convert font.pfb --to otf --preset type1_to_modern --output font.otf
349
+ ----
350
+
351
+ == Working with Glyphs
352
+
353
+ === Accessing Glyph Outlines
354
+
355
+ [source,ruby]
356
+ ----
357
+ font = Fontisan::FontLoader.load('font.pfb')
358
+
359
+ # Iterate over glyphs
360
+ font.charstrings.each_charstring do |glyph_name, charstring|
361
+ puts "Glyph: #{glyph_name}"
362
+
363
+ # Convert to universal outline format
364
+ outline = charstring.to_outline
365
+
366
+ # Access contour points
367
+ outline.contours.each do |contour|
368
+ contour.points.each do |point|
369
+ puts " Point: #{point.x}, #{point.y} (#{point.on_curve? ? 'on' : 'off'} curve)"
370
+ end
371
+ end
372
+ end
373
+ ----
374
+
375
+ === Glyph Names to Unicode
376
+
377
+ Type 1 fonts use glyph names rather than Unicode codepoints. Use the Adobe Glyph List to map names to codepoints:
378
+
379
+ [source,ruby]
380
+ ----
381
+ # Enable Unicode generation
382
+ options = Fontisan::ConversionOptions.new(
383
+ from: :type1,
384
+ to: :otf,
385
+ opening: { generate_unicode: true }
386
+ )
387
+
388
+ converter = Fontisan::Converters::Type1Converter.new
389
+ tables = converter.convert(font, options: options)
390
+ ----
391
+
392
+ == Limitations
393
+
394
+ Current limitations of Type 1 support:
395
+
396
+ * Subroutines in CharStrings are not fully optimized for space efficiency
397
+ * Multiple master Type 1 fonts are not supported (only single master fonts)
398
+ * Some hinting parameters may not convert perfectly between Type 1 and CFF formats
399
+ * Converting modern fonts back to Type 1 may lose some OpenType features (GSUB/GPOS)
400
+
401
+ == Technical Details
402
+
403
+ === Decryption Algorithm
404
+
405
+ Fontisan uses the Rabin-Miller cipher with byte shuffle for decryption:
406
+
407
+ [source,ruby]
408
+ ----
409
+ # eexec decryption (key: 55665)
410
+ cipher = key
411
+ data.each_byte do |byte|
412
+ cipher = ((cipher << 8) & 0xFFFFFFFF) | byte
413
+ plain = cipher >> 8
414
+ result << plain.chr
415
+ cipher = plain ^ (cipher >> 16)
416
+ end
417
+
418
+ # CharString decryption (key: 4330)
419
+ # Same cipher, but skips lenIV bytes at start
420
+ decrypted = decrypt(charstring_data, 4330)
421
+ charstring = lenIV > 0 ? decrypted[lenIV..-1] : decrypted
422
+ ----
423
+
424
+ === Command Mapping
425
+
426
+ Type 1 CharString commands map directly to CFF commands for conversion:
427
+
428
+ | Type 1 Command | CFF Command | Notes |
429
+ |----------------|-------------|-------|
430
+ | hsbw | hsbw | Identical |
431
+ | sbw | sbw | Identical |
432
+ | endchar | endchar | Identical |
433
+ | hstem/vstem | hstem/vstem | Identical |
434
+ | rmoveto/hmoveto/vmoveto | rmoveto/hmoveto/vmoveto | Identical |
435
+ | rlineto/hlineto/vlineto | rlineto/hlineto/vlineto | Identical |
436
+ | rrcurveto/hhcurveto/vvcurveto/hvhcurveto/vhcurveto | rrcurveto/hhcurveto/vvcurveto/hvhcurveto/vhcurveto | Identical |
437
+ | seac | - | Decomposed (not in CFF) |
438
+ | closepath | endchar | Mapped |
439
+ | callsubr/callgsubr | callsubr/callgsubr | Identical |
440
+
441
+ == See Also
442
+
443
+ * https://www.adobe.com/devnet/font/pdfs/Type1.pdf[Adobe Type 1 Font Format Specification]
444
+ * link:CONVERSION_GUIDE.adoc[Conversion Guide] - Comprehensive conversion options reference
445
+ * link:README.adoc[README] - Main Fontisan documentation
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