fontisan 0.2.22 → 0.3.0

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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.rubocop_todo.yml +93 -17
  4. data/CHANGELOG.md +12 -2
  5. data/README.adoc +6 -210
  6. data/fontisan.gemspec +48 -0
  7. data/lib/fontisan/cldr/unicode_set_parser.rb +23 -6
  8. data/lib/fontisan/cldr/version_resolver.rb +1 -1
  9. data/lib/fontisan/cli.rb +4 -168
  10. data/lib/fontisan/commands.rb +0 -3
  11. data/lib/fontisan/formatters/text_formatter.rb +0 -6
  12. data/lib/fontisan/formatters.rb +0 -3
  13. data/lib/fontisan/hints.rb +6 -3
  14. data/lib/fontisan/models.rb +4 -4
  15. data/lib/fontisan/pipeline/strategies.rb +4 -2
  16. data/lib/fontisan/pipeline.rb +2 -1
  17. data/lib/fontisan/stitcher/selector/codepoints.rb +29 -0
  18. data/lib/fontisan/stitcher/selector/gid.rb +25 -0
  19. data/lib/fontisan/stitcher/selector/range.rb +30 -0
  20. data/lib/fontisan/stitcher/selector.rb +26 -0
  21. data/lib/fontisan/stitcher/source.rb +97 -0
  22. data/lib/fontisan/stitcher.rb +182 -0
  23. data/lib/fontisan/stitcher_cli.rb +69 -0
  24. data/lib/fontisan/tables/cff.rb +2 -1
  25. data/lib/fontisan/tables.rb +2 -1
  26. data/lib/fontisan/ufo/anchor.rb +17 -0
  27. data/lib/fontisan/ufo/cli.rb +85 -0
  28. data/lib/fontisan/ufo/compile/base_compiler.rb +81 -0
  29. data/lib/fontisan/ufo/compile/cff.rb +224 -0
  30. data/lib/fontisan/ufo/compile/cmap.rb +129 -0
  31. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +174 -0
  32. data/lib/fontisan/ufo/compile/filters/decompose_components.rb +33 -0
  33. data/lib/fontisan/ufo/compile/filters/flatten_components.rb +22 -0
  34. data/lib/fontisan/ufo/compile/filters/reverse_contour_direction.rb +27 -0
  35. data/lib/fontisan/ufo/compile/filters.rb +57 -0
  36. data/lib/fontisan/ufo/compile/glyf_loca.rb +145 -0
  37. data/lib/fontisan/ufo/compile/head.rb +98 -0
  38. data/lib/fontisan/ufo/compile/hhea.rb +36 -0
  39. data/lib/fontisan/ufo/compile/hmtx.rb +27 -0
  40. data/lib/fontisan/ufo/compile/maxp.rb +57 -0
  41. data/lib/fontisan/ufo/compile/name.rb +79 -0
  42. data/lib/fontisan/ufo/compile/os2.rb +81 -0
  43. data/lib/fontisan/ufo/compile/otf_compiler.rb +43 -0
  44. data/lib/fontisan/ufo/compile/post.rb +32 -0
  45. data/lib/fontisan/ufo/compile/ttf_compiler.rb +69 -0
  46. data/lib/fontisan/ufo/compile.rb +48 -0
  47. data/lib/fontisan/ufo/component.rb +18 -0
  48. data/lib/fontisan/ufo/contour.rb +29 -0
  49. data/lib/fontisan/ufo/convert/from_bin_data.rb +246 -0
  50. data/lib/fontisan/ufo/convert.rb +18 -0
  51. data/lib/fontisan/ufo/data_set.rb +21 -0
  52. data/lib/fontisan/ufo/features.rb +17 -0
  53. data/lib/fontisan/ufo/font.rb +61 -0
  54. data/lib/fontisan/ufo/glyph.rb +421 -0
  55. data/lib/fontisan/ufo/guideline.rb +19 -0
  56. data/lib/fontisan/ufo/image.rb +16 -0
  57. data/lib/fontisan/ufo/image_set.rb +19 -0
  58. data/lib/fontisan/ufo/info.rb +79 -0
  59. data/lib/fontisan/ufo/kerning.rb +32 -0
  60. data/lib/fontisan/ufo/layer.rb +38 -0
  61. data/lib/fontisan/ufo/layer_set.rb +37 -0
  62. data/lib/fontisan/ufo/lib.rb +24 -0
  63. data/lib/fontisan/ufo/plist.rb +118 -0
  64. data/lib/fontisan/ufo/point.rb +38 -0
  65. data/lib/fontisan/ufo/reader.rb +144 -0
  66. data/lib/fontisan/ufo/transformation.rb +39 -0
  67. data/lib/fontisan/ufo/writer.rb +115 -0
  68. data/lib/fontisan/ufo.rb +44 -0
  69. data/lib/fontisan/version.rb +1 -1
  70. data/lib/fontisan.rb +3 -3
  71. metadata +57 -70
  72. data/lib/fontisan/audit/codepoint_range_coalescer.rb +0 -41
  73. data/lib/fontisan/audit/context.rb +0 -122
  74. data/lib/fontisan/audit/differ.rb +0 -124
  75. data/lib/fontisan/audit/extractors/aggregations.rb +0 -54
  76. data/lib/fontisan/audit/extractors/base.rb +0 -26
  77. data/lib/fontisan/audit/extractors/color_capabilities.rb +0 -141
  78. data/lib/fontisan/audit/extractors/coverage.rb +0 -48
  79. data/lib/fontisan/audit/extractors/hinting.rb +0 -197
  80. data/lib/fontisan/audit/extractors/identity.rb +0 -52
  81. data/lib/fontisan/audit/extractors/language_coverage.rb +0 -37
  82. data/lib/fontisan/audit/extractors/licensing.rb +0 -79
  83. data/lib/fontisan/audit/extractors/metrics.rb +0 -103
  84. data/lib/fontisan/audit/extractors/opentype_layout.rb +0 -69
  85. data/lib/fontisan/audit/extractors/provenance.rb +0 -29
  86. data/lib/fontisan/audit/extractors/style.rb +0 -32
  87. data/lib/fontisan/audit/extractors/variation_detail.rb +0 -99
  88. data/lib/fontisan/audit/extractors.rb +0 -27
  89. data/lib/fontisan/audit/library_aggregator.rb +0 -83
  90. data/lib/fontisan/audit/library_auditor.rb +0 -90
  91. data/lib/fontisan/audit/registry.rb +0 -60
  92. data/lib/fontisan/audit/style_extractor.rb +0 -80
  93. data/lib/fontisan/audit.rb +0 -20
  94. data/lib/fontisan/cli/ucd_cli.rb +0 -97
  95. data/lib/fontisan/commands/audit_command.rb +0 -123
  96. data/lib/fontisan/commands/audit_compare_command.rb +0 -66
  97. data/lib/fontisan/commands/audit_library_command.rb +0 -46
  98. data/lib/fontisan/config/ucd.yml +0 -23
  99. data/lib/fontisan/formatters/audit_diff_text_renderer.rb +0 -122
  100. data/lib/fontisan/formatters/audit_text_renderer.rb +0 -324
  101. data/lib/fontisan/formatters/library_summary_text_renderer.rb +0 -99
  102. data/lib/fontisan/models/audit/audit_axis.rb +0 -30
  103. data/lib/fontisan/models/audit/audit_block.rb +0 -32
  104. data/lib/fontisan/models/audit/audit_diff.rb +0 -77
  105. data/lib/fontisan/models/audit/audit_report.rb +0 -153
  106. data/lib/fontisan/models/audit/codepoint_range.rb +0 -40
  107. data/lib/fontisan/models/audit/codepoint_set_diff.rb +0 -34
  108. data/lib/fontisan/models/audit/color_capabilities.rb +0 -93
  109. data/lib/fontisan/models/audit/duplicate_group.rb +0 -23
  110. data/lib/fontisan/models/audit/embedding_type.rb +0 -76
  111. data/lib/fontisan/models/audit/field_change.rb +0 -28
  112. data/lib/fontisan/models/audit/fs_selection_flags.rb +0 -61
  113. data/lib/fontisan/models/audit/gasp_range.rb +0 -63
  114. data/lib/fontisan/models/audit/hinting.rb +0 -93
  115. data/lib/fontisan/models/audit/library_summary.rb +0 -40
  116. data/lib/fontisan/models/audit/licensing.rb +0 -48
  117. data/lib/fontisan/models/audit/metrics.rb +0 -111
  118. data/lib/fontisan/models/audit/named_instance.rb +0 -41
  119. data/lib/fontisan/models/audit/opentype_layout.rb +0 -40
  120. data/lib/fontisan/models/audit/script_coverage_row.rb +0 -26
  121. data/lib/fontisan/models/audit/script_features.rb +0 -28
  122. data/lib/fontisan/models/audit/variation_detail.rb +0 -44
  123. data/lib/fontisan/models/audit.rb +0 -33
  124. data/lib/fontisan/models/ucd/ucd.rb +0 -38
  125. data/lib/fontisan/models/ucd/ucd_char.rb +0 -67
  126. data/lib/fontisan/models/ucd.rb +0 -19
  127. data/lib/fontisan/ucd/aggregator.rb +0 -73
  128. data/lib/fontisan/ucd/cache_manager.rb +0 -111
  129. data/lib/fontisan/ucd/config.rb +0 -59
  130. data/lib/fontisan/ucd/download_error.rb +0 -9
  131. data/lib/fontisan/ucd/downloader.rb +0 -88
  132. data/lib/fontisan/ucd/error.rb +0 -8
  133. data/lib/fontisan/ucd/index.rb +0 -103
  134. data/lib/fontisan/ucd/index_builder.rb +0 -107
  135. data/lib/fontisan/ucd/range_entry.rb +0 -56
  136. data/lib/fontisan/ucd/unknown_version_error.rb +0 -9
  137. data/lib/fontisan/ucd/version_resolver.rb +0 -79
  138. data/lib/fontisan/ucd.rb +0 -23
data/lib/fontisan/cli.rb CHANGED
@@ -25,92 +25,14 @@ module Fontisan
25
25
  desc: "Suppress non-error output",
26
26
  aliases: "-q"
27
27
 
28
- desc "ucd", "Manage local UCD cache (subcommands)", hide: true
29
- subcommand "ucd", UcdCli
30
-
31
28
  desc "cldr", "Manage local CLDR cache (subcommands)", hide: true
32
29
  subcommand "cldr", CldrCli
33
30
 
34
- desc "audit PATH", "Produce a per-face font audit report, or diff/summarize"
35
- long_desc <<~DESC
36
- Produce a complete per-face font audit report covering identity, style,
37
- metrics, coverage (Unicode blocks/scripts), licensing, hinting, color
38
- capabilities, variable font detail, and OpenType layout features.
39
-
40
- For TTC/OTC/dfont collections, one report per face is produced. Use
41
- --output to write reports to disk; --font-index to audit a single face.
42
-
43
- Variants:
44
- fontisan audit FONT.ttf
45
- fontisan audit COLLECTION.ttc
46
- fontisan audit DIR/ --recursive --summary
47
- fontisan audit --compare A.ttf B.ttf
48
- fontisan audit --compare A.yaml B.yaml
49
-
50
- Output formats: text (default), yaml, json.
51
-
52
- Use --brief for a fast inventory pass that skips metrics, hinting,
53
- color, variable-font detail, OpenType layout, and UCD/CLDR
54
- aggregation — only identity, style, licensing, and codepoint coverage.
55
- DESC
56
- option :font_index, type: :numeric,
57
- desc: "Audit only this face in a collection (default: all)"
58
- option :all_codepoints, type: :boolean, default: false,
59
- desc: "Include the full per-codepoint list " \
60
- "(defaults to compact range view)"
61
- option :ucd_version, type: :string,
62
- desc: "UCD version to aggregate against " \
63
- "(default: configured default; 'latest' to probe)"
64
- option :with_language_coverage, type: :boolean, default: false,
65
- desc: "Compute coverage % per CLDR language " \
66
- "(requires CLDR cache; auto-downloads)"
67
- option :cldr_version, type: :string,
68
- desc: "CLDR version (default: configured default; " \
69
- "'latest' to probe)"
70
- option :brief, type: :boolean, default: false,
71
- desc: "Skip metrics/hinting/color/layout/UCD/CLDR for a " \
72
- "fast inventory pass"
73
- option :compare, type: :boolean, default: false,
74
- desc: "Diff two fonts or two saved reports " \
75
- "(requires exactly two PATHs)"
76
- option :recursive, type: :boolean, default: false,
77
- desc: "Audit every font under a directory tree " \
78
- "(library mode)"
79
- option :summary, type: :boolean, default: false,
80
- desc: "Produce a LibrarySummary over a directory of fonts"
81
- option :output, type: :string,
82
- desc: "Output directory (collections/library) or file " \
83
- "(single font / compare)",
84
- aliases: "-o"
85
- # Produce a complete font audit report, or diff two fonts/reports,
86
- # or summarize a whole library.
87
- #
88
- # @param paths [Array<String>] one path (audit/library), or two paths (--compare)
89
- def audit(*paths)
90
- raise Thor::Error, "audit requires one PATH (or two with --compare)" if paths.empty?
91
-
92
- if options[:compare]
93
- unless paths.length == 2
94
- raise Thor::Error,
95
- "audit --compare requires exactly two paths"
96
- end
97
-
98
- return run_compare(paths[0], paths[1])
99
- end
100
-
101
- raise Thor::Error, "audit requires exactly one PATH" unless paths.length == 1
31
+ desc "ufo", "UFO source operations (build, convert, validate)"
32
+ subcommand "ufo", Fontisan::Ufo::Cli
102
33
 
103
- path = paths[0]
104
- if Dir.exist?(path) && !library_mode?(path)
105
- raise Thor::Error,
106
- "audit on a directory requires --recursive or --summary"
107
- end
108
- return run_library_audit(path) if library_mode?(path)
109
-
110
- run_single_audit(path)
111
- rescue Errno::ENOENT, Error, Thor::Error => e
112
- handle_error(e)
113
- end
34
+ desc "stitch", "Stitch glyphs from multiple source fonts into one output"
35
+ subcommand "stitch", StitcherCli
114
36
 
115
37
  desc "info PATH", "Display font information"
116
38
  option :brief, type: :boolean, default: false,
@@ -788,92 +710,6 @@ module Fontisan
788
710
  puts output unless options[:quiet]
789
711
  end
790
712
 
791
- # Write audit reports to disk. If `target` is a directory (or there are
792
- # multiple reports), one file per face is written under it. If `target`
793
- # is a file path and there's exactly one report, that exact path is used.
794
- #
795
- # @param reports [Array<Models::Audit::AuditReport>]
796
- # @param target [String] directory (collection) or file path (single)
797
- # @param format [String] "yaml" or "json"
798
- # @return [void]
799
- def write_audit_outputs(reports, target, format)
800
- sym_format = format.to_sym
801
-
802
- if reports.one? && !Dir.exist?(target) && File.extname(target) != ""
803
- File.write(target, serialize_report(reports.first, sym_format))
804
- puts "Wrote #{target}" unless options[:quiet]
805
- return
806
- end
807
-
808
- paths = Commands::AuditCommand.write_reports(reports, to: target,
809
- format: sym_format)
810
- paths.each { |p| puts "Wrote #{p}" unless options[:quiet] }
811
- end
812
-
813
- def run_single_audit(path)
814
- cmd_options = options.dup
815
- cmd_options.delete(:output)
816
- # Audit's --brief selects a cheap extractor subset but still needs FULL
817
- # font loading (Coverage reads cmap). Translate to :audit_brief so
818
- # BaseCommand's :brief → METADATA shortcut does not fire.
819
- cmd_options[:audit_brief] = cmd_options.delete(:brief) if cmd_options.key?(:brief)
820
- command = Commands::AuditCommand.new(path, cmd_options)
821
- reports = Array(command.run)
822
-
823
- if options[:output]
824
- write_audit_outputs(reports, options[:output], options[:format])
825
- else
826
- reports.each { |r| output_result(r) }
827
- end
828
- end
829
-
830
- def run_compare(left_path, right_path)
831
- cmd = Commands::AuditCompareCommand.new(left_path, right_path, options)
832
- diff = cmd.run
833
- return if options[:quiet]
834
-
835
- if options[:output]
836
- File.write(options[:output], serialize_report(diff, options[:format].to_sym))
837
- puts "Wrote #{options[:output]}"
838
- return
839
- end
840
-
841
- output_result(diff)
842
- end
843
-
844
- def run_library_audit(path)
845
- cmd = Commands::AuditLibraryCommand.new(
846
- path,
847
- recursive: options[:recursive],
848
- options: options.dup,
849
- )
850
- summary = cmd.run
851
- announce_skipped(cmd.skipped)
852
- return if options[:quiet]
853
-
854
- if options[:output]
855
- File.write(options[:output],
856
- serialize_report(summary, options[:format].to_sym))
857
- puts "Wrote #{options[:output]}"
858
- return
859
- end
860
-
861
- output_result(summary)
862
- end
863
-
864
- # Library mode triggers when the path is a directory and either
865
- # --recursive or --summary is requested. A single-file audit ignores
866
- # both flags (a TTC is audited face-by-face via run_single_audit).
867
- def library_mode?(path)
868
- Dir.exist?(path) && (options[:recursive] || options[:summary])
869
- end
870
-
871
- def announce_skipped(skipped)
872
- return if skipped.empty?
873
-
874
- skipped.each { |p| warn "skipped #{p}" }
875
- end
876
-
877
713
  def serialize_report(report, format)
878
714
  format == :json ? report.to_json : report.to_yaml
879
715
  end
@@ -4,9 +4,6 @@
4
4
 
5
5
  module Fontisan
6
6
  module Commands
7
- autoload :AuditCompareCommand, "fontisan/commands/audit_compare_command"
8
- autoload :AuditCommand, "fontisan/commands/audit_command"
9
- autoload :AuditLibraryCommand, "fontisan/commands/audit_library_command"
10
7
  autoload :BaseCommand, "fontisan/commands/base_command"
11
8
  autoload :ConvertCommand, "fontisan/commands/convert_command"
12
9
  autoload :DumpTableCommand, "fontisan/commands/dump_table_command"
@@ -44,12 +44,6 @@ module Fontisan
44
44
  format_collection_info(model)
45
45
  when Models::CollectionBriefInfo
46
46
  format_collection_brief_info(model)
47
- when Models::Audit::AuditReport
48
- Formatters::AuditTextRenderer.new(model).render
49
- when Models::Audit::AuditDiff
50
- Formatters::AuditDiffTextRenderer.new(model).render
51
- when Models::Audit::LibrarySummary
52
- Formatters::LibrarySummaryTextRenderer.new(model).render
53
47
  else
54
48
  model.to_s
55
49
  end
@@ -4,9 +4,6 @@
4
4
 
5
5
  module Fontisan
6
6
  module Formatters
7
- autoload :AuditDiffTextRenderer, "fontisan/formatters/audit_diff_text_renderer"
8
- autoload :AuditTextRenderer, "fontisan/formatters/audit_text_renderer"
9
- autoload :LibrarySummaryTextRenderer, "fontisan/formatters/library_summary_text_renderer"
10
7
  autoload :TextFormatter, "fontisan/formatters/text_formatter"
11
8
  end
12
9
  end
@@ -7,10 +7,13 @@ module Fontisan
7
7
  autoload :HintConverter, "fontisan/hints/hint_converter"
8
8
  autoload :HintValidator, "fontisan/hints/hint_validator"
9
9
  autoload :PostScriptHintApplier, "fontisan/hints/postscript_hint_applier"
10
- autoload :PostScriptHintExtractor, "fontisan/hints/postscript_hint_extractor"
10
+ autoload :PostScriptHintExtractor,
11
+ "fontisan/hints/postscript_hint_extractor"
11
12
  autoload :TrueTypeHintApplier, "fontisan/hints/truetype_hint_applier"
12
13
  autoload :TrueTypeHintExtractor, "fontisan/hints/truetype_hint_extractor"
13
- autoload :TrueTypeInstructionAnalyzer, "fontisan/hints/truetype_instruction_analyzer"
14
- autoload :TrueTypeInstructionGenerator, "fontisan/hints/truetype_instruction_generator"
14
+ autoload :TrueTypeInstructionAnalyzer,
15
+ "fontisan/hints/truetype_instruction_analyzer"
16
+ autoload :TrueTypeInstructionGenerator,
17
+ "fontisan/hints/truetype_instruction_generator"
15
18
  end
16
19
  end
@@ -4,8 +4,8 @@
4
4
 
5
5
  module Fontisan
6
6
  module Models
7
- autoload :AllScriptsFeaturesInfo, "fontisan/models/all_scripts_features_info"
8
- autoload :Audit, "fontisan/models/audit"
7
+ autoload :AllScriptsFeaturesInfo,
8
+ "fontisan/models/all_scripts_features_info"
9
9
  autoload :AxisInfo, "fontisan/models/variable_font_info"
10
10
  autoload :BitmapGlyph, "fontisan/models/bitmap_glyph"
11
11
  autoload :BitmapStrike, "fontisan/models/bitmap_strike"
@@ -13,7 +13,8 @@ module Fontisan
13
13
  autoload :CollectionFontSummary, "fontisan/models/collection_font_summary"
14
14
  autoload :CollectionInfo, "fontisan/models/collection_info"
15
15
  autoload :CollectionListInfo, "fontisan/models/collection_list_info"
16
- autoload :CollectionValidationReport, "fontisan/models/collection_validation_report"
16
+ autoload :CollectionValidationReport,
17
+ "fontisan/models/collection_validation_report"
17
18
  autoload :Cldr, "fontisan/models/cldr"
18
19
  autoload :ColorGlyph, "fontisan/models/color_glyph"
19
20
  autoload :ColorLayer, "fontisan/models/color_layer"
@@ -38,7 +39,6 @@ module Fontisan
38
39
  autoload :TableInfo, "fontisan/models/table_info"
39
40
  autoload :TableSharingInfo, "fontisan/models/table_sharing_info"
40
41
  autoload :Ttx, "fontisan/models/ttx/ttfont"
41
- autoload :Ucd, "fontisan/models/ucd"
42
42
  autoload :UnicodeMapping, "fontisan/models/unicode_mappings"
43
43
  autoload :UnicodeMappings, "fontisan/models/unicode_mappings"
44
44
  autoload :ValidationReport, "fontisan/models/validation_report"
@@ -6,9 +6,11 @@ module Fontisan
6
6
  module Pipeline
7
7
  module Strategies
8
8
  autoload :BaseStrategy, "fontisan/pipeline/strategies/base_strategy"
9
- autoload :InstanceStrategy, "fontisan/pipeline/strategies/instance_strategy"
9
+ autoload :InstanceStrategy,
10
+ "fontisan/pipeline/strategies/instance_strategy"
10
11
  autoload :NamedStrategy, "fontisan/pipeline/strategies/named_strategy"
11
- autoload :PreserveStrategy, "fontisan/pipeline/strategies/preserve_strategy"
12
+ autoload :PreserveStrategy,
13
+ "fontisan/pipeline/strategies/preserve_strategy"
12
14
  end
13
15
  end
14
16
  end
@@ -7,7 +7,8 @@ module Fontisan
7
7
  autoload :FormatDetector, "fontisan/pipeline/format_detector"
8
8
  autoload :OutputWriter, "fontisan/pipeline/output_writer"
9
9
  autoload :Strategies, "fontisan/pipeline/strategies/base_strategy"
10
- autoload :TransformationPipeline, "fontisan/pipeline/transformation_pipeline"
10
+ autoload :TransformationPipeline,
11
+ "fontisan/pipeline/transformation_pipeline"
11
12
  autoload :VariationResolver, "fontisan/pipeline/variation_resolver"
12
13
  end
13
14
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ module Selector
6
+ # Include an explicit list of codepoints.
7
+ class Codepoints
8
+ attr_reader :codepoints
9
+
10
+ def initialize(codepoints)
11
+ @codepoints = codepoints
12
+ end
13
+
14
+ def apply(source, bindings)
15
+ @codepoints.each do |cp|
16
+ gid = source.gid_for_codepoint(cp)
17
+ next unless gid
18
+
19
+ bindings << {
20
+ codepoint: cp,
21
+ source: source,
22
+ donor_gid: gid,
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ module Selector
6
+ # Include a single glyph by its donor gid. Used for unencoded
7
+ # glyphs (.notdef, spaces, format-specific specials).
8
+ class Gid
9
+ attr_reader :gid
10
+
11
+ def initialize(gid)
12
+ @gid = gid
13
+ end
14
+
15
+ def apply(source, bindings)
16
+ bindings << {
17
+ codepoint: nil,
18
+ source: source,
19
+ donor_gid: @gid,
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ module Selector
6
+ # Include every codepoint in a Range (e.g. 0x41..0x5A = A-Z).
7
+ # Glyphs missing from the source are silently skipped.
8
+ class Range
9
+ attr_reader :range
10
+
11
+ def initialize(range)
12
+ @range = range
13
+ end
14
+
15
+ def apply(source, bindings)
16
+ @range.each do |cp|
17
+ gid = source.gid_for_codepoint(cp)
18
+ next unless gid
19
+
20
+ bindings << {
21
+ codepoint: cp,
22
+ source: source,
23
+ donor_gid: gid,
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ # Selectors decide which glyphs from a source to include in the
6
+ # stitched font. Each selector appends to the Stitcher's bindings
7
+ # list. OCP: adding a new way to select = adding a new Selector
8
+ # class + a registry entry.
9
+ module Selector
10
+ autoload :Range, "fontisan/stitcher/selector/range"
11
+ autoload :Codepoints, "fontisan/stitcher/selector/codepoints"
12
+ autoload :Gid, "fontisan/stitcher/selector/gid"
13
+
14
+ REGISTRY = {
15
+ range: Range,
16
+ codepoints: Codepoints,
17
+ gid: Gid,
18
+ }.freeze
19
+
20
+ def self.resolve(name)
21
+ REGISTRY[name.to_sym] or
22
+ raise ArgumentError, "unknown selector: #{name.inspect}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ class Stitcher
5
+ # Wraps a source font (UFO or loaded TTF/OTF) behind a single
6
+ # extraction API used by the selectors.
7
+ #
8
+ # For UFO sources, glyphs are accessed by name directly. For TTF
9
+ # or OTF sources, the source is lazily converted to a UFO::Font
10
+ # via Ufo::Convert::FromBinData on first glyph access, then cached.
11
+ # This is O(n) in donor glyph count but amortized across all
12
+ # codepoint extractions from that donor.
13
+ class Source
14
+ attr_reader :font
15
+
16
+ def initialize(font)
17
+ @font = font
18
+ @ufo_cache = nil
19
+ end
20
+
21
+ # @return [Symbol] :ufo, :ttf, :otf
22
+ def format
23
+ case @font
24
+ when Fontisan::Ufo::Font then :ufo
25
+ when Fontisan::TrueTypeFont then :ttf
26
+ when Fontisan::OpenTypeFont then :otf
27
+ else :unknown
28
+ end
29
+ end
30
+
31
+ # Find the gid for a Unicode codepoint in this source.
32
+ # @param codepoint [Integer]
33
+ # @return [Integer, nil]
34
+ def gid_for_codepoint(codepoint)
35
+ case @font
36
+ when Fontisan::Ufo::Font then ufo_gid_for(codepoint)
37
+ else bin_data_gid_for(codepoint)
38
+ end
39
+ end
40
+
41
+ # Extract a glyph by gid.
42
+ # @param gid [Integer]
43
+ # @return [Fontisan::Ufo::Glyph, nil]
44
+ def glyph_for_gid(gid)
45
+ case @font
46
+ when Fontisan::Ufo::Font then ufo_glyph_at(gid)
47
+ else converted_ufo_glyph_at(gid)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # ---------- UFO source ----------
54
+
55
+ def ufo_gid_for(codepoint)
56
+ @font.glyphs.each_with_index do |(_name, glyph), index|
57
+ return index if glyph.unicodes.include?(codepoint)
58
+ end
59
+ nil
60
+ end
61
+
62
+ def ufo_glyph_at(gid)
63
+ names = @font.glyphs.keys
64
+ name = names[gid]
65
+ return nil unless name
66
+
67
+ @font.glyph(name)
68
+ end
69
+
70
+ # ---------- TTF/OTF source ----------
71
+
72
+ def bin_data_gid_for(codepoint)
73
+ cmap = @font.table("cmap")
74
+ return nil unless cmap
75
+
76
+ cmap.unicode_mappings[codepoint]
77
+ end
78
+
79
+ # Lazily convert the loaded TTF/OTF to a UFO::Font, then
80
+ # extract glyphs from the cached UFO model.
81
+ def converted_ufo
82
+ return @ufo_cache if @ufo_cache
83
+
84
+ @ufo_cache = Fontisan::Ufo::Convert::FromBinData.convert(@font)
85
+ end
86
+
87
+ def converted_ufo_glyph_at(gid)
88
+ ufo = converted_ufo
89
+ names = ufo.glyphs.keys
90
+ name = names[gid]
91
+ return nil unless name
92
+
93
+ ufo.glyph(name)
94
+ end
95
+ end
96
+ end
97
+ end