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,378 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ # Conversion options for font format transformations
5
+ #
6
+ # Defines all options for opening (reading) and generating (writing) fonts
7
+ # during conversion operations. Includes validation, defaults, and presets.
8
+ #
9
+ # @example Using options directly
10
+ # options = ConversionOptions.new(
11
+ # from: :ttf,
12
+ # to: :otf,
13
+ # opening: { convert_curves: true, scale_to_1000: true },
14
+ # generating: { hinting_mode: "auto" }
15
+ # )
16
+ #
17
+ # @example Using recommended options
18
+ # options = ConversionOptions.recommended(from: :ttf, to: :otf)
19
+ #
20
+ # @example Using a preset
21
+ # options = ConversionOptions.from_preset(:type1_to_modern)
22
+ class ConversionOptions
23
+ # Opening options (input processing)
24
+ OPENING_OPTIONS = %i[
25
+ decompose_composites
26
+ convert_curves
27
+ scale_to_1000
28
+ scale_from_1000
29
+ autohint
30
+ generate_unicode
31
+ store_custom_tables
32
+ store_native_hinting
33
+ interpret_ot
34
+ read_all_records
35
+ preserve_encoding
36
+ ].freeze
37
+
38
+ # Generating options (output processing)
39
+ GENERATING_OPTIONS = %i[
40
+ write_pfm
41
+ write_afm
42
+ write_inf
43
+ select_encoding_automatically
44
+ hinting_mode
45
+ decompose_on_output
46
+ write_custom_tables
47
+ optimize_tables
48
+ reencode_first_256
49
+ encoding_vector
50
+ compression
51
+ transform_tables
52
+ preserve_metadata
53
+ strip_metadata
54
+ target_format
55
+ ].freeze
56
+
57
+ # Valid hinting modes
58
+ HINTING_MODES = %w[preserve auto none full].freeze
59
+
60
+ # Valid compression modes
61
+ COMPRESSION_MODES = %w[zlib brotli none].freeze
62
+
63
+ attr_reader :from, :to, :opening, :generating
64
+
65
+ # Initialize conversion options
66
+ #
67
+ # @param from [String, Symbol] Source format
68
+ # @param to [String, Symbol] Target format
69
+ # @param opening [Hash] Opening options
70
+ # @param generating [Hash] Generating options
71
+ def initialize(from:, to:, opening: {}, generating: {})
72
+ @from = self.class.normalize_format(from)
73
+ @to = self.class.normalize_format(to)
74
+ @opening = apply_opening_defaults(opening)
75
+ @generating = apply_generating_defaults(generating)
76
+ validate!
77
+ end
78
+
79
+ # Get recommended options for a conversion pair
80
+ #
81
+ # @param from [String, Symbol] Source format
82
+ # @param to [String, Symbol] Target format
83
+ # @return [ConversionOptions] Pre-configured options
84
+ def self.recommended(from:, to:)
85
+ from_sym = normalize_format(from)
86
+ to_sym = normalize_format(to)
87
+
88
+ options = RECOMMENDED_OPTIONS.dig(from_sym, to_sym) || {}
89
+ new(
90
+ from: from_sym,
91
+ to: to_sym,
92
+ opening: options[:opening] || {},
93
+ generating: options[:generating] || {},
94
+ )
95
+ end
96
+
97
+ # Load options from a named preset
98
+ #
99
+ # @param preset_name [Symbol, String] Preset name
100
+ # @return [ConversionOptions] Pre-configured options
101
+ def self.from_preset(preset_name)
102
+ preset_key = preset_name.to_sym
103
+ preset = PRESETS.fetch(preset_key) do
104
+ raise ArgumentError, "Unknown preset: #{preset_name}. " \
105
+ "Available: #{PRESETS.keys.join(', ')}"
106
+ end
107
+
108
+ new(
109
+ from: preset[:from],
110
+ to: preset[:to],
111
+ opening: preset[:opening] || {},
112
+ generating: preset[:generating] || {},
113
+ )
114
+ end
115
+
116
+ # Check if an opening option is set
117
+ #
118
+ # @param key [Symbol] Option key
119
+ # @return [Boolean] true if option is truthy
120
+ def opening_option?(key)
121
+ !!@opening[key]
122
+ end
123
+
124
+ # Check if a generating option has a specific value
125
+ #
126
+ # @param key [Symbol] Option key
127
+ # @param value [Object] Value to check
128
+ # @return [Boolean] true if option equals value
129
+ def generating_option?(key, value = true)
130
+ @generating[key] == value
131
+ end
132
+
133
+ # Get list of available presets
134
+ #
135
+ # @return [Array<Symbol>] Available preset names
136
+ def self.available_presets
137
+ PRESETS.keys
138
+ end
139
+
140
+ private
141
+
142
+ # Normalize format symbol
143
+ #
144
+ # @param format [String, Symbol] Format identifier
145
+ # @return [Symbol] Normalized format symbol
146
+ def self.normalize_format(format)
147
+ case format.to_s.downcase
148
+ when "ttf", "truetype", "truetype-tt", "ot-tt" then :ttf
149
+ when "otf", "cff", "opentype", "ot-ps", "opentype-ps" then :otf
150
+ when "type1", "type-1", "t1", "pfb", "pfa" then :type1
151
+ when "ttc" then :ttc
152
+ when "otc" then :otc
153
+ when "dfont", "suitcase" then :dfont
154
+ when "woff" then :woff
155
+ when "woff2" then :woff2
156
+ when "svg" then :svg
157
+ else
158
+ raise ArgumentError, "Unknown format: #{format}. " \
159
+ "Supported: ttf, otf, type1, ttc, otc, dfont, woff, woff2, svg"
160
+ end
161
+ end
162
+
163
+ # Apply default values for opening options
164
+ #
165
+ # @param options [Hash] User-provided options
166
+ # @return [Hash] Options with defaults applied
167
+ def apply_opening_defaults(options)
168
+ defaults = OPENING_DEFAULTS.dig(@from, @to) || {}
169
+ defaults.merge(options)
170
+ end
171
+
172
+ # Apply default values for generating options
173
+ #
174
+ # @param options [Hash] User-provided options
175
+ # @return [Hash] Options with defaults applied
176
+ def apply_generating_defaults(options)
177
+ defaults = GENERATING_DEFAULTS.dig(@from, @to) || {}
178
+ defaults.merge(options)
179
+ end
180
+
181
+ # Validate options
182
+ #
183
+ # @raise [ArgumentError] If options are invalid
184
+ def validate!
185
+ validate_opening_options!
186
+ validate_generating_options!
187
+ end
188
+
189
+ # Validate opening options
190
+ def validate_opening_options!
191
+ @opening.each_key do |key|
192
+ unless OPENING_OPTIONS.include?(key)
193
+ raise ArgumentError, "Unknown opening option: #{key}. " \
194
+ "Available: #{OPENING_OPTIONS.join(', ')}"
195
+ end
196
+ end
197
+ end
198
+
199
+ # Validate generating options
200
+ def validate_generating_options!
201
+ @generating.each_key do |key|
202
+ unless GENERATING_OPTIONS.include?(key)
203
+ raise ArgumentError, "Unknown generating option: #{key}. " \
204
+ "Available: #{GENERATING_OPTIONS.join(', ')}"
205
+ end
206
+ end
207
+
208
+ # Validate hinting_mode
209
+ if @generating[:hinting_mode]
210
+ mode = @generating[:hinting_mode].to_s
211
+ unless HINTING_MODES.include?(mode)
212
+ raise ArgumentError, "Invalid hinting_mode: #{mode}. " \
213
+ "Available: #{HINTING_MODES.join(', ')}"
214
+ end
215
+ end
216
+
217
+ # Validate compression mode
218
+ if @generating[:compression]
219
+ comp = @generating[:compression].to_s
220
+ unless COMPRESSION_MODES.include?(comp)
221
+ raise ArgumentError, "Invalid compression: #{comp}. " \
222
+ "Available: #{COMPRESSION_MODES.join(', ')}"
223
+ end
224
+ end
225
+
226
+ # Validate target_format for collection conversions
227
+ if @generating[:target_format]
228
+ target = @generating[:target_format].to_s
229
+ unless ["ttf", "otf", "preserve"].include?(target)
230
+ raise ArgumentError, "Invalid target_format: #{target}"
231
+ end
232
+ end
233
+ end
234
+
235
+ # Default opening options per conversion pair
236
+ OPENING_DEFAULTS = {
237
+ ttf: {
238
+ ttf: { decompose_composites: false, convert_curves: false,
239
+ store_custom_tables: true },
240
+ otf: { convert_curves: true, scale_to_1000: true,
241
+ autohint: true, decompose_composites: false },
242
+ type1: { convert_curves: true, scale_to_1000: true,
243
+ autohint: true, decompose_composites: false },
244
+ },
245
+ otf: {
246
+ ttf: { convert_curves: true, decompose_composites: false,
247
+ interpret_ot: true },
248
+ otf: { decompose_composites: false, store_custom_tables: true },
249
+ type1: { decompose_composites: false },
250
+ },
251
+ type1: {
252
+ ttf: { decompose_composites: false, generate_unicode: true,
253
+ read_all_records: true },
254
+ otf: { decompose_composites: false, generate_unicode: true,
255
+ read_all_records: true },
256
+ },
257
+ }.freeze
258
+
259
+ # Default generating options per conversion pair
260
+ GENERATING_DEFAULTS = {
261
+ ttf: {
262
+ ttf: { hinting_mode: "preserve", write_custom_tables: true,
263
+ optimize_tables: true },
264
+ otf: { hinting_mode: "auto", decompose_on_output: false,
265
+ write_custom_tables: true },
266
+ type1: { write_pfm: true, write_afm: true, write_inf: true,
267
+ select_encoding_automatically: true, hinting_mode: "auto" },
268
+ },
269
+ otf: {
270
+ ttf: { hinting_mode: "auto", decompose_on_output: false,
271
+ write_custom_tables: true },
272
+ otf: { hinting_mode: "preserve", decompose_on_output: false,
273
+ write_custom_tables: true, optimize_tables: true },
274
+ type1: { write_pfm: true, write_afm: true, write_inf: true,
275
+ select_encoding_automatically: true, hinting_mode: "preserve",
276
+ decompose_on_output: false },
277
+ },
278
+ type1: {
279
+ ttf: { hinting_mode: "auto", decompose_on_output: true },
280
+ otf: { hinting_mode: "preserve", decompose_on_output: true },
281
+ type1: { write_pfm: true, write_afm: true, write_inf: true,
282
+ select_encoding_automatically: true },
283
+ },
284
+ }.freeze
285
+
286
+ # Recommended options from TypeTool 3 manual
287
+ # Based on "Options for Converting Fonts" table (lines 6735-6803)
288
+ RECOMMENDED_OPTIONS = {
289
+ ttf: {
290
+ ttf: {
291
+ opening: { convert_curves: false, scale_to_1000: false,
292
+ decompose_composites: false, autohint: false,
293
+ store_custom_tables: true, store_native_hinting: true },
294
+ generating: { hinting_mode: "full", write_custom_tables: true },
295
+ },
296
+ otf: {
297
+ opening: { convert_curves: true, scale_to_1000: true,
298
+ autohint: true, decompose_composites: false,
299
+ store_custom_tables: true },
300
+ generating: { hinting_mode: "auto", decompose_on_output: true },
301
+ },
302
+ type1: {
303
+ opening: { convert_curves: true, scale_to_1000: true,
304
+ autohint: true, decompose_composites: false,
305
+ store_custom_tables: false },
306
+ generating: { write_pfm: true, write_afm: true, write_inf: true,
307
+ select_encoding_automatically: true },
308
+ },
309
+ },
310
+ otf: {
311
+ ttf: {
312
+ opening: { decompose_composites: false, read_all_records: true,
313
+ interpret_ot: true, store_custom_tables: true,
314
+ store_native_hinting: false },
315
+ generating: { hinting_mode: "full", reencode_first_256: false },
316
+ },
317
+ otf: {
318
+ opening: { decompose_composites: false, store_custom_tables: true },
319
+ generating: { hinting_mode: "none", decompose_on_output: false,
320
+ write_custom_tables: true },
321
+ },
322
+ type1: {
323
+ opening: { decompose_composites: false },
324
+ generating: { write_pfm: true, write_afm: true, write_inf: true,
325
+ select_encoding_automatically: true,
326
+ hinting_mode: "none" },
327
+ },
328
+ },
329
+ type1: {
330
+ ttf: {
331
+ opening: { decompose_composites: false, generate_unicode: true },
332
+ generating: { hinting_mode: "full" },
333
+ },
334
+ otf: {
335
+ opening: { decompose_composites: false, generate_unicode: true },
336
+ generating: { hinting_mode: "none", decompose_on_output: true },
337
+ },
338
+ type1: {
339
+ opening: { decompose_composites: false, generate_unicode: true },
340
+ generating: { write_pfm: true, write_afm: true, write_inf: true,
341
+ select_encoding_automatically: true },
342
+ },
343
+ },
344
+ }.freeze
345
+
346
+ # Named presets for common conversion scenarios
347
+ PRESETS = {
348
+ type1_to_modern: {
349
+ from: :type1,
350
+ to: :otf,
351
+ opening: { generate_unicode: true, decompose_composites: false },
352
+ generating: { hinting_mode: "preserve", decompose_on_output: true },
353
+ },
354
+ modern_to_type1: {
355
+ from: :otf,
356
+ to: :type1,
357
+ opening: { convert_curves: true, scale_to_1000: true,
358
+ autohint: true, decompose_composites: false,
359
+ store_custom_tables: false },
360
+ generating: { write_pfm: true, write_afm: true, write_inf: true,
361
+ select_encoding_automatically: true, hinting_mode: "preserve" },
362
+ },
363
+ web_optimized: {
364
+ from: :otf,
365
+ to: :woff2,
366
+ opening: {},
367
+ generating: { compression: "brotli", transform_tables: true,
368
+ optimize_tables: true, preserve_metadata: true },
369
+ },
370
+ archive_to_modern: {
371
+ from: :ttc,
372
+ to: :otf,
373
+ opening: { convert_curves: true, decompose_composites: false },
374
+ generating: { target_format: "otf", hinting_mode: "preserve" },
375
+ },
376
+ }.freeze
377
+ end
378
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../conversion_options"
3
4
  require_relative "format_converter"
4
5
  require_relative "../collection/builder"
5
6
  require_relative "../collection/dfont_builder"
@@ -27,14 +28,15 @@ module Fontisan
27
28
  # converter = CollectionConverter.new
28
29
  # result = converter.convert(ttc_path, target_type: :otc, output: 'family.otc')
29
30
  #
30
- # @example Convert TTC to OTC with outline conversion
31
+ # @example Convert with ConversionOptions
32
+ # options = ConversionOptions.recommended(from: :ttc, to: :otc)
31
33
  # converter = CollectionConverter.new
32
- # result = converter.convert(ttc_path, target_type: :otc,
33
- # options: { output: 'family.otc', convert_outlines: true })
34
+ # result = converter.convert(ttc_path, target_type: :otc, options: { output: 'family.otc', options: options })
34
35
  #
35
- # @example Convert dfont to TTC
36
+ # @example Convert TTC to OTC with outline conversion
36
37
  # converter = CollectionConverter.new
37
- # result = converter.convert(dfont_path, target_type: :ttc, output: 'family.ttc')
38
+ # result = converter.convert(ttc_path, target_type: :otc,
39
+ # options: { output: 'family.otc', target_format: 'otf' })
38
40
  class CollectionConverter
39
41
  # Convert collection to target format
40
42
  #
@@ -45,6 +47,7 @@ module Fontisan
45
47
  # @option options [String] :target_format Target outline format: 'preserve' (default), 'ttf', or 'otf'
46
48
  # @option options [Boolean] :optimize Enable table sharing (default: true, TTC/OTC only)
47
49
  # @option options [Boolean] :verbose Enable verbose output (default: false)
50
+ # @option options [ConversionOptions] :options ConversionOptions object
48
51
  # @return [Hash] Conversion result with:
49
52
  # - :input [String] - Input collection path
50
53
  # - :output [String] - Output collection path
@@ -57,9 +60,19 @@ module Fontisan
57
60
  def convert(collection_path, target_type:, options: {})
58
61
  validate_parameters!(collection_path, target_type, options)
59
62
 
63
+ # Extract ConversionOptions if provided
64
+ conv_options = extract_conversion_options(options)
65
+
60
66
  verbose = options.fetch(:verbose, false)
61
67
  output_path = options[:output]
62
- target_format = options.fetch(:target_format, "preserve").to_s
68
+
69
+ # Determine target format from ConversionOptions or options hash
70
+ target_format = if conv_options&.generating_option?(:target_format,
71
+ "otf")
72
+ conv_options.generating[:target_format]
73
+ else
74
+ options.fetch(:target_format, "preserve").to_s
75
+ end
63
76
 
64
77
  # Validate target_format
65
78
  unless %w[preserve ttf otf].include?(target_format)
@@ -84,7 +97,8 @@ module Fontisan
84
97
  # Step 2: Convert - transform fonts if requested
85
98
  puts " Converting #{fonts.size} font(s)..." if verbose
86
99
  converted_fonts, conversions = convert_fonts(fonts, source_type,
87
- target_type, options.merge(target_format: target_format))
100
+ target_type, options.merge(target_format: target_format),
101
+ conv_options)
88
102
 
89
103
  # Step 3: Repack - build target collection
90
104
  puts " Repacking into #{target_type.to_s.upcase} format..." if verbose
@@ -206,13 +220,20 @@ module Fontisan
206
220
  # @param source_type [Symbol] Source collection type
207
221
  # @param target_type [Symbol] Target collection type
208
222
  # @param options [Hash] Conversion options
223
+ # @param conv_options [ConversionOptions, nil] Conversion options object
209
224
  # @return [Array<(Array<Font>, Array<Hash>)>] [converted_fonts, conversions]
210
- def convert_fonts(fonts, _source_type, target_type, options)
225
+ def convert_fonts(fonts, _source_type, target_type, options,
226
+ conv_options = nil)
211
227
  converted_fonts = []
212
228
  conversions = []
213
229
 
214
230
  # Determine if outline conversion is needed
215
- target_format = options.fetch(:target_format, "preserve").to_s
231
+ target_format = if conv_options&.generating_option?(:target_format,
232
+ "otf")
233
+ conv_options.generating[:target_format]
234
+ else
235
+ options.fetch(:target_format, "preserve").to_s
236
+ end
216
237
 
217
238
  fonts.each_with_index do |font, index|
218
239
  source_format = detect_font_format(font)
@@ -224,8 +245,12 @@ module Fontisan
224
245
  desired_format = target_format == "preserve" ? source_format : target_format.to_sym
225
246
  converter = FormatConverter.new
226
247
 
248
+ # Build converter options, including ConversionOptions if provided
249
+ converter_opts = options.dup
250
+ converter_opts[:options] = conv_options if conv_options
251
+
227
252
  begin
228
- tables = converter.convert(font, desired_format, options)
253
+ tables = converter.convert(font, desired_format, converter_opts)
229
254
  converted_font = build_font_from_tables(tables, desired_format)
230
255
  converted_fonts << converted_font
231
256
 
@@ -442,6 +467,16 @@ conversions)
442
467
 
443
468
  puts ""
444
469
  end
470
+
471
+ # Extract ConversionOptions from options hash
472
+ #
473
+ # @param options [Hash, ConversionOptions] Options or hash containing :options key
474
+ # @return [ConversionOptions, nil] Extracted ConversionOptions or nil
475
+ def extract_conversion_options(options)
476
+ return options if options.is_a?(ConversionOptions)
477
+
478
+ options[:options] if options.is_a?(Hash)
479
+ end
445
480
  end
446
481
  end
447
482
  end
@@ -6,6 +6,7 @@ require_relative "outline_converter"
6
6
  require_relative "woff_writer"
7
7
  require_relative "woff2_encoder"
8
8
  require_relative "svg_generator"
9
+ require_relative "type1_converter"
9
10
  require "yaml"
10
11
 
11
12
  module Fontisan
@@ -54,6 +55,7 @@ module Fontisan
54
55
  @strategies = [
55
56
  TableCopier.new,
56
57
  OutlineConverter.new,
58
+ Type1Converter.new,
57
59
  WoffWriter.new,
58
60
  Woff2Encoder.new,
59
61
  SvgGenerator.new,
@@ -191,9 +193,12 @@ module Fontisan
191
193
 
192
194
  # Check if font is a variable font
193
195
  #
194
- # @param font [TrueTypeFont, OpenTypeFont] Font to check
196
+ # @param font [TrueTypeFont, OpenTypeFont, Type1Font] Font to check
195
197
  # @return [Boolean] True if font has fvar table
196
198
  def variable_font?(font)
199
+ # Type 1 fonts are never variable fonts
200
+ return false if font.is_a?(Type1Font)
201
+
197
202
  font.has_table?("fvar")
198
203
  end
199
204
 
@@ -341,8 +346,12 @@ _options)
341
346
  def validate_parameters!(font, target_format)
342
347
  raise ArgumentError, "Font cannot be nil" if font.nil?
343
348
 
344
- unless font.respond_to?(:table)
345
- raise ArgumentError, "Font must respond to :table method"
349
+ # Type1Font uses a different interface (font_dictionary, charstrings, etc.)
350
+ # rather than the SFNT table interface
351
+ is_type1 = font.is_a?(Type1Font)
352
+
353
+ unless is_type1 || font.respond_to?(:table)
354
+ raise ArgumentError, "Font must respond to :table method or be a Type1Font"
346
355
  end
347
356
 
348
357
  unless target_format.is_a?(Symbol)
@@ -392,10 +401,13 @@ _options)
392
401
 
393
402
  # Detect font format from tables
394
403
  #
395
- # @param font [TrueTypeFont, OpenTypeFont] Font to detect
396
- # @return [Symbol] Format (:ttf or :otf)
404
+ # @param font [TrueTypeFont, OpenTypeFont, Type1Font] Font to detect
405
+ # @return [Symbol] Format (:ttf, :otf, or :type1)
397
406
  # @raise [Error] If format cannot be detected
398
407
  def detect_format(font)
408
+ # Check for Type1Font first (uses different interface)
409
+ return :type1 if font.is_a?(Type1Font)
410
+
399
411
  # Check for CFF/CFF2 tables (OpenType/CFF)
400
412
  if font.has_table?("CFF ") || font.has_table?("CFF2")
401
413
  :otf
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../conversion_options"
3
4
  require_relative "conversion_strategy"
4
5
  require_relative "../outline_extractor"
5
6
  require_relative "../models/outline"
@@ -54,9 +55,10 @@ module Fontisan
54
55
  # converter = Fontisan::Converters::OutlineConverter.new
55
56
  # otf_font = converter.convert(ttf_font, target_format: :otf)
56
57
  #
57
- # @example Converting OTF to TTF
58
+ # @example Converting with ConversionOptions
59
+ # options = ConversionOptions.recommended(from: :ttf, to: :otf)
58
60
  # converter = Fontisan::Converters::OutlineConverter.new
59
- # ttf_font = converter.convert(otf_font, target_format: :ttf)
61
+ # otf_font = converter.convert(ttf_font, options: options)
60
62
  #
61
63
  # @example Converting with hint preservation
62
64
  # converter = Fontisan::Converters::OutlineConverter.new
@@ -89,19 +91,31 @@ module Fontisan
89
91
  # @option options [Boolean] :preserve_variations Keep variation data during conversion (default: true)
90
92
  # @option options [Boolean] :generate_instance Generate static instance instead of variable font (default: false)
91
93
  # @option options [Hash] :instance_coordinates Axis coordinates for instance generation (default: {})
94
+ # @option options [ConversionOptions] :options ConversionOptions object
92
95
  # @return [Hash<String, String>] Map of table tags to binary data
93
96
  def convert(font, options = {})
94
97
  @font = font
95
98
  @options = options
99
+
100
+ # Extract ConversionOptions if provided
101
+ conv_options = extract_conversion_options(options)
102
+
96
103
  @optimize_cff = options.fetch(:optimize_cff, false)
97
- @preserve_hints = options.fetch(:preserve_hints, false)
104
+ @preserve_hints = options.fetch(:preserve_hints,
105
+ conv_options&.generating_option?(
106
+ :hinting_mode, "preserve"
107
+ ) || false)
98
108
  @preserve_variations = options.fetch(:preserve_variations, true)
99
109
  @generate_instance = options.fetch(:generate_instance, false)
100
110
  @instance_coordinates = options.fetch(:instance_coordinates, {})
101
- target_format = options[:target_format] ||
111
+
112
+ target_format = options[:target_format] || conv_options&.to ||
102
113
  detect_target_format(font)
103
114
  validate(font, target_format)
104
115
 
116
+ # Apply opening options to source font
117
+ apply_opening_options(font, conv_options) if conv_options
118
+
105
119
  source_format = detect_format(font)
106
120
 
107
121
  # Check if we should generate a static instance instead
@@ -664,6 +678,66 @@ module Fontisan
664
678
  def variable_font?(font)
665
679
  font.has_table?("fvar")
666
680
  end
681
+
682
+ # Extract ConversionOptions from options hash
683
+ #
684
+ # @param options [Hash, ConversionOptions] Options or hash containing :options key
685
+ # @return [ConversionOptions, nil] Extracted ConversionOptions or nil
686
+ def extract_conversion_options(options)
687
+ return options if options.is_a?(ConversionOptions)
688
+
689
+ options[:options] if options.is_a?(Hash)
690
+ end
691
+
692
+ # Apply opening options to source font
693
+ #
694
+ # @param font [TrueTypeFont, OpenTypeFont] Source font
695
+ # @param conv_options [ConversionOptions] Conversion options with opening options
696
+ def apply_opening_options(font, conv_options)
697
+ return unless conv_options
698
+
699
+ # Decompose composites if requested
700
+ if conv_options.opening_option?(:decompose_composites)
701
+ decompose_composite_glyphs(font)
702
+ end
703
+
704
+ # Interpret OpenType tables if requested
705
+ if conv_options.opening_option?(:interpret_ot)
706
+ # Ensure GSUB/GPOS tables are fully loaded
707
+ interpret_opentype_features(font)
708
+ end
709
+
710
+ # Note: scale_to_1000 and convert_curves are handled during conversion
711
+ # These options affect the conversion process itself
712
+ end
713
+
714
+ # Decompose composite glyphs in font
715
+ #
716
+ # @param font [TrueTypeFont, OpenTypeFont] Source font
717
+ def decompose_composite_glyphs(_font)
718
+ # Placeholder: Decompose composite glyphs
719
+ # A full implementation would:
720
+ # 1. Identify composite glyphs (TrueType: flags & 0x0020, CFF: seac operator)
721
+ # 2. Extract component outlines
722
+ # 3. Merge into single glyph
723
+ #
724
+ # For now, this is a no-op placeholder
725
+ nil
726
+ end
727
+
728
+ # Interpret OpenType layout features
729
+ #
730
+ # @param font [TrueTypeFont, OpenTypeFont] Source font
731
+ def interpret_opentype_features(_font)
732
+ # Placeholder: Interpret GSUB/GPOS tables
733
+ # A full implementation would:
734
+ # 1. Parse GSUB table for substitution rules
735
+ # 2. Parse GPOS table for positioning rules
736
+ # 3. Store interpreted features for later use
737
+ #
738
+ # For now, this is a no-op placeholder
739
+ nil
740
+ end
667
741
  end
668
742
  end
669
743
  end