fontisan 0.2.5 → 0.2.7

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "base_command"
4
4
  require_relative "../collection/builder"
5
+ require_relative "../collection/dfont_builder"
5
6
  require_relative "../font_loader"
6
7
 
7
8
  module Fontisan
@@ -11,6 +12,7 @@ module Fontisan
11
12
  # This command provides CLI access to font collection creation functionality.
12
13
  # It loads multiple font files and combines them into a single TTC (TrueType Collection)
13
14
  # or OTC (OpenType Collection) file with shared table deduplication to save space.
15
+ # It also supports creating dfont (Apple Data Fork Font) suitcases.
14
16
  #
15
17
  # @example Pack fonts into TTC
16
18
  # command = PackCommand.new(
@@ -30,13 +32,21 @@ module Fontisan
30
32
  # analyze: true
31
33
  # )
32
34
  # result = command.run
35
+ #
36
+ # @example Pack into dfont
37
+ # command = PackCommand.new(
38
+ # ['font1.ttf', 'font2.otf'],
39
+ # output: 'family.dfont',
40
+ # format: :dfont
41
+ # )
42
+ # result = command.run
33
43
  class PackCommand
34
44
  # Initialize pack command
35
45
  #
36
46
  # @param font_paths [Array<String>] Paths to input font files
37
47
  # @param options [Hash] Command options
38
48
  # @option options [String] :output Output file path (required)
39
- # @option options [Symbol, String] :format Format type (:ttc or :otc, default: :ttc)
49
+ # @option options [Symbol, String] :format Format type (:ttc, :otc, or :dfont, default: auto-detect)
40
50
  # @option options [Boolean] :optimize Enable table sharing optimization (default: true)
41
51
  # @option options [Boolean] :analyze Show analysis report before building (default: false)
42
52
  # @option options [Boolean] :verbose Enable verbose output (default: false)
@@ -45,7 +55,7 @@ module Fontisan
45
55
  @font_paths = font_paths
46
56
  @options = options
47
57
  @output_path = options[:output]
48
- @format = parse_format(options[:format] || :ttc)
58
+ @format = options[:format] ? parse_format(options[:format]) : nil
49
59
  @optimize = options.fetch(:optimize, true)
50
60
  @analyze = options.fetch(:analyze, false)
51
61
  @verbose = options.fetch(:verbose, false)
@@ -55,16 +65,16 @@ module Fontisan
55
65
 
56
66
  # Execute the pack command
57
67
  #
58
- # Loads all fonts, analyzes tables, and creates a TTC/OTC collection.
68
+ # Loads all fonts, analyzes tables, and creates a TTC/OTC/dfont collection.
59
69
  # Optionally displays analysis before building.
60
70
  #
61
71
  # @return [Hash] Result information with:
62
72
  # - :output [String] - Output file path
63
73
  # - :output_size [Integer] - Output file size in bytes
64
74
  # - :num_fonts [Integer] - Number of fonts packed
65
- # - :format [Symbol] - Collection format (:ttc or :otc)
66
- # - :space_savings [Integer] - Bytes saved through sharing
67
- # - :sharing_percentage [Float] - Percentage of tables shared
75
+ # - :format [Symbol] - Collection format (:ttc, :otc, or :dfont)
76
+ # - :space_savings [Integer] - Bytes saved through sharing (TTC/OTC only)
77
+ # - :sharing_percentage [Float] - Percentage of tables shared (TTC/OTC only)
68
78
  # - :analysis [Hash] - Analysis report (if analyze option enabled)
69
79
  # @raise [ArgumentError] if options are invalid
70
80
  # @raise [Fontisan::Error] if packing fails
@@ -74,6 +84,46 @@ module Fontisan
74
84
  # Load all fonts
75
85
  fonts = load_fonts
76
86
 
87
+ # Auto-detect format if not specified
88
+ @format ||= auto_detect_format(fonts)
89
+ puts "Auto-detected format: #{@format}" if @verbose && !@options[:format]
90
+
91
+ # Build collection based on format
92
+ if @format == :dfont
93
+ build_dfont(fonts)
94
+ else
95
+ build_ttc_otc(fonts)
96
+ end
97
+ rescue Fontisan::Error => e
98
+ raise Fontisan::Error, "Collection packing failed: #{e.message}"
99
+ rescue StandardError => e
100
+ raise Fontisan::Error, "Unexpected error during packing: #{e.message}"
101
+ end
102
+
103
+ private
104
+
105
+ # Build dfont collection
106
+ #
107
+ # @param fonts [Array] Loaded fonts
108
+ # @return [Hash] Build result
109
+ def build_dfont(fonts)
110
+ puts "Building dfont suitcase..." if @verbose
111
+
112
+ builder = Collection::DfontBuilder.new(fonts)
113
+ result = builder.build_to_file(@output_path)
114
+
115
+ if @verbose
116
+ display_dfont_results(result)
117
+ end
118
+
119
+ result
120
+ end
121
+
122
+ # Build TTC/OTC collection
123
+ #
124
+ # @param fonts [Array] Loaded fonts
125
+ # @return [Hash] Build result
126
+ def build_ttc_otc(fonts)
77
127
  # Create builder
78
128
  builder = Collection::Builder.new(fonts, {
79
129
  format: @format,
@@ -98,13 +148,53 @@ module Fontisan
98
148
  end
99
149
 
100
150
  result
101
- rescue Fontisan::Error => e
102
- raise Fontisan::Error, "Collection packing failed: #{e.message}"
103
- rescue StandardError => e
104
- raise Fontisan::Error, "Unexpected error during packing: #{e.message}"
105
151
  end
106
152
 
107
- private
153
+ # Auto-detect collection format based on fonts
154
+ #
155
+ # @param fonts [Array<TrueTypeFont, OpenTypeFont>] Loaded fonts
156
+ # @return [Symbol] Detected format (:ttc, :otc, or :dfont)
157
+ def auto_detect_format(fonts)
158
+ # Check output extension first
159
+ ext = File.extname(@output_path).downcase
160
+ return :ttc if ext == ".ttc"
161
+ return :otc if ext == ".otc"
162
+ return :dfont if ext == ".dfont"
163
+
164
+ # Detect based on font types
165
+ has_truetype = fonts.any? { |f| truetype_font?(f) }
166
+ has_opentype = fonts.any? { |f| opentype_font?(f) }
167
+
168
+ if has_truetype && !has_opentype
169
+ :ttc # All TrueType
170
+ elsif has_opentype
171
+ :otc # Has OpenType/CFF fonts
172
+ else
173
+ :ttc # Default to TTC
174
+ end
175
+ end
176
+
177
+ # Check if font is TrueType
178
+ #
179
+ # @param font [Object] Font object
180
+ # @return [Boolean]
181
+ def truetype_font?(font)
182
+ return false unless font.respond_to?(:header)
183
+
184
+ sfnt = font.header.sfnt_version
185
+ [0x00010000, 0x74727565].include?(sfnt) # 0x74727565 = 'true'
186
+ end
187
+
188
+ # Check if font is OpenType/CFF
189
+ #
190
+ # @param font [Object] Font object
191
+ # @return [Boolean]
192
+ def opentype_font?(font)
193
+ return false unless font.respond_to?(:header)
194
+
195
+ sfnt = font.header.sfnt_version
196
+ sfnt == 0x4F54544F # 'OTTO'
197
+ end
108
198
 
109
199
  # Validate command options
110
200
  #
@@ -125,17 +215,19 @@ module Fontisan
125
215
  "Collection requires at least 2 fonts, got #{@font_paths.size}"
126
216
  end
127
217
 
128
- # Validate format
129
- unless %i[ttc otc].include?(@format)
218
+ # Validate format if specified
219
+ if @format && !%i[ttc otc dfont].include?(@format)
130
220
  raise ArgumentError,
131
- "Invalid format: #{@format}. Must be :ttc or :otc"
221
+ "Invalid format: #{@format}. Must be :ttc, :otc, or :dfont"
132
222
  end
133
223
 
134
- # Check output extension matches format
135
- ext = File.extname(@output_path).downcase
136
- expected_ext = @format == :ttc ? ".ttc" : ".otc"
137
- if ext != expected_ext
138
- warn "Warning: Output extension '#{ext}' doesn't match format '#{@format}' (expected '#{expected_ext}')"
224
+ # Warn if output extension doesn't match format (if format specified)
225
+ if @format
226
+ ext = File.extname(@output_path).downcase
227
+ expected_ext = ".#{@format}"
228
+ if ext != expected_ext
229
+ warn "Warning: Output extension '#{ext}' doesn't match format '#{@format}' (expected '#{expected_ext}')"
230
+ end
139
231
  end
140
232
  end
141
233
 
@@ -165,19 +257,21 @@ module Fontisan
165
257
  # Parse format option
166
258
  #
167
259
  # @param format [Symbol, String] Format option
168
- # @return [Symbol] Parsed format (:ttc or :otc)
260
+ # @return [Symbol] Parsed format (:ttc, :otc, or :dfont)
169
261
  # @raise [ArgumentError] if format is invalid
170
262
  def parse_format(format)
171
- return format if format.is_a?(Symbol) && %i[ttc otc].include?(format)
263
+ return format if format.is_a?(Symbol) && %i[ttc otc dfont].include?(format)
172
264
 
173
265
  case format.to_s.downcase
174
266
  when "ttc"
175
267
  :ttc
176
268
  when "otc"
177
269
  :otc
270
+ when "dfont"
271
+ :dfont
178
272
  else
179
273
  raise ArgumentError,
180
- "Invalid format: #{format}. Must be 'ttc' or 'otc'"
274
+ "Invalid format: #{format}. Must be 'ttc', 'otc', or 'dfont'"
181
275
  end
182
276
  end
183
277
 
@@ -223,6 +317,19 @@ module Fontisan
223
317
  puts ""
224
318
  end
225
319
 
320
+ # Display dfont build results
321
+ #
322
+ # @param result [Hash] Build result
323
+ # @return [void]
324
+ def display_dfont_results(result)
325
+ puts "\n=== dfont Suitcase Created ==="
326
+ puts "Output: #{result[:output_path]}"
327
+ puts "Format: #{result[:format].upcase}"
328
+ puts "Fonts: #{result[:num_fonts]}"
329
+ puts "Size: #{format_bytes(result[:total_size])}"
330
+ puts ""
331
+ end
332
+
226
333
  # Format bytes for display
227
334
  #
228
335
  # @param bytes [Integer] Byte count
@@ -177,6 +177,91 @@ conversions:
177
177
  Same-format copy operation. Decompresses and re-compresses WOFF2 data
178
178
  with Brotli, useful for normalizing compression or updating metadata.
179
179
 
180
+ # Collection format conversions (TTC/OTC/dfont)
181
+ # Same-format collection operations
182
+ - from: ttc
183
+ to: ttc
184
+ strategy: collection_copier
185
+ description: "Copy TrueType Collection"
186
+ status: implemented
187
+ notes: >
188
+ Same-format copy operation for TTC files. Preserves all fonts and
189
+ table sharing structure.
190
+
191
+ - from: otc
192
+ to: otc
193
+ strategy: collection_copier
194
+ description: "Copy OpenType Collection"
195
+ status: implemented
196
+ notes: >
197
+ Same-format copy operation for OTC files. Preserves all fonts and
198
+ table sharing structure.
199
+
200
+ - from: dfont
201
+ to: dfont
202
+ strategy: collection_copier
203
+ description: "Copy Apple dfont suitcase"
204
+ status: implemented
205
+ notes: >
206
+ Same-format copy operation for dfont files. Preserves all fonts in
207
+ the suitcase.
208
+
209
+ # Cross-collection conversions
210
+ - from: ttc
211
+ to: otc
212
+ strategy: collection_converter
213
+ description: "Convert TrueType Collection to OpenType Collection"
214
+ status: implemented
215
+ notes: >
216
+ Unpacks TTC, converts all TrueType fonts to OpenType/CFF format,
217
+ then repacks as OTC with table sharing optimization.
218
+
219
+ - from: otc
220
+ to: ttc
221
+ strategy: collection_converter
222
+ description: "Convert OpenType Collection to TrueType Collection"
223
+ status: implemented
224
+ notes: >
225
+ Unpacks OTC, converts all OpenType/CFF fonts to TrueType format,
226
+ then repacks as TTC with table sharing optimization.
227
+
228
+ - from: ttc
229
+ to: dfont
230
+ strategy: collection_converter
231
+ description: "Convert TrueType Collection to dfont suitcase"
232
+ status: implemented
233
+ notes: >
234
+ Unpacks TTC and repacks as Apple dfont suitcase. No outline conversion
235
+ needed as dfont supports TrueType fonts.
236
+
237
+ - from: otc
238
+ to: dfont
239
+ strategy: collection_converter
240
+ description: "Convert OpenType Collection to dfont suitcase"
241
+ status: implemented
242
+ notes: >
243
+ Unpacks OTC and repacks as Apple dfont suitcase. No outline conversion
244
+ needed as dfont supports OpenType/CFF fonts.
245
+
246
+ - from: dfont
247
+ to: ttc
248
+ strategy: collection_converter
249
+ description: "Convert dfont suitcase to TrueType Collection"
250
+ status: implemented
251
+ notes: >
252
+ Unpacks dfont suitcase and repacks as TTC. If dfont contains OpenType
253
+ fonts, they are converted to TrueType format. Only valid if all fonts
254
+ can be converted to TrueType.
255
+
256
+ - from: dfont
257
+ to: otc
258
+ strategy: collection_converter
259
+ description: "Convert dfont suitcase to OpenType Collection"
260
+ status: implemented
261
+ notes: >
262
+ Unpacks dfont suitcase and repacks as OTC. If dfont contains TrueType
263
+ fonts, they are converted to OpenType/CFF format.
264
+
180
265
  # Conversion compatibility matrix
181
266
  #
182
267
  # This section documents which source features are preserved in conversions.
@@ -247,4 +332,93 @@ compatibility:
247
332
  use_cases:
248
333
  - fallback: Emergency fallback for very old browsers
249
334
  - conversion: Intermediate format for font conversion workflows
250
- - inspection: Easy-to-read format for font inspection
335
+ - inspection: Easy-to-read format for font inspection
336
+
337
+ collection_conversions:
338
+ ttc_to_otc:
339
+ preserves:
340
+ - all_fonts: All fonts in collection
341
+ - font_metrics: Font metrics for each font
342
+ - layout_features: Layout features for each font
343
+ limitations:
344
+ - outline_conversion_required: TrueType to CFF conversion applied
345
+ - hinting_loss: TrueType hinting not preserved in CFF
346
+ notes: >
347
+ Converts all TrueType fonts to OpenType/CFF format using the standard
348
+ TTF→OTF conversion pipeline, then repacks with table sharing.
349
+
350
+ otc_to_ttc:
351
+ preserves:
352
+ - all_fonts: All fonts in collection
353
+ - font_metrics: Font metrics for each font
354
+ - layout_features: Layout features for each font
355
+ limitations:
356
+ - outline_conversion_required: CFF to TrueType conversion applied
357
+ - hinting_loss: CFF hints not preserved in TrueType
358
+ - approximation: Cubic to quadratic curve approximation
359
+ notes: >
360
+ Converts all OpenType/CFF fonts to TrueType format using the standard
361
+ OTF→TTF conversion pipeline, then repacks with table sharing.
362
+
363
+ to_dfont:
364
+ preserves:
365
+ - all_fonts: All fonts in collection
366
+ - original_formats: Preserves original outline formats (TTF or OTF)
367
+ - font_metrics: All font metrics
368
+ - layout_features: All layout features
369
+ benefits:
370
+ - mixed_formats: dfont supports both TrueType and OpenType fonts
371
+ - mac_compatibility: Native Mac OS X suitcase format
372
+ limitations:
373
+ - platform_specific: Mac OS X only, not cross-platform
374
+ - no_table_sharing: dfont doesn't deduplicate tables like TTC/OTC
375
+ notes: >
376
+ Repackages fonts into Apple dfont suitcase format. No outline conversion
377
+ needed as dfont supports any SFNT font type (TTF, OTF, or mixed).
378
+
379
+ from_dfont:
380
+ preserves:
381
+ - all_fonts: All fonts in suitcase
382
+ - font_metrics: All font metrics
383
+ - layout_features: All layout features
384
+ limitations:
385
+ - conversion_may_be_required: Outline conversion if target requires specific format
386
+ - hinting_loss: If outline conversion occurs
387
+ benefits:
388
+ - cross_platform: TTC/OTC work on all platforms
389
+ - table_sharing: TTC/OTC optimize with table deduplication
390
+ notes: >
391
+ Extracts fonts from dfont suitcase and repacks into TTC or OTC.
392
+ Outline conversion may occur depending on target format requirements.
393
+
394
+ # Collection format rules
395
+ collection_rules:
396
+ ttc:
397
+ required_format: truetype
398
+ allows_mixed: false
399
+ spec_compliance: opentype_spec
400
+ description: >
401
+ TrueType Collection per OpenType specification. Contains only TrueType
402
+ fonts (glyf/loca tables). CFF fonts are not allowed.
403
+
404
+ otc:
405
+ required_format: cff_preferred
406
+ allows_mixed: true
407
+ spec_compliance: opentype_1.8
408
+ description: >
409
+ OpenType Collection per OpenType 1.8 specification. Requires at least
410
+ one CFF font. Fontisan extension allows mixed TTF+OTF for flexibility.
411
+
412
+ dfont:
413
+ required_format: any_sfnt
414
+ allows_mixed: true
415
+ spec_compliance: apple_proprietary
416
+ description: >
417
+ Apple dfont (Data Fork Font) suitcase. Contains Mac font suitcase
418
+ resources (FOND, NFNT, sfnt). Supports any SFNT fonts (TrueType,
419
+ OpenType/CFF, or mixed). Mac OS X specific, not cross-platform.
420
+
421
+ prohibited:
422
+ - web_fonts_in_collections: >
423
+ WOFF and WOFF2 fonts cannot be included in collections (TTC, OTC, or dfont).
424
+ Web fonts are designed for single-font web delivery only.
@@ -25,6 +25,14 @@ module Fontisan
25
25
  # SFNT version for OpenType fonts with CFF outlines ('OTTO')
26
26
  SFNT_VERSION_OTTO = 0x4F54544F
27
27
 
28
+ # Apple 'true' TrueType signature (alternate to 0x00010000)
29
+ SFNT_VERSION_TRUE = 0x74727965 # 'true' in ASCII
30
+
31
+ # dfont resource fork signatures
32
+ DFONT_RESOURCE_HEADER = "\x00\x00\x01\x00"
33
+ SFNT_RESOURCE_TYPE = "sfnt"
34
+ FOND_RESOURCE_TYPE = "FOND"
35
+
28
36
  # Head table tag identifier.
29
37
  # The 'head' table contains global font header information including
30
38
  # the checksum adjustment field.