fontisan 0.1.0 → 0.2.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +529 -65
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1301 -275
  6. data/Rakefile +27 -2
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +309 -0
  12. data/lib/fontisan/collection/builder.rb +260 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +241 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +8 -1
  18. data/lib/fontisan/commands/convert_command.rb +291 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +295 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +178 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +69 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +259 -0
  39. data/lib/fontisan/converters/outline_converter.rb +936 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +121 -12
  57. data/lib/fontisan/font_writer.rb +301 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +177 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
  65. data/lib/fontisan/loading_modes.rb +113 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +233 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +296 -10
  88. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  89. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  90. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  91. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  92. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  93. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  94. data/lib/fontisan/outline_extractor.rb +423 -0
  95. data/lib/fontisan/subset/builder.rb +268 -0
  96. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  97. data/lib/fontisan/subset/options.rb +142 -0
  98. data/lib/fontisan/subset/profile.rb +152 -0
  99. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  100. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  101. data/lib/fontisan/svg/font_generator.rb +264 -0
  102. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  103. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  104. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  105. data/lib/fontisan/tables/cff/charset.rb +282 -0
  106. data/lib/fontisan/tables/cff/charstring.rb +905 -0
  107. data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
  108. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  109. data/lib/fontisan/tables/cff/dict.rb +351 -0
  110. data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
  111. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  112. data/lib/fontisan/tables/cff/header.rb +102 -0
  113. data/lib/fontisan/tables/cff/index.rb +237 -0
  114. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  115. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  116. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  117. data/lib/fontisan/tables/cff.rb +487 -0
  118. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  119. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  120. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  121. data/lib/fontisan/tables/cff2.rb +341 -0
  122. data/lib/fontisan/tables/cvar.rb +242 -0
  123. data/lib/fontisan/tables/fvar.rb +2 -2
  124. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  125. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  126. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  127. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  128. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  129. data/lib/fontisan/tables/glyf.rb +235 -0
  130. data/lib/fontisan/tables/gvar.rb +270 -0
  131. data/lib/fontisan/tables/hhea.rb +124 -0
  132. data/lib/fontisan/tables/hmtx.rb +287 -0
  133. data/lib/fontisan/tables/hvar.rb +191 -0
  134. data/lib/fontisan/tables/loca.rb +322 -0
  135. data/lib/fontisan/tables/maxp.rb +192 -0
  136. data/lib/fontisan/tables/mvar.rb +185 -0
  137. data/lib/fontisan/tables/name.rb +99 -30
  138. data/lib/fontisan/tables/variation_common.rb +346 -0
  139. data/lib/fontisan/tables/vvar.rb +234 -0
  140. data/lib/fontisan/true_type_collection.rb +156 -2
  141. data/lib/fontisan/true_type_font.rb +297 -11
  142. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  143. data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
  144. data/lib/fontisan/utils/thread_pool.rb +134 -0
  145. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  146. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  147. data/lib/fontisan/validation/structure_validator.rb +198 -0
  148. data/lib/fontisan/validation/table_validator.rb +158 -0
  149. data/lib/fontisan/validation/validator.rb +152 -0
  150. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  151. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  152. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  153. data/lib/fontisan/variable/instancer.rb +344 -0
  154. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  155. data/lib/fontisan/variable/region_matcher.rb +208 -0
  156. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  157. data/lib/fontisan/variable/table_updater.rb +219 -0
  158. data/lib/fontisan/variation/blend_applier.rb +199 -0
  159. data/lib/fontisan/variation/cache.rb +298 -0
  160. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  161. data/lib/fontisan/variation/converter.rb +268 -0
  162. data/lib/fontisan/variation/data_extractor.rb +86 -0
  163. data/lib/fontisan/variation/delta_applier.rb +266 -0
  164. data/lib/fontisan/variation/delta_parser.rb +228 -0
  165. data/lib/fontisan/variation/inspector.rb +275 -0
  166. data/lib/fontisan/variation/instance_generator.rb +273 -0
  167. data/lib/fontisan/variation/interpolator.rb +231 -0
  168. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  169. data/lib/fontisan/variation/optimizer.rb +418 -0
  170. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  171. data/lib/fontisan/variation/region_matcher.rb +221 -0
  172. data/lib/fontisan/variation/subsetter.rb +463 -0
  173. data/lib/fontisan/variation/table_accessor.rb +105 -0
  174. data/lib/fontisan/variation/validator.rb +345 -0
  175. data/lib/fontisan/variation/variation_context.rb +211 -0
  176. data/lib/fontisan/version.rb +1 -1
  177. data/lib/fontisan/woff2/directory.rb +257 -0
  178. data/lib/fontisan/woff2/header.rb +101 -0
  179. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  180. data/lib/fontisan/woff2_font.rb +712 -0
  181. data/lib/fontisan/woff_font.rb +483 -0
  182. data/lib/fontisan.rb +120 -0
  183. data/scripts/compare_stack_aware.rb +187 -0
  184. data/scripts/measure_optimization.rb +141 -0
  185. metadata +205 -4
data/Rakefile CHANGED
@@ -11,6 +11,20 @@ RuboCop::RakeTask.new
11
11
  namespace :fixtures do
12
12
  fixtures_dir = "spec/fixtures/fonts"
13
13
 
14
+ # Helper method to download a single file
15
+ def download_single_file(name, url, target_path)
16
+ require "open-uri"
17
+
18
+ puts "[fixtures:download] Downloading #{name}..."
19
+ FileUtils.mkdir_p(File.dirname(target_path))
20
+
21
+ URI.open(url) do |remote|
22
+ File.binwrite(target_path, remote.read)
23
+ end
24
+
25
+ puts "[fixtures:download] #{name} downloaded successfully"
26
+ end
27
+
14
28
  # Helper method to download and extract a font archive
15
29
  def download_font(name, url, target_dir)
16
30
  require "open-uri"
@@ -44,6 +58,12 @@ namespace :fixtures do
44
58
  # Font configurations with target directories and marker files
45
59
  # All fonts are downloaded via Rake
46
60
  fonts = {
61
+ "NotoSans" => {
62
+ url: "https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Regular.ttf",
63
+ target_dir: fixtures_dir.to_s,
64
+ marker: "#{fixtures_dir}/NotoSans-Regular.ttf",
65
+ single_file: true,
66
+ },
47
67
  "Libertinus" => {
48
68
  url: "https://github.com/alerque/libertinus/releases/download/v7.051/Libertinus-7.051.zip",
49
69
  target_dir: "#{fixtures_dir}/libertinus",
@@ -69,7 +89,11 @@ namespace :fixtures do
69
89
  # Create file tasks for each font
70
90
  fonts.each do |name, config|
71
91
  file config[:marker] do
72
- download_font(name, config[:url], config[:target_dir])
92
+ if config[:single_file]
93
+ download_single_file(name, config[:url], config[:marker])
94
+ else
95
+ download_font(name, config[:url], config[:target_dir])
96
+ end
73
97
  end
74
98
  end
75
99
 
@@ -78,7 +102,8 @@ namespace :fixtures do
78
102
 
79
103
  desc "Clean downloaded fixtures"
80
104
  task :clean do
81
- %w[libertinus MonaSans NotoSerifCJK NotoSerifCJK-VF].each do |dir|
105
+ %w[libertinus MonaSans NotoSerifCJK NotoSerifCJK-VF
106
+ NotoSans-Regular.ttf].each do |dir|
82
107
  path = File.join(fixtures_dir, dir)
83
108
  if File.exist?(path)
84
109
  FileUtils.rm_rf(path)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+ require 'fontisan'
5
+
6
+ # Load variable font
7
+ FONT_PATH = 'spec/fixtures/SourceSans3VF-Roman.otf'
8
+
9
+ unless File.exist?(FONT_PATH)
10
+ puts "Error: Test font not found at #{FONT_PATH}"
11
+ exit 1
12
+ end
13
+
14
+ font = Fontisan::FontLoader.load(FONT_PATH)
15
+
16
+ puts "=== Variable Font Parallel Performance ==="
17
+ puts "Font: #{FONT_PATH}"
18
+ puts
19
+
20
+ # Test coordinates
21
+ coords = 4.times.map { |i| { "wght" => 300 + i * 200 } }
22
+ puts "Generating #{coords.size} instances"
23
+ puts
24
+
25
+ # Sequential (1 thread)
26
+ seq_gen = Fontisan::Variation::ParallelGenerator.new(font, threads: 1)
27
+ seq_time = Benchmark.realtime do
28
+ seq_gen.generate_batch(coords)
29
+ end
30
+
31
+ # Parallel (4 threads)
32
+ par_gen = Fontisan::Variation::ParallelGenerator.new(font, threads: 4)
33
+ par_time = Benchmark.realtime do
34
+ par_gen.generate_batch(coords)
35
+ end
36
+
37
+ puts "Sequential (1 thread): #{format('%.3f', seq_time)}s"
38
+ puts "Parallel (4 threads): #{format('%.3f', par_time)}s"
39
+ puts "Speedup: #{format('%.2f', seq_time / par_time)}x"
40
+ puts
41
+
42
+ # Cache stats
43
+ cache = Fontisan::Variation::ThreadSafeCache.new
44
+ 5.times { cache.fetch("test_#{rand(100)}") { rand } }
45
+
46
+ puts "Cache Statistics:"
47
+ puts cache.statistics.inspect
@@ -0,0 +1,549 @@
1
+ # Migration Guide: extract_ttc → fontisan
2
+
3
+ ## Overview
4
+
5
+ **fontisan now fully supersedes extract_ttc** with superior functionality and a better user experience. All extract_ttc features are available in fontisan, plus many more.
6
+
7
+ ## Quick Command Translation
8
+
9
+ | extract_ttc Command | fontisan Equivalent | Notes |
10
+ |---------------------|---------------------|-------|
11
+ | `extract_ttc ls file.ttc` | `fontisan ls file.ttc` | ✅ Identical output, better formatting |
12
+ | `extract_ttc info file.ttc` | `fontisan info file.ttc` | ✅ Plus table sharing statistics |
13
+ | `extract_ttc extract file.ttc` | `fontisan unpack file.ttc --output-dir .` | ✅ More powerful with format conversion |
14
+
15
+ ## Feature Comparison
16
+
17
+ | Feature | extract_ttc | fontisan | Advantage |
18
+ |---------|-------------|----------|-----------|
19
+ | **List fonts in TTC** | ✓ | ✓ | Equal |
20
+ | **Show TTC metadata** | ✓ | ✓ | Equal |
21
+ | **Extract TTC fonts** | ✓ | ✓ | Equal |
22
+ | **Table sharing stats** | ✗ | ✓ | 🎯 fontisan |
23
+ | **Auto-detection** | ✗ | ✓ | 🎯 fontisan |
24
+ | **Works on TTF/OTF** | ✗ | ✓ | 🎯 fontisan |
25
+ | **Format conversion** | ✗ | ✓ | 🎯 fontisan |
26
+ | **Output formats** | Text only | Text, YAML, JSON | 🎯 fontisan |
27
+ | **Font analysis** | ✗ | ✓ (17 commands) | 🎯 fontisan |
28
+ | **Variable fonts** | ✗ | ✓ | 🎯 fontisan |
29
+ | **Font subsetting** | ✗ | ✓ | 🎯 fontisan |
30
+ | **Font validation** | ✗ | ✓ | 🎯 fontisan |
31
+ | **Create collections** | ✗ | ✓ | 🎯 fontisan |
32
+
33
+ ## Migration Examples
34
+
35
+ ### Listing Fonts
36
+
37
+ **extract_ttc:**
38
+ ```bash
39
+ extract_ttc ls fonts.ttc
40
+ ```
41
+
42
+ **fontisan:**
43
+ ```bash
44
+ fontisan ls fonts.ttc
45
+ ```
46
+
47
+ Output is cleaner and shows more info:
48
+ ```
49
+ Collection: fonts.ttc
50
+ Fonts: 2
51
+
52
+ 0. Helvetica Regular
53
+ PostScript: Helvetica-Regular
54
+ Format: TrueType
55
+ Glyphs: 268, Tables: 14
56
+
57
+ 1. Helvetica Bold
58
+ PostScript: Helvetica-Bold
59
+ Format: TrueType
60
+ Glyphs: 268, Tables: 14
61
+ ```
62
+
63
+ ### Collection Information
64
+
65
+ **extract_ttc:**
66
+ ```bash
67
+ extract_ttc info fonts.ttc
68
+ ```
69
+
70
+ **fontisan:**
71
+ ```bash
72
+ fontisan info fonts.ttc
73
+ ```
74
+
75
+ fontisan shows MORE information:
76
+ ```
77
+ === Collection Information ===
78
+ File: fonts.ttc
79
+ Format: TTC
80
+ Size: 2.55 KB
81
+
82
+ === Header ===
83
+ Tag: ttcf
84
+ Version: 1.0
85
+ Number of fonts: 2
86
+
87
+ === Font Offsets ===
88
+ 0. Offset: 20
89
+ 1. Offset: 272
90
+
91
+ === Table Sharing === ← NEW!
92
+ Shared tables: 17 ← NEW!
93
+ Unique tables: 13 ← NEW!
94
+ Sharing: 56.67% ← NEW!
95
+ Space saved: 2.02 KB ← NEW!
96
+ ```
97
+
98
+ ### Extracting Fonts
99
+
100
+ **extract_ttc:**
101
+ ```bash
102
+ extract_ttc extract fonts.ttc -o output/
103
+ ```
104
+
105
+ **fontisan:**
106
+ ```bash
107
+ fontisan unpack fonts.ttc --output-dir output/
108
+ ```
109
+
110
+ fontisan can also convert during extraction:
111
+ ```bash
112
+ # Extract as WOFF2 for web use
113
+ fontisan unpack fonts.ttc --output-dir web/ --format woff2
114
+
115
+ # Extract specific font only
116
+ fontisan unpack fonts.ttc --output-dir . --font-index 0
117
+ ```
118
+
119
+ ## Why Switch to fontisan?
120
+
121
+ ### 1. Universal Commands
122
+
123
+ fontisan commands work on **all** font formats:
124
+ ```bash
125
+ # Works on collections
126
+ fontisan ls fonts.ttc
127
+ fontisan info fonts.ttc
128
+
129
+ # Also works on individual fonts
130
+ fontisan ls font.ttf
131
+ fontisan info font.otf
132
+ ```
133
+
134
+ ### 2. Comprehensive Font Analysis
135
+
136
+ Beyond extract_ttc, fontisan provides:
137
+
138
+ ```bash
139
+ # Analyze font features
140
+ fontisan features font.ttf --script latn
141
+
142
+ # Check variable font axes
143
+ fontisan variable VariableFont.ttf
144
+
145
+ # Validate font integrity
146
+ fontisan validate font.ttf
147
+
148
+ # Subset to specific characters
149
+ fontisan subset font.ttf --text "Hello" --output hello.ttf
150
+
151
+ # Convert formats
152
+ fontisan convert font.ttf --to woff2 --output font.woff2
153
+
154
+ # Create collections
155
+ fontisan pack Regular.ttf Bold.ttf --output Family.ttc
156
+ ```
157
+
158
+ ### 3. Modern Output Formats
159
+
160
+ ```bash
161
+ # Human-readable text (default)
162
+ fontisan info fonts.ttc
163
+
164
+ # Machine-readable YAML
165
+ fontisan info fonts.ttc --format yaml
166
+
167
+ # Machine-readable JSON
168
+ fontisan info fonts.ttc --format json
169
+ ```
170
+
171
+ ### 4. Better Error Messages
172
+
173
+ fontisan provides clear, actionable error messages:
174
+ ```
175
+ Error: Font index 5 out of range (collection has 2 fonts)
176
+ ```
177
+
178
+ vs extract_ttc generic Ruby errors.
179
+
180
+ ## Installation
181
+
182
+ ### Remove extract_ttc (optional)
183
+
184
+ ```bash
185
+ gem uninstall extract_ttc
186
+ ```
187
+
188
+ ### Install fontisan
189
+
190
+ ```bash
191
+ gem install fontisan
192
+ ```
193
+
194
+ Or add to Gemfile:
195
+ ```ruby
196
+ gem 'fontisan'
197
+ ```
198
+
199
+ ## API Usage
200
+
201
+ ### extract_ttc API
202
+
203
+ ```ruby
204
+ require 'extract_ttc'
205
+
206
+ # List fonts
207
+ output_files = ExtractTtc.extract("fonts.ttc")
208
+ ```
209
+
210
+ ### fontisan API (More Powerful)
211
+
212
+ ```ruby
213
+ require 'fontisan'
214
+
215
+ # List fonts in collection
216
+ collection = Fontisan::FontLoader.load_collection("fonts.ttc")
217
+ File.open("fonts.ttc", "rb") do |io|
218
+ list = collection.list_fonts(io)
219
+ list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
220
+ end
221
+
222
+ # Get collection metadata
223
+ File.open("fonts.ttc", "rb") do |io|
224
+ info = collection.collection_info(io, "fonts.ttc")
225
+ puts "Fonts: #{info.num_fonts}"
226
+ puts "Sharing: #{info.table_sharing.sharing_percentage}%"
227
+ end
228
+
229
+ # Extract fonts with more control
230
+ File.open("fonts.ttc", "rb") do |io|
231
+ fonts = collection.extract_fonts(io)
232
+ fonts.each_with_index do |font, i|
233
+ font.to_file("output/font_#{i}.ttf")
234
+ end
235
+ end
236
+ ```
237
+
238
+ ## Backward Compatibility
239
+
240
+ fontisan maintains 100% backward compatibility:
241
+ - All existing `fontisan` commands continue to work
242
+ - No breaking changes to existing functionality
243
+ - New commands add features without removing anything
244
+
245
+ #=========================================================================
246
+
247
+ = ExtractTTC to Fontisan Migration Guide
248
+
249
+ This guide helps users migrate from https://github.com/fontist/extract_ttc[ExtractTTC] to Fontisan.
250
+
251
+ Fontisan provides complete compatibility with all ExtractTTC functionality while adding comprehensive font analysis, subsetting, validation, and format conversion capabilities.
252
+
253
+ == Command Mapping Reference
254
+
255
+ [cols="2,2,3"]
256
+ |===
257
+ |ExtractTTC Command |Fontisan Equivalent |Description
258
+
259
+ |`extract_ttc --list FONT.ttc`
260
+ |`fontisan ls FONT.ttc`
261
+ |List fonts in collection with index, family, and style
262
+
263
+ |`extract_ttc --info FONT.ttc`
264
+ |`fontisan info FONT.ttc`
265
+ |Show detailed font information for collection
266
+
267
+ |`extract_ttc --unpack FONT.ttc OUTPUT_DIR`
268
+ |`fontisan unpack FONT.ttc OUTPUT_DIR`
269
+ |Extract all fonts from collection to directory
270
+
271
+ |`extract_ttc --font-index INDEX FONT.ttc OUTPUT.ttf`
272
+ |`fontisan unpack FONT.ttc --font-index INDEX OUTPUT.ttf`
273
+ |Extract specific font by index
274
+
275
+ |`extract_ttc --validate FONT.ttc`
276
+ |`fontisan validate FONT.ttc`
277
+ |Validate font/collection structure and checksums
278
+ |===
279
+
280
+ == Enhanced Collection Management
281
+
282
+ Fontisan provides additional collection features beyond ExtractTTC:
283
+
284
+ === List Collection Contents
285
+
286
+ [source,shell]
287
+ ----
288
+ # List all fonts in a TTC with detailed info
289
+ $ fontisan ls spec/fixtures/fonts/NotoSerifCJK/NotoSerifCJK.ttc
290
+
291
+ Font 0: Noto Serif CJK JP
292
+ Family: Noto Serif CJK JP
293
+ Subfamily: Regular
294
+ PostScript: NotoSerifCJKJP-Regular
295
+
296
+ Font 1: Noto Serif CJK KR
297
+ Family: Noto Serif CJK KR
298
+ Subfamily: Regular
299
+ PostScript: NotoSerifCJKKR-Regular
300
+
301
+ Font 2: Noto Serif CJK SC
302
+ Family: Noto Serif CJK SC
303
+ Subfamily: Regular
304
+ PostScript: NotoSerifCJKSC-Regular
305
+
306
+ Font 3: Noto Serif CJK TC
307
+ Family: Noto Serif CJK TC
308
+ Subfamily: Regular
309
+ PostScript: NotoSerifCJKTC-Regular
310
+ ----
311
+
312
+ === Extract with Validation
313
+
314
+ [source,shell]
315
+ ----
316
+ # Extract and validate simultaneously
317
+ $ fontisan unpack spec/fixtures/fonts/NotoSerifCJK/NotoSerifCJK.ttc extracted_fonts/ --validate
318
+
319
+ Extracting font 0: Noto Serif CJK JP → extracted_fonts/NotoSerifCJKJP-Regular.ttf
320
+ Extracting font 1: Noto Serif CJK KR → extracted_fonts/NotoSerifCJKKR-Regular.ttf
321
+ Extracting font 2: Noto Serif CJK SC → extracted_fonts/NotoSerifCJKSC-Regular.ttf
322
+ Extracting font 3: Noto Serif CJK TC → extracted_fonts/NotoSerifCJKTC-Regular.ttf
323
+
324
+ Validation: All fonts extracted successfully
325
+ ----
326
+
327
+ === Get Collection Information
328
+
329
+ [source,shell]
330
+ ----
331
+ # Detailed collection analysis
332
+ $ fontisan info spec/fixtures/fonts/NotoSerifCJK/NotoSerifCJK.ttc --format yaml
333
+
334
+ ---
335
+ collection_type: ttc
336
+ font_count: 4
337
+ fonts:
338
+ - index: 0
339
+ family_name: Noto Serif CJK JP
340
+ subfamily_name: Regular
341
+ postscript_name: NotoSerifCJKJP-Regular
342
+ font_format: opentype
343
+ - index: 1
344
+ family_name: Noto Serif CJK KR
345
+ subfamily_name: Regular
346
+ postscript_name: NotoSerifCJKKR-Regular
347
+ font_format: opentype
348
+ - index: 2
349
+ family_name: Noto Serif CJK SC
350
+ subfamily_name: Regular
351
+ postscript_name: NotoSerifCJKSC-Regular
352
+ font_format: opentype
353
+ - index: 3
354
+ family_name: Noto Serif CJK TC
355
+ subfamily_name: Regular
356
+ postscript_name: NotoSerifCJKTC-Regular
357
+ font_format: opentype
358
+ ----
359
+
360
+ == Advanced Features Beyond ExtractTTC
361
+
362
+ Fontisan provides capabilities that ExtractTTC never had:
363
+
364
+ === Collection Creation
365
+
366
+ Create new TTC/OTC files from individual fonts with automatic table sharing optimization:
367
+
368
+ [source,shell]
369
+ ----
370
+ # Pack fonts into TTC with table sharing optimization
371
+ $ fontisan pack font1.ttf font2.ttf font3.ttf --output family.ttc --analyze
372
+
373
+ Collection Analysis:
374
+ Total fonts: 3
375
+ Shared tables: 12
376
+ Potential space savings: 45.2 KB
377
+ Table sharing: 68.5%
378
+
379
+ Collection created successfully:
380
+ Output: family.ttc
381
+ Format: TTC
382
+ Fonts: 3
383
+ Size: 245.8 KB
384
+ Space saved: 45.2 KB
385
+ Sharing: 68.5%
386
+ ----
387
+
388
+ === Format Conversion and Subsetting
389
+
390
+ Convert between font formats and create optimized subsets:
391
+
392
+ [source,shell]
393
+ ----
394
+ # Convert TTF to WOFF2 for web usage
395
+ $ fontisan convert font.ttf --to woff2 --output font.woff2
396
+
397
+ # Create PDF-optimized subset
398
+ $ fontisan subset font.ttf --text "Hello World" --output subset.ttf --profile pdf
399
+ ----
400
+
401
+ === Font Analysis and Inspection
402
+
403
+ Comprehensive font analysis capabilities:
404
+
405
+ [source,shell]
406
+ ----
407
+ # Extract OpenType tables with details
408
+ $ fontisan tables font.ttf --format yaml
409
+
410
+ # Display variable font information
411
+ $ fontisan variable font.ttf
412
+
413
+ # Show supported scripts and features
414
+ $ fontisan scripts font.ttf
415
+ $ fontisan features font.ttf --script latn
416
+
417
+ # Dump raw table data for analysis
418
+ $ fontisan dump-table font.ttf name > name_table.bin
419
+ ----
420
+
421
+ === Font Validation
422
+
423
+ Multiple validation levels with detailed reporting:
424
+
425
+ [source,shell]
426
+ ----
427
+ # Standard validation (allows warnings)
428
+ $ fontisan validate font.ttf
429
+
430
+ # Strict validation (no warnings allowed)
431
+ $ fontisan validate font.ttf --level strict
432
+
433
+ # Detailed validation report
434
+ $ fontisan validate font.ttf --format yaml
435
+ ----
436
+
437
+ == Migration Examples
438
+
439
+ === Basic Collection Operations
440
+
441
+ [source,shell]
442
+ # ExtractTTC style operations
443
+ extract_ttc --list collection.ttc
444
+ fontisan ls collection.ttc
445
+
446
+ extract_ttc --info collection.ttc
447
+ fontisan info collection.ttc
448
+
449
+ extract_ttc --unpack collection.ttc output_dir/
450
+ fontisan unpack collection.ttc output_dir/
451
+
452
+ extract_ttc --font-index 0 collection.ttc font0.ttf
453
+ fontisan unpack collection.ttc --font-index 0 font0.ttf
454
+ ----
455
+
456
+ === Enhanced Operations (Fontisan Only)
457
+
458
+ [source,shell]
459
+ # Create collections (not possible with ExtractTTC)
460
+ fontisan pack font1.ttf font2.ttf --output new_collection.ttc
461
+
462
+ # Convert formats
463
+ fontisan convert font.ttf --to woff2 --output font.woff2
464
+
465
+ # Create subsets
466
+ fontisan subset font.ttf --unicode "U+0041-U+005A" --output latin.ttf
467
+
468
+ # Validate fonts
469
+ fontisan validate font.ttf --level strict
470
+
471
+ # Analyze fonts
472
+ fontisan scripts font.ttf
473
+ fontisan features font.ttf --script latn
474
+ fontisan variable font.ttf
475
+ ----
476
+
477
+ == Configuration and Options
478
+
479
+ Fontisan provides extensive configuration options:
480
+
481
+ === Global Options
482
+ All commands support these options:
483
+
484
+ * `--format FORMAT`: Output format (`text`, `json`, `yaml`)
485
+ * `--font-index INDEX`: Font index for TTC files (default: 0)
486
+ * `--verbose`: Enable verbose output
487
+ * `--quiet`: Suppress non-error output
488
+
489
+ === Collection-Specific Options
490
+
491
+ * `--analyze`: Show space analysis for pack operations
492
+ * `--optimize`: Enable table sharing optimization (default: true)
493
+ * `--format {ttc|otc}`: Collection format for pack operations
494
+
495
+ === Conversion Options
496
+
497
+ * `--to FORMAT`: Target format for conversion
498
+ * `--quality LEVEL`: Compression quality (0-11)
499
+ * `--profile PROFILE`: Subsetting profile (`pdf`, `web`, `minimal`)
500
+
501
+ == Error Handling
502
+
503
+ Fontisan provides improved error handling compared to ExtractTTC:
504
+
505
+ [source,shell]
506
+ ----
507
+ # Clear error messages
508
+ $ fontisan convert font.ttf --to unsupported
509
+ Error: Unsupported conversion from ttf to unsupported
510
+ Available targets: ttf, otf, woff2, svg
511
+
512
+ # Helpful validation errors
513
+ $ fontisan validate corrupted.ttf
514
+ Validation failed: Invalid checksum in head table
515
+ ----
516
+
517
+ == Performance
518
+
519
+ Fontisan is optimized for performance:
520
+
521
+ * **Collection Operations**: Sub-second for typical collections
522
+ * **Format Conversion**: <1 second for most conversions
523
+ * **Font Analysis**: <100ms for table inspection
524
+ * **Memory Efficient**: Streaming processing for large fonts
525
+
526
+ == Backward Compatibility
527
+
528
+ Fontisan maintains full backward compatibility:
529
+
530
+ * All ExtractTTC commands work identically
531
+ * Same exit codes and error handling
532
+ * Compatible with existing scripts and workflows
533
+ * Enhanced with additional features
534
+
535
+ == Getting Help
536
+
537
+ [source,shell]
538
+ ----
539
+ # General help
540
+ $ fontisan --help
541
+
542
+ # Command-specific help
543
+ $ fontisan pack --help
544
+ $ fontisan convert --help
545
+ $ fontisan subset --help
546
+
547
+ # Version information
548
+ $ fontisan version
549
+ ----
data/fontisan.gemspec CHANGED
@@ -38,7 +38,10 @@ Gem::Specification.new do |spec|
38
38
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
39
39
  spec.require_paths = ["lib"]
40
40
 
41
+ spec.add_dependency "base64"
41
42
  spec.add_dependency "bindata", "~> 2.5"
43
+ spec.add_dependency "brotli", "~> 0.5"
42
44
  spec.add_dependency "lutaml-model", "~> 0.7"
43
- spec.add_dependency "thor", "~> 1.4"
45
+ spec.add_dependency "nokogiri", "~> 1.16"
46
+ spec.add_dependency "thor", "~> 1.3"
44
47
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bindata"
4
+ require "stringio"
4
5
 
5
6
  module Fontisan
6
7
  module Binary
@@ -25,7 +26,27 @@ module Fontisan
25
26
  def self.read(io)
26
27
  return new if io.nil? || (io.respond_to?(:empty?) && io.empty?)
27
28
 
28
- super
29
+ # Store the original data for later parsing
30
+ # Convert IO to string if needed
31
+ if io.is_a?(String)
32
+ data = io
33
+ instance = super(StringIO.new(data))
34
+ else
35
+ # For IO objects, read to string first
36
+ data = io.read
37
+ io.rewind if io.respond_to?(:rewind)
38
+ instance = super(io)
39
+ end
40
+
41
+ instance.instance_variable_set(:@raw_data, data)
42
+ instance
43
+ end
44
+
45
+ # Get the raw binary data that was read
46
+ #
47
+ # @return [String] Raw binary data
48
+ def raw_data
49
+ @raw_data || to_binary_s
29
50
  end
30
51
 
31
52
  # Check if the record is valid