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
@@ -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,
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tables/glyf/glyph_builder"
4
+
5
+ module Fontisan
6
+ module Converters
7
+ # Builds glyf and loca tables from glyph outlines
8
+ #
9
+ # This module handles the construction of TrueType glyph tables:
10
+ # - glyf table: Contains actual glyph outline data
11
+ # - loca table: Contains offsets to glyph data in glyf table
12
+ #
13
+ # The loca table format depends on the maximum offset:
14
+ # - Short format (offsets/2) if max offset <= 0x1FFFE
15
+ # - Long format (raw offsets) if max offset > 0x1FFFE
16
+ module GlyfTableBuilder
17
+ # Build glyf and loca tables from outlines
18
+ #
19
+ # @param outlines [Array<Outline>] Glyph outlines
20
+ # @return [Array<String, String, Integer>] [glyf_data, loca_data, loca_format]
21
+ def build_glyf_loca_tables(outlines)
22
+ glyf_data = "".b
23
+ offsets = []
24
+
25
+ # Build each glyph
26
+ outlines.each do |outline|
27
+ offsets << glyf_data.bytesize
28
+
29
+ if outline.empty?
30
+ # Empty glyph - no data
31
+ next
32
+ end
33
+
34
+ # Build glyph data using GlyphBuilder class method
35
+ glyph_data = Fontisan::Tables::GlyphBuilder.build_simple_glyph(outline)
36
+ glyf_data << glyph_data
37
+
38
+ # Add padding to 4-byte boundary
39
+ padding = (4 - (glyf_data.bytesize % 4)) % 4
40
+ glyf_data << ("\x00" * padding) if padding.positive?
41
+ end
42
+
43
+ # Add final offset
44
+ offsets << glyf_data.bytesize
45
+
46
+ # Build loca table
47
+ # Determine format based on max offset
48
+ max_offset = offsets.max
49
+ if max_offset <= 0x1FFFE
50
+ # Short format (offsets / 2)
51
+ loca_format = 0
52
+ loca_data = offsets.map { |off| off / 2 }.pack("n*")
53
+ else
54
+ # Long format
55
+ loca_format = 1
56
+ loca_data = offsets.pack("N*")
57
+ end
58
+
59
+ [glyf_data, loca_data, loca_format]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,17 +1,14 @@
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"
6
7
  require_relative "../tables/cff/charstring_builder"
7
- require_relative "../tables/cff/index_builder"
8
- require_relative "../tables/cff/dict_builder"
9
- require_relative "../tables/glyf/glyph_builder"
10
- require_relative "../tables/glyf/compound_glyph_resolver"
11
- require_relative "../optimizers/pattern_analyzer"
12
- require_relative "../optimizers/subroutine_optimizer"
13
- require_relative "../optimizers/subroutine_builder"
14
- require_relative "../optimizers/charstring_rewriter"
8
+ require_relative "outline_extraction"
9
+ require_relative "cff_table_builder"
10
+ require_relative "glyf_table_builder"
11
+ require_relative "outline_optimizer"
15
12
  require_relative "../hints/truetype_hint_extractor"
16
13
  require_relative "../hints/postscript_hint_extractor"
17
14
  require_relative "../hints/hint_converter"
@@ -58,15 +55,20 @@ module Fontisan
58
55
  # converter = Fontisan::Converters::OutlineConverter.new
59
56
  # otf_font = converter.convert(ttf_font, target_format: :otf)
60
57
  #
61
- # @example Converting OTF to TTF
58
+ # @example Converting with ConversionOptions
59
+ # options = ConversionOptions.recommended(from: :ttf, to: :otf)
62
60
  # converter = Fontisan::Converters::OutlineConverter.new
63
- # ttf_font = converter.convert(otf_font, target_format: :ttf)
61
+ # otf_font = converter.convert(ttf_font, options: options)
64
62
  #
65
63
  # @example Converting with hint preservation
66
64
  # converter = Fontisan::Converters::OutlineConverter.new
67
65
  # otf_font = converter.convert(ttf_font, target_format: :otf, preserve_hints: true)
68
66
  class OutlineConverter
69
67
  include ConversionStrategy
68
+ include OutlineExtraction
69
+ include CffTableBuilder
70
+ include GlyfTableBuilder
71
+ include OutlineOptimizer
70
72
 
71
73
  # Supported outline formats
72
74
  SUPPORTED_FORMATS = %i[ttf otf cff2].freeze
@@ -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
@@ -138,8 +152,30 @@ module Fontisan
138
152
  # Extract hints if preservation is enabled
139
153
  hints_per_glyph = @preserve_hints ? extract_ttf_hints(font) : {}
140
154
 
141
- # Build CFF table from outlines and hints
142
- cff_data = build_cff_table(outlines, font, hints_per_glyph)
155
+ # Build CharStrings from outlines
156
+ charstrings = outlines.map do |outline|
157
+ builder = Tables::Cff::CharStringBuilder.new
158
+ if outline.empty?
159
+ builder.build_empty
160
+ else
161
+ builder.build(outline)
162
+ end
163
+ end
164
+
165
+ # Apply subroutine optimization if enabled
166
+ local_subrs = []
167
+ if @optimize_cff
168
+ begin
169
+ charstrings, local_subrs = optimize_charstrings(charstrings)
170
+ rescue StandardError => e
171
+ # If optimization fails, fall back to unoptimized CharStrings
172
+ warn "CFF optimization failed: #{e.message}, using unoptimized CharStrings"
173
+ local_subrs = []
174
+ end
175
+ end
176
+
177
+ # Build CFF table from charstrings and local subrs
178
+ cff_data = build_cff_table(charstrings, local_subrs, font)
143
179
 
144
180
  # Copy all tables except glyf/loca
145
181
  tables = copy_tables(font, %w[glyf loca])
@@ -184,8 +220,7 @@ module Fontisan
184
220
  hints_per_glyph = @preserve_hints ? extract_cff_hints(font) : {}
185
221
 
186
222
  # Build glyf and loca tables
187
- glyf_data, loca_data, loca_format = build_glyf_loca_tables(outlines,
188
- hints_per_glyph)
223
+ glyf_data, loca_data, loca_format = build_glyf_loca_tables(outlines)
189
224
 
190
225
  # Copy all tables except CFF
191
226
  tables = copy_tables(font, ["CFF ", "CFF2"])
@@ -279,285 +314,6 @@ module Fontisan
279
314
  true
280
315
  end
281
316
 
282
- # Extract outlines from TrueType font
283
- #
284
- # @param font [TrueTypeFont] Source font
285
- # @return [Array<Outline>] Array of outline objects
286
- def extract_ttf_outlines(font)
287
- # Get required tables
288
- head = font.table("head")
289
- maxp = font.table("maxp")
290
- loca = font.table("loca")
291
- glyf = font.table("glyf")
292
-
293
- # Parse loca with context
294
- loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
295
-
296
- # Create resolver for compound glyphs
297
- resolver = Tables::CompoundGlyphResolver.new(glyf, loca, head)
298
-
299
- # Extract all glyphs
300
- outlines = []
301
- maxp.num_glyphs.times do |glyph_id|
302
- glyph = glyf.glyph_for(glyph_id, loca, head)
303
-
304
- outlines << if glyph.nil? || glyph.empty?
305
- # Empty glyph - create empty outline
306
- Models::Outline.new(
307
- glyph_id: glyph_id,
308
- commands: [],
309
- bbox: { x_min: 0, y_min: 0, x_max: 0, y_max: 0 },
310
- )
311
- elsif glyph.simple?
312
- # Convert simple glyph to outline
313
- Models::Outline.from_truetype(glyph, glyph_id)
314
- else
315
- # Compound glyph - resolve to simple outline
316
- resolver.resolve(glyph)
317
- end
318
- end
319
-
320
- outlines
321
- end
322
-
323
- # Extract outlines from CFF font
324
- #
325
- # @param font [OpenTypeFont] Source font
326
- # @return [Array<Outline>] Array of outline objects
327
- def extract_cff_outlines(font)
328
- # Get CFF table
329
- cff = font.table("CFF ")
330
- raise Fontisan::Error, "CFF table not found" unless cff
331
-
332
- # Get number of glyphs
333
- num_glyphs = cff.glyph_count
334
-
335
- # Extract all glyphs
336
- outlines = []
337
- num_glyphs.times do |glyph_id|
338
- charstring = cff.charstring_for_glyph(glyph_id)
339
-
340
- outlines << if charstring.nil? || charstring.path.empty?
341
- # Empty glyph
342
- Models::Outline.new(
343
- glyph_id: glyph_id,
344
- commands: [],
345
- bbox: { x_min: 0, y_min: 0, x_max: 0, y_max: 0 },
346
- )
347
- else
348
- # Convert CharString to outline
349
- Models::Outline.from_cff(charstring, glyph_id)
350
- end
351
- end
352
-
353
- outlines
354
- end
355
-
356
- # Build CFF table from outlines
357
- #
358
- # @param outlines [Array<Outline>] Glyph outlines
359
- # @param font [TrueTypeFont] Source font (for metadata)
360
- # @return [String] CFF table binary data
361
- def build_cff_table(outlines, font, _hints_per_glyph)
362
- # Build CharStrings INDEX from outlines
363
- begin
364
- charstrings = outlines.map do |outline|
365
- builder = Tables::Cff::CharStringBuilder.new
366
- if outline.empty?
367
- builder.build_empty
368
- else
369
- builder.build(outline)
370
- end
371
- end
372
- rescue StandardError => e
373
- raise Fontisan::Error, "Failed to build CharStrings: #{e.message}"
374
- end
375
-
376
- # Apply subroutine optimization if enabled
377
- local_subrs = []
378
-
379
- if @optimize_cff
380
- begin
381
- charstrings, local_subrs = optimize_charstrings(charstrings)
382
- rescue StandardError => e
383
- # If optimization fails, fall back to unoptimized CharStrings
384
- warn "CFF optimization failed: #{e.message}, using unoptimized CharStrings"
385
- local_subrs = []
386
- end
387
- end
388
-
389
- # Build font metadata
390
- begin
391
- font_name = extract_font_name(font)
392
- rescue StandardError => e
393
- raise Fontisan::Error, "Failed to extract font name: #{e.message}"
394
- end
395
-
396
- # Build all INDEXes
397
- begin
398
- header_size = 4
399
- name_index_data = Tables::Cff::IndexBuilder.build([font_name])
400
- string_index_data = Tables::Cff::IndexBuilder.build([]) # Empty strings
401
- global_subr_index_data = Tables::Cff::IndexBuilder.build([]) # Empty global subrs
402
- charstrings_index_data = Tables::Cff::IndexBuilder.build(charstrings)
403
- local_subrs_index_data = Tables::Cff::IndexBuilder.build(local_subrs)
404
- rescue StandardError => e
405
- raise Fontisan::Error, "Failed to build CFF indexes: #{e.message}"
406
- end
407
-
408
- # Build Private DICT with Subrs offset if we have local subroutines
409
- begin
410
- private_dict_hash = {
411
- default_width_x: 1000,
412
- nominal_width_x: 0,
413
- }
414
-
415
- # If we have local subroutines, add Subrs offset
416
- # Subrs offset is relative to Private DICT start
417
- if local_subrs.any?
418
- # Add a placeholder Subrs offset first to get accurate size
419
- private_dict_hash[:subrs] = 0
420
-
421
- # Calculate size of Private DICT with Subrs entry
422
- temp_private_dict_data = Tables::Cff::DictBuilder.build(private_dict_hash)
423
- subrs_offset = temp_private_dict_data.bytesize
424
-
425
- # Update with actual Subrs offset
426
- private_dict_hash[:subrs] = subrs_offset
427
- end
428
-
429
- # Build final Private DICT
430
- private_dict_data = Tables::Cff::DictBuilder.build(private_dict_hash)
431
- private_dict_size = private_dict_data.bytesize
432
- rescue StandardError => e
433
- raise Fontisan::Error, "Failed to build Private DICT: #{e.message}"
434
- end
435
-
436
- # Calculate offsets with iterative refinement
437
- begin
438
- # Initial pass
439
- top_dict_index_start = header_size + name_index_data.bytesize
440
- string_index_start = top_dict_index_start + 100 # Approximate
441
- global_subr_index_start = string_index_start + string_index_data.bytesize
442
- charstrings_offset = global_subr_index_start + global_subr_index_data.bytesize
443
-
444
- # Build Top DICT
445
- top_dict_hash = {
446
- charset: 0,
447
- encoding: 0,
448
- charstrings: charstrings_offset,
449
- }
450
- top_dict_data = Tables::Cff::DictBuilder.build(top_dict_hash)
451
- top_dict_index_data = Tables::Cff::IndexBuilder.build([top_dict_data])
452
-
453
- # Recalculate with actual Top DICT size
454
- string_index_start = top_dict_index_start + top_dict_index_data.bytesize
455
- global_subr_index_start = string_index_start + string_index_data.bytesize
456
- charstrings_offset = global_subr_index_start + global_subr_index_data.bytesize
457
- private_dict_offset = charstrings_offset + charstrings_index_data.bytesize
458
-
459
- # Update Top DICT with Private DICT info
460
- top_dict_hash = {
461
- charset: 0,
462
- encoding: 0,
463
- charstrings: charstrings_offset,
464
- private: [private_dict_size, private_dict_offset],
465
- }
466
- top_dict_data = Tables::Cff::DictBuilder.build(top_dict_hash)
467
- top_dict_index_data = Tables::Cff::IndexBuilder.build([top_dict_data])
468
-
469
- # Final recalculation
470
- string_index_start = top_dict_index_start + top_dict_index_data.bytesize
471
- global_subr_index_start = string_index_start + string_index_data.bytesize
472
- charstrings_offset = global_subr_index_start + global_subr_index_data.bytesize
473
- private_dict_offset = charstrings_offset + charstrings_index_data.bytesize
474
-
475
- # Final Top DICT
476
- top_dict_hash = {
477
- charset: 0,
478
- encoding: 0,
479
- charstrings: charstrings_offset,
480
- private: [private_dict_size, private_dict_offset],
481
- }
482
- top_dict_data = Tables::Cff::DictBuilder.build(top_dict_hash)
483
- top_dict_index_data = Tables::Cff::IndexBuilder.build([top_dict_data])
484
- rescue StandardError => e
485
- raise Fontisan::Error,
486
- "Failed to calculate CFF table offsets: #{e.message}"
487
- end
488
-
489
- # Build CFF Header
490
- begin
491
- header = [
492
- 1, # major version
493
- 0, # minor version
494
- 4, # header size
495
- 4, # offSize (will be in INDEX)
496
- ].pack("C4")
497
- rescue StandardError => e
498
- raise Fontisan::Error, "Failed to build CFF header: #{e.message}"
499
- end
500
-
501
- # Assemble complete CFF table
502
- begin
503
- header +
504
- name_index_data +
505
- top_dict_index_data +
506
- string_index_data +
507
- global_subr_index_data +
508
- charstrings_index_data +
509
- private_dict_data +
510
- local_subrs_index_data
511
- rescue StandardError => e
512
- raise Fontisan::Error, "Failed to assemble CFF table: #{e.message}"
513
- end
514
- end
515
-
516
- # Build glyf and loca tables from outlines
517
- #
518
- # @param outlines [Array<Outline>] Glyph outlines
519
- # @return [Array<String, String, Integer>] [glyf_data, loca_data, loca_format]
520
- def build_glyf_loca_tables(outlines, _hints_per_glyph)
521
- glyf_data = "".b
522
- offsets = []
523
-
524
- # Build each glyph
525
- outlines.each do |outline|
526
- offsets << glyf_data.bytesize
527
-
528
- if outline.empty?
529
- # Empty glyph - no data
530
- next
531
- end
532
-
533
- # Build glyph data using GlyphBuilder class method
534
- glyph_data = Fontisan::Tables::GlyphBuilder.build_simple_glyph(outline)
535
- glyf_data << glyph_data
536
-
537
- # Add padding to 4-byte boundary
538
- padding = (4 - (glyf_data.bytesize % 4)) % 4
539
- glyf_data << ("\x00" * padding) if padding.positive?
540
- end
541
-
542
- # Add final offset
543
- offsets << glyf_data.bytesize
544
-
545
- # Build loca table
546
- # Determine format based on max offset
547
- max_offset = offsets.max
548
- if max_offset <= 0x1FFFE
549
- # Short format (offsets / 2)
550
- loca_format = 0
551
- loca_data = offsets.map { |off| off / 2 }.pack("n*")
552
- else
553
- # Long format
554
- loca_format = 1
555
- loca_data = offsets.pack("N*")
556
- end
557
-
558
- [glyf_data, loca_data, loca_format]
559
- end
560
-
561
317
  # Copy non-outline tables from source to target
562
318
  #
563
319
  # @param font [TrueTypeFont, OpenTypeFont] Source font
@@ -664,85 +420,6 @@ module Fontisan
664
420
  head_data
665
421
  end
666
422
 
667
- # Extract font name from name table
668
- #
669
- # @param font [TrueTypeFont, OpenTypeFont] Font
670
- # @return [String] Font name
671
- def extract_font_name(font)
672
- name_table = font.table("name")
673
- if name_table
674
- font_name = name_table.english_name(Tables::Name::FAMILY)
675
- return font_name.dup.force_encoding("ASCII-8BIT") if font_name
676
- end
677
-
678
- "UnnamedFont"
679
- end
680
-
681
- # Optimize CharStrings using subroutine extraction
682
- #
683
- # @param charstrings [Array<String>] Original CharString bytes
684
- # @return [Array<Array<String>, Array<String>>] [optimized_charstrings, local_subrs]
685
- def optimize_charstrings(charstrings)
686
- # Convert to hash format expected by PatternAnalyzer
687
- charstrings_hash = {}
688
- charstrings.each_with_index do |cs, index|
689
- charstrings_hash[index] = cs
690
- end
691
-
692
- # Analyze patterns
693
- analyzer = Optimizers::PatternAnalyzer.new(
694
- min_length: 10,
695
- stack_aware: true,
696
- )
697
- patterns = analyzer.analyze(charstrings_hash)
698
-
699
- # Return original if no patterns found
700
- return [charstrings, []] if patterns.empty?
701
-
702
- # Optimize selection
703
- optimizer = Optimizers::SubroutineOptimizer.new(patterns,
704
- max_subrs: 65_535)
705
- selected_patterns = optimizer.optimize_selection
706
-
707
- # Optimize ordering
708
- selected_patterns = optimizer.optimize_ordering(selected_patterns)
709
-
710
- # Return original if no patterns selected
711
- return [charstrings, []] if selected_patterns.empty?
712
-
713
- # Build subroutines
714
- builder = Optimizers::SubroutineBuilder.new(selected_patterns,
715
- type: :local)
716
- local_subrs = builder.build
717
-
718
- # Build subroutine map
719
- subroutine_map = {}
720
- selected_patterns.each_with_index do |pattern, index|
721
- subroutine_map[pattern.bytes] = index
722
- end
723
-
724
- # Rewrite CharStrings
725
- rewriter = Optimizers::CharstringRewriter.new(subroutine_map, builder)
726
- optimized_charstrings = charstrings.map.with_index do |charstring, glyph_id|
727
- # Find patterns for this glyph
728
- glyph_patterns = selected_patterns.select do |p|
729
- p.glyphs.include?(glyph_id)
730
- end
731
-
732
- if glyph_patterns.empty?
733
- charstring
734
- else
735
- rewriter.rewrite(charstring, glyph_patterns, glyph_id)
736
- end
737
- end
738
-
739
- [optimized_charstrings, local_subrs]
740
- rescue StandardError => e
741
- # If optimization fails for any reason, return original CharStrings
742
- warn "Optimization warning: #{e.message}"
743
- [charstrings, []]
744
- end
745
-
746
423
  # Generate static instance from variable font
747
424
  #
748
425
  # @param font [TrueTypeFont, OpenTypeFont] Variable font
@@ -1001,6 +678,66 @@ module Fontisan
1001
678
  def variable_font?(font)
1002
679
  font.has_table?("fvar")
1003
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
1004
741
  end
1005
742
  end
1006
743
  end