fontisan 0.2.6 → 0.2.8

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +103 -0
  3. data/.rubocop_todo.yml +107 -318
  4. data/Gemfile +1 -1
  5. data/README.adoc +127 -17
  6. data/Rakefile +12 -7
  7. data/benchmark/variation_quick_bench.rb +1 -1
  8. data/lib/fontisan/base_collection.rb +5 -33
  9. data/lib/fontisan/cli.rb +45 -13
  10. data/lib/fontisan/collection/dfont_builder.rb +2 -1
  11. data/lib/fontisan/collection/shared_logic.rb +54 -0
  12. data/lib/fontisan/commands/convert_command.rb +2 -4
  13. data/lib/fontisan/commands/info_command.rb +3 -3
  14. data/lib/fontisan/commands/pack_command.rb +2 -1
  15. data/lib/fontisan/commands/validate_command.rb +157 -6
  16. data/lib/fontisan/converters/collection_converter.rb +22 -13
  17. data/lib/fontisan/converters/svg_generator.rb +2 -1
  18. data/lib/fontisan/converters/woff2_encoder.rb +6 -6
  19. data/lib/fontisan/converters/woff_writer.rb +3 -1
  20. data/lib/fontisan/dfont_collection.rb +84 -0
  21. data/lib/fontisan/font_loader.rb +9 -9
  22. data/lib/fontisan/formatters/text_formatter.rb +18 -14
  23. data/lib/fontisan/hints/hint_converter.rb +1 -1
  24. data/lib/fontisan/hints/hint_validator.rb +13 -10
  25. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
  26. data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
  27. data/lib/fontisan/models/collection_validation_report.rb +104 -0
  28. data/lib/fontisan/models/font_report.rb +24 -0
  29. data/lib/fontisan/models/validation_report.rb +7 -2
  30. data/lib/fontisan/open_type_font.rb +2 -3
  31. data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
  32. data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
  33. data/lib/fontisan/subset/glyph_mapping.rb +2 -0
  34. data/lib/fontisan/subset/table_subsetter.rb +2 -2
  35. data/lib/fontisan/tables/cblc.rb +8 -4
  36. data/lib/fontisan/tables/cff/index.rb +2 -0
  37. data/lib/fontisan/tables/cff.rb +6 -3
  38. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
  39. data/lib/fontisan/tables/cff2.rb +1 -1
  40. data/lib/fontisan/tables/cmap.rb +5 -5
  41. data/lib/fontisan/tables/glyf.rb +8 -10
  42. data/lib/fontisan/tables/head.rb +3 -3
  43. data/lib/fontisan/tables/hhea.rb +4 -4
  44. data/lib/fontisan/tables/maxp.rb +2 -2
  45. data/lib/fontisan/tables/name.rb +1 -1
  46. data/lib/fontisan/tables/os2.rb +8 -8
  47. data/lib/fontisan/tables/post.rb +2 -2
  48. data/lib/fontisan/tables/sbix.rb +5 -4
  49. data/lib/fontisan/true_type_font.rb +2 -3
  50. data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
  51. data/lib/fontisan/validation/collection_validator.rb +4 -2
  52. data/lib/fontisan/validators/basic_validator.rb +11 -21
  53. data/lib/fontisan/validators/font_book_validator.rb +29 -50
  54. data/lib/fontisan/validators/opentype_validator.rb +24 -28
  55. data/lib/fontisan/validators/validator.rb +87 -66
  56. data/lib/fontisan/validators/web_font_validator.rb +16 -21
  57. data/lib/fontisan/version.rb +1 -1
  58. data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
  59. data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
  60. data/lib/fontisan/woff2/table_transformer.rb +4 -2
  61. data/lib/fontisan/woff2_font.rb +4 -2
  62. data/lib/fontisan/woff_font.rb +2 -2
  63. data/lib/fontisan.rb +2 -2
  64. data/scripts/compare_stack_aware.rb +1 -1
  65. data/scripts/measure_optimization.rb +1 -2
  66. metadata +5 -2
data/README.adoc CHANGED
@@ -184,7 +184,7 @@ Family: Noto Serif CJK JP ExtraLight
184
184
  ====
185
185
  [source,shell]
186
186
  ----
187
- $ fontisan info spec/fixtures/fonts/MonaSans/mona-sans-2.0.8/googlefonts/variable/MonaSans[wdth,wght].ttf --brief
187
+ $ fontisan info spec/fixtures/fonts/MonaSans/variable/MonaSans[wdth,wght].ttf --brief
188
188
 
189
189
  Font type: TrueType (Variable)
190
190
  Family: Mona Sans ExtraLight
@@ -1034,26 +1034,19 @@ structures, usability, instructions, and glyphs.
1034
1034
 
1035
1035
  === Predefined profiles
1036
1036
 
1037
- Fontisan includes several predefined validation profiles for common use cases:
1037
+ Fontisan includes validation profiles for different use cases:
1038
1038
 
1039
- `indexability`:: Fast validation for font discovery and indexing (< 50ms). Uses
1040
- BasicValidator with 8 essential checks. Loading mode: metadata.
1039
+ `indexability`:: Fast font discovery and indexing (~5x faster, 8 checks, metadata-only)
1041
1040
 
1042
- `usability`:: Basic usability for font installation. Uses FontBookValidator with
1043
- 26 checks including macOS Font Book compatibility. Loading mode: full.
1041
+ `usability`:: Font installation compatibility (26 checks, macOS Font Book focused)
1044
1042
 
1045
- `production`:: Comprehensive quality checks for production fonts (default
1046
- profile). Uses OpenTypeValidator with 36 checks for OpenType spec compliance.
1047
- Loading mode: full.
1043
+ `production`:: Comprehensive production quality (36 checks, OpenType spec compliance - default)
1048
1044
 
1049
- `web`:: Web font embedding and optimization validation. Uses WebFontValidator
1050
- with 18 checks for web deployment. Loading mode: full.
1045
+ `web`:: Web font embedding readiness (18 checks for web deployment)
1051
1046
 
1052
- `spec_compliance`:: Full OpenType specification compliance with detailed checks.
1053
- Uses OpenTypeValidator with info-level severity for comprehensive analysis.
1054
- Loading mode: full.
1047
+ `spec_compliance`:: Full OpenType specification compliance (detailed analysis mode)
1055
1048
 
1056
- `default`:: Alias for the production profile.
1049
+ `default`:: Alias for the production profile
1057
1050
 
1058
1051
 
1059
1052
  === Command-line usage
@@ -1154,6 +1147,80 @@ production, web, spec_compliance, default)
1154
1147
 
1155
1148
  -e, --exclude CHECKS:: Exclude specific checks (comma-separated list)
1156
1149
 
1150
+ === Collection validation
1151
+
1152
+ The `validate` command automatically detects and validates font collections (TTC, OTC, dfont).
1153
+ When validating a collection, fontisan validates all fonts in the collection and displays
1154
+ per-font results with an overall summary.
1155
+
1156
+ .Validation profiles with collections
1157
+ [example]
1158
+ ====
1159
+ All validation profiles work with font collections. The selected profile determines:
1160
+
1161
+ * **Which tables are loaded** - metadata vs full mode
1162
+ * **Which checks are performed** - number and type of validations
1163
+ * **Performance characteristics** - indexability is ~5x faster than production
1164
+
1165
+ For large collections (CJK fonts with 30+ fonts), use the `indexability` profile for fast
1166
+ validation when scanning for font discovery, or `production` for comprehensive quality
1167
+ checks.
1168
+
1169
+ [source,shell]
1170
+ ----
1171
+ # Quick validation for indexing (metadata mode, 8 checks, ~5x faster)
1172
+ $ fontisan validate /path/to/font.ttc -t indexability
1173
+
1174
+ # Comprehensive validation (full mode, 37 checks)
1175
+ $ fontisan validate /path/to/font.ttc -t production
1176
+
1177
+ # Web font readiness validation
1178
+ $ fontisan validate /path/to/font.ttc -t web
1179
+ ----
1180
+
1181
+ The collection validation output includes:
1182
+
1183
+ * *Collection header* - Path, type, and number of fonts
1184
+ * *Summary* - Total errors, warnings, and info across all fonts
1185
+ * *Per-font sections* - Individual validation results for each font with:
1186
+ ** Font index and name
1187
+ ** Font path in `collection.ttc:index` format
1188
+ ** Individual font status (VALID/INVALID/VALID_WITH_WARNINGS)
1189
+ ** Font-specific errors and warnings
1190
+ ** Exit codes* - For collections, uses the "worst" status across all fonts:
1191
+ ** 0 = All fonts valid
1192
+ ** 2 = Any font has fatal errors
1193
+ ** 3 = Any font has errors (and no fatal)
1194
+ ** 4 = Any font has warnings (and no errors)
1195
+ ** 5 = Any font has info issues (and no errors or warnings)
1196
+ ====
1197
+
1198
+ .Validate a font collection
1199
+ [example]
1200
+ ====
1201
+ [source,shell]
1202
+ ----
1203
+ $ fontisan validate /path/to/font.ttc
1204
+ Collection: /path/to/font.ttc
1205
+ Type: TTC
1206
+ Fonts: 4
1207
+
1208
+ Summary:
1209
+ Total Errors: 14
1210
+ Total Warnings: 8
1211
+ Total Info: 0
1212
+
1213
+ === Font 0: Lucida Grande ===
1214
+ Font: /path/to/font.ttc:0
1215
+ Status: INVALID
1216
+ ...
1217
+
1218
+ === Font 1: Lucida Grande Bold ===
1219
+ Font: /path/to/font.ttc:1
1220
+ Status: INVALID
1221
+ ...
1222
+ ----
1223
+ ====
1157
1224
 
1158
1225
  === Ruby API usage
1159
1226
 
@@ -1241,7 +1308,7 @@ validator = Fontisan::Validators::OpenTypeValidator.new
1241
1308
  report = validator.validate(font)
1242
1309
 
1243
1310
  # Check individual results
1244
- name_check = report.result_of(:name_version)
1311
+ name_check = report.result_of(:name_validation)
1245
1312
  puts name_check.passed?
1246
1313
  puts name_check.severity
1247
1314
  ----
@@ -1308,7 +1375,7 @@ font = Fontisan::FontLoader.load('font.ttf')
1308
1375
  validator = MyFontValidator.new
1309
1376
  report = validator.validate(font)
1310
1377
 
1311
- # Check results
1378
+ # Check validation results
1312
1379
  puts report.valid? # => true/false
1313
1380
  puts report.status # => "valid", "valid_with_warnings", "invalid"
1314
1381
  puts report.summary.errors # => number of errors
@@ -1576,6 +1643,14 @@ dfont (Apple suitcase)::: Apple proprietary format storing complete Mac font
1576
1643
  suitcase resources in the data fork. Supports any SFNT fonts (TTF, OTF, or
1577
1644
  mixed). Mac OS X specific. File extension: `.dfont`
1578
1645
 
1646
+ Fontist returns the appropriate collection type based on the font data:
1647
+
1648
+ * Examines font data within collection to determine type (TTC vs OTC)
1649
+ * TTC contains fonts with TrueType outlines (glyf table)
1650
+ * OTC contains fonts with CFF outlines (CFF table)
1651
+ * If ANY font in the collection has CFF outlines, use OpenTypeCollection
1652
+ * Only use TrueTypeCollection if ALL fonts have TrueType outlines
1653
+
1579
1654
 
1580
1655
  === Collection compatibility
1581
1656
 
@@ -2076,6 +2151,41 @@ Fontisan can:
2076
2151
  * Importing and generation PNG block ruler layers
2077
2152
 
2078
2153
 
2154
+ == Testing
2155
+
2156
+ === General
2157
+
2158
+ Fontisan has a comprehensive test suite covering all font operations, formats,
2159
+ and features.
2160
+
2161
+ === Running tests
2162
+
2163
+ [source,shell]
2164
+ ----
2165
+ # Run full test suite
2166
+ bundle exec rspec
2167
+
2168
+ # Run with documentation format
2169
+ bundle exec rspec --format documentation
2170
+
2171
+ # Run specific file
2172
+ bundle exec rspec spec/fontisan/tables/maxp_spec.rb
2173
+ ----
2174
+
2175
+ === Test fixtures
2176
+
2177
+ All required font fixtures are automatically downloaded before tests run. The
2178
+ test suite uses a centralized fixture configuration system that ensures all
2179
+ necessary fonts are available.
2180
+
2181
+ [source,shell]
2182
+ ----
2183
+ # Manual fixture management (if needed)
2184
+ bundle exec rake fixtures:download # Download all test fonts
2185
+ bundle exec rake fixtures:clean # Remove downloaded fonts
2186
+ ----
2187
+
2188
+
2079
2189
  == Copyright and license
2080
2190
 
2081
2191
  Copyright https://www.ribose.com[Ribose].
data/Rakefile CHANGED
@@ -8,6 +8,7 @@ require "rubocop/rake_task"
8
8
 
9
9
  RuboCop::RakeTask.new
10
10
 
11
+ # rubocop:disable Metrics/BlockLength
11
12
  namespace :fixtures do
12
13
  # Load centralized fixture configuration
13
14
  require_relative "spec/support/fixture_fonts"
@@ -99,8 +100,13 @@ namespace :fixtures do
99
100
  end
100
101
  end
101
102
 
103
+ # Compute download task prerequisites (marker files for non-skipped fonts)
104
+ download_prerequisites = fonts.values.reject do |config|
105
+ config[:skip_download]
106
+ end.map { |config| config[:marker] }
107
+
102
108
  desc "Download all test fixture fonts"
103
- task download: fonts.values.reject { |config| config[:skip_download] }.map { |config| config[:marker] }
109
+ task download: download_prerequisites
104
110
 
105
111
  desc "Clean downloaded fixtures"
106
112
  task :clean do
@@ -112,17 +118,16 @@ namespace :fixtures do
112
118
  FileUtils.rm_f(config[:marker])
113
119
  puts "[fixtures:clean] Removed #{config[:marker]}"
114
120
  end
115
- else
121
+ elsif File.exist?(config[:target_dir])
116
122
  # For archives, delete the entire target directory
117
- if File.exist?(config[:target_dir])
118
- puts "[fixtures:clean] Removing #{config[:target_dir]}..."
119
- FileUtils.rm_rf(config[:target_dir])
120
- puts "[fixtures:clean] Removed #{config[:target_dir]}"
121
- end
123
+ puts "[fixtures:clean] Removing #{config[:target_dir]}..."
124
+ FileUtils.rm_rf(config[:target_dir])
125
+ puts "[fixtures:clean] Removed #{config[:target_dir]}"
122
126
  end
123
127
  end
124
128
  end
125
129
  end
130
+ # rubocop:enable Metrics/BlockLength
126
131
 
127
132
  # RSpec task depends on fixtures
128
133
  RSpec::Core::RakeTask.new(spec: "fixtures:download")
@@ -18,7 +18,7 @@ puts "Font: #{FONT_PATH}"
18
18
  puts
19
19
 
20
20
  # Test coordinates
21
- coords = Array.new(4) { |i| { "wght" => 300 + i * 200 } }
21
+ coords = Array.new(4) { |i| { "wght" => 300 + (i * 200) } }
22
22
  puts "Generating #{coords.size} instances"
23
23
  puts
24
24
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bindata"
4
4
  require_relative "constants"
5
+ require_relative "collection/shared_logic"
5
6
 
6
7
  module Fontisan
7
8
  # Abstract base class for font collections (TTC/OTC)
@@ -29,6 +30,8 @@ module Fontisan
29
30
  # end
30
31
  # end
31
32
  class BaseCollection < BinData::Record
33
+ include Collection::SharedLogic
34
+
32
35
  endian :big
33
36
 
34
37
  string :tag, length: 4, assert: "ttcf"
@@ -251,8 +254,6 @@ module Fontisan
251
254
  # @param io [IO] Open file handle
252
255
  # @return [TableSharingInfo] Sharing statistics
253
256
  def calculate_table_sharing(io)
254
- require_relative "models/table_sharing_info"
255
-
256
257
  font_class = self.class.font_class
257
258
 
258
259
  # Extract all fonts
@@ -260,37 +261,8 @@ module Fontisan
260
261
  font_class.from_collection(io, offset)
261
262
  end
262
263
 
263
- # Build table hash map (checksum -> size)
264
- table_map = {}
265
- total_table_size = 0
266
-
267
- fonts.each do |font|
268
- font.tables.each do |entry|
269
- key = entry.checksum
270
- size = entry.table_length
271
- table_map[key] ||= size
272
- total_table_size += size
273
- end
274
- end
275
-
276
- # Count unique vs shared
277
- unique_tables = table_map.size
278
- total_tables = fonts.sum { |f| f.tables.length }
279
- shared_tables = total_tables - unique_tables
280
-
281
- # Calculate space saved
282
- unique_size = table_map.values.sum
283
- space_saved = total_table_size - unique_size
284
-
285
- # Calculate sharing percentage
286
- sharing_pct = total_tables.positive? ? (shared_tables.to_f / total_tables * 100).round(2) : 0.0
287
-
288
- Models::TableSharingInfo.new(
289
- shared_tables: shared_tables,
290
- unique_tables: unique_tables,
291
- sharing_percentage: sharing_pct,
292
- space_saved_bytes: space_saved,
293
- )
264
+ # Use shared logic for calculation
265
+ calculate_table_sharing_for_fonts(fonts)
294
266
  end
295
267
  end
296
268
  end
data/lib/fontisan/cli.rb CHANGED
@@ -367,18 +367,27 @@ module Fontisan
367
367
  handle_error(e)
368
368
  end
369
369
 
370
- desc "validate FONT_FILE", "Validate font file"
370
+ desc "validate [FONT_FILE]", "Validate font file"
371
371
  long_desc <<-DESC
372
372
  Validate font file against quality checks and standards.
373
373
 
374
- Test lists (-t/--test-list):
375
- indexability - Fast indexing validation
374
+ Supports individual fonts (TTF/OTF) and font collections (TTC/OTC/dfont).
375
+ For collections, validates each font and provides a summary report.
376
+
377
+ Profiles (-p/--profile):
378
+ indexability - Fast indexing validation (metadata only, 8 checks)
376
379
  usability - Installation compatibility
377
380
  production - Comprehensive quality (default)
378
381
  web - Web font readiness
379
382
  spec_compliance - OpenType spec compliance
380
383
  default - Production profile (alias)
381
384
 
385
+ Collection validation with profiles:
386
+ When validating TTC/OTC/dfont collections, the selected profile
387
+ determines which tables are loaded and which checks are performed
388
+ for each font in the collection. Use 'indexability' for quick
389
+ validation or 'production' for comprehensive quality checks.
390
+
382
391
  Return values (with -R/--return-value-results):
383
392
  0 No results
384
393
  1 Execution errors
@@ -389,25 +398,41 @@ module Fontisan
389
398
  DESC
390
399
 
391
400
  option :exclude, aliases: "-e", type: :array, desc: "Tests to exclude"
392
- option :list, aliases: "-l", type: :boolean, desc: "List available tests"
401
+ option :list, aliases: "-l", type: :boolean, desc: "List available profiles"
393
402
  option :output, aliases: "-o", type: :string, desc: "Output file"
394
403
  option :full_report, aliases: "-r", type: :boolean, desc: "Full report"
395
- option :return_value_results, aliases: "-R", type: :boolean, desc: "Use return value for results"
396
- option :summary_report, aliases: "-S", type: :boolean, desc: "Summary report"
397
- option :test_list, aliases: "-t", type: :string, default: "default", desc: "Tests to execute"
404
+ option :return_value_results, aliases: "-R", type: :boolean,
405
+ desc: "Use return value for results"
406
+ option :summary_report, aliases: "-S", type: :boolean,
407
+ desc: "Summary report"
408
+ option :profile, aliases: "-p", type: :string, default: "default",
409
+ desc: "Validation profile"
398
410
  option :table_report, aliases: "-T", type: :boolean, desc: "Tabular report"
399
411
  option :verbose, aliases: "-v", type: :boolean, desc: "Verbose output"
400
- option :suppress_warnings, aliases: "-W", type: :boolean, desc: "Suppress warnings"
412
+ option :suppress_warnings, aliases: "-W", type: :boolean,
413
+ desc: "Suppress warnings"
401
414
 
402
- def validate(font_file)
415
+ def validate(*font_files)
403
416
  if options[:list]
404
417
  list_available_tests
405
418
  return
406
419
  end
407
420
 
421
+ # Validate argument count
422
+ if font_files.empty?
423
+ raise(Thor::Error, "FONT_FILE is required unless using --list")
424
+ elsif font_files.size > 1
425
+ raise(Thor::Error,
426
+ "Too many arguments. validate accepts only one font file.\n" \
427
+ "To validate multiple files, run validate separately for each.\n" \
428
+ "To validate all fonts in a collection, use: fontisan validate collection.ttc")
429
+ end
430
+
431
+ font_file = font_files.first
432
+
408
433
  cmd = Commands::ValidateCommand.new(
409
434
  input: font_file,
410
- profile: options[:test_list],
435
+ profile: options[:profile],
411
436
  exclude: options[:exclude] || [],
412
437
  output: options[:output],
413
438
  format: options[:format].to_sym,
@@ -416,11 +441,18 @@ module Fontisan
416
441
  table_report: options[:table_report],
417
442
  verbose: options[:verbose],
418
443
  suppress_warnings: options[:suppress_warnings],
419
- return_value_results: options[:return_value_results]
444
+ return_value_results: options[:return_value_results],
420
445
  )
421
446
 
422
447
  exit cmd.run
423
- rescue => e
448
+ rescue Thor::Error => e
449
+ unless options[:quiet]
450
+ warn "ERROR: #{e.message}"
451
+ warn
452
+ help("validate")
453
+ end
454
+ exit 1
455
+ rescue StandardError => e
424
456
  error "Validation failed: #{e.message}"
425
457
  exit 1
426
458
  end
@@ -546,7 +578,7 @@ module Fontisan
546
578
  puts " Format: #{result[:format].upcase}"
547
579
  puts " Fonts: #{result[:num_fonts]}"
548
580
  puts " Size: #{format_size(result[:output_size] || result[:total_size])}"
549
- if result[:space_savings] && result[:space_savings].positive?
581
+ if result[:space_savings]&.positive?
550
582
  puts " Space saved: #{format_size(result[:space_savings])}"
551
583
  puts " Sharing: #{result[:statistics][:sharing_percentage]}%"
552
584
  end
@@ -86,7 +86,8 @@ module Fontisan
86
86
  map_size = 28 + 2 + 8 + (sfnt_binaries.size * 12)
87
87
 
88
88
  # Step 4: Build resource map
89
- resource_map = build_resource_map(sfnt_binaries, resource_data.bytesize, map_size)
89
+ resource_map = build_resource_map(sfnt_binaries,
90
+ resource_data.bytesize, map_size)
90
91
 
91
92
  # Step 5: Build header
92
93
  header = build_header(resource_data.bytesize, resource_map.bytesize)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Collection
5
+ # Shared logic for font collection classes
6
+ #
7
+ # This module provides common functionality for all collection types
8
+ # (TTC, OTC, dfont) to maintain DRY principles.
9
+ module SharedLogic
10
+ # Calculate table sharing statistics
11
+ #
12
+ # Analyzes which tables are shared between fonts and calculates
13
+ # space savings from deduplication.
14
+ #
15
+ # @param fonts [Array<TrueTypeFont, OpenTypeFont>] Array of fonts
16
+ # @return [Models::TableSharingInfo] Sharing statistics
17
+ def calculate_table_sharing_for_fonts(fonts)
18
+ require_relative "../models/table_sharing_info"
19
+
20
+ # Build table hash map (checksum -> size)
21
+ table_map = {}
22
+ total_table_size = 0
23
+
24
+ fonts.each do |font|
25
+ font.tables.each do |entry|
26
+ key = entry.checksum
27
+ size = entry.table_length
28
+ table_map[key] ||= size
29
+ total_table_size += size
30
+ end
31
+ end
32
+
33
+ # Count unique vs shared
34
+ unique_tables = table_map.size
35
+ total_tables = fonts.sum { |f| f.tables.length }
36
+ shared_tables = total_tables - unique_tables
37
+
38
+ # Calculate space saved
39
+ unique_size = table_map.values.sum
40
+ space_saved = total_table_size - unique_size
41
+
42
+ # Calculate sharing percentage
43
+ sharing_pct = total_tables.positive? ? (shared_tables.to_f / total_tables * 100).round(2) : 0.0
44
+
45
+ Models::TableSharingInfo.new(
46
+ shared_tables: shared_tables,
47
+ unique_tables: unique_tables,
48
+ sharing_percentage: sharing_pct,
49
+ space_saved_bytes: space_saved,
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
@@ -73,7 +73,7 @@ module Fontisan
73
73
  @instance_index = opts[:instance_index]
74
74
  @preserve_variation = opts[:preserve_variation]
75
75
  @preserve_hints = opts.fetch(:preserve_hints, false)
76
- @collection_target_format = opts.fetch(:target_format, 'preserve').to_s
76
+ @collection_target_format = opts.fetch(:target_format, "preserve").to_s
77
77
  @validate = !opts[:no_validate]
78
78
  end
79
79
 
@@ -196,7 +196,7 @@ module Fontisan
196
196
  output: @output_path,
197
197
  target_format: @collection_target_format,
198
198
  verbose: @options[:verbose],
199
- }
199
+ },
200
200
  )
201
201
 
202
202
  # Display results
@@ -245,8 +245,6 @@ module Fontisan
245
245
  :otc
246
246
  when ".dfont"
247
247
  :dfont
248
- else
249
- nil
250
248
  end
251
249
  end
252
250
  end
@@ -311,7 +311,7 @@ module Fontisan
311
311
  start_glyph_id: strike_rec.start_glyph_index,
312
312
  end_glyph_id: strike_rec.end_glyph_index,
313
313
  bit_depth: strike_rec.bit_depth,
314
- num_glyphs: strike_rec.glyph_range.size
314
+ num_glyphs: strike_rec.glyph_range.size,
315
315
  )
316
316
  end
317
317
  formats << "PNG" # CBDT typically contains PNG data
@@ -329,8 +329,8 @@ module Fontisan
329
329
  ppem: strike[:ppem],
330
330
  start_glyph_id: 0,
331
331
  end_glyph_id: strike[:num_glyphs] - 1,
332
- bit_depth: 32, # sbix is typically 32-bit
333
- num_glyphs: strike[:num_glyphs]
332
+ bit_depth: 32, # sbix is typically 32-bit
333
+ num_glyphs: strike[:num_glyphs],
334
334
  )
335
335
  end
336
336
  end
@@ -260,7 +260,8 @@ module Fontisan
260
260
  # @return [Symbol] Parsed format (:ttc, :otc, or :dfont)
261
261
  # @raise [ArgumentError] if format is invalid
262
262
  def parse_format(format)
263
- return format if format.is_a?(Symbol) && %i[ttc otc dfont].include?(format)
263
+ return format if format.is_a?(Symbol) && %i[ttc otc
264
+ dfont].include?(format)
264
265
 
265
266
  case format.to_s.downcase
266
267
  when "ttc"