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
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "conversion_strategy"
4
+ require_relative "../outline_extractor"
5
+ require_relative "../svg/font_generator"
6
+
7
+ module Fontisan
8
+ module Converters
9
+ # SVG font generator conversion strategy
10
+ #
11
+ # [`SvgGenerator`](lib/fontisan/converters/svg_generator.rb) implements
12
+ # the ConversionStrategy interface to convert TTF or OTF fonts to SVG
13
+ # font format for web use, inspection, or conversion purposes.
14
+ #
15
+ # SVG font generation process:
16
+ # 1. Extract font metadata from tables
17
+ # 2. Extract glyph outlines using OutlineExtractor
18
+ # 3. Get unicode mappings from cmap table
19
+ # 4. Get advance widths from hmtx table
20
+ # 5. Build glyph data map
21
+ # 6. Generate complete SVG XML using FontGenerator
22
+ #
23
+ # Note: SVG fonts are deprecated in favor of WOFF/WOFF2 but remain useful
24
+ # for fallback, conversion workflows, and font inspection.
25
+ #
26
+ # @example Convert TTF to SVG
27
+ # generator = SvgGenerator.new
28
+ # svg_xml = generator.convert(font)
29
+ # File.write('font.svg', svg_xml[:svg_xml])
30
+ class SvgGenerator
31
+ include ConversionStrategy
32
+
33
+ # Convert font to SVG format
34
+ #
35
+ # Returns a hash with :svg_xml key containing complete SVG font XML.
36
+ # This follows the same pattern as Woff2Encoder.
37
+ #
38
+ # @param font [TrueTypeFont, OpenTypeFont] Source font
39
+ # @param options [Hash] Conversion options
40
+ # @option options [Boolean] :pretty_print Pretty print XML (default: true)
41
+ # @option options [Array<Integer>] :glyph_ids Specific glyph IDs to include (default: all)
42
+ # @option options [Integer] :max_glyphs Maximum glyphs to include (default: all)
43
+ # @return [Hash] Hash with :svg_xml key containing SVG XML string
44
+ # @raise [Error] If conversion fails
45
+ def convert(font, options = {})
46
+ validate(font, :svg)
47
+
48
+ # Extract glyph data
49
+ glyph_data = extract_glyph_data(font, options)
50
+
51
+ # Generate SVG XML
52
+ generator = Svg::FontGenerator.new(font, glyph_data, options)
53
+ svg_xml = generator.generate
54
+
55
+ # Return in special format for ConvertCommand to handle
56
+ { svg_xml: svg_xml }
57
+ end
58
+
59
+ # Get list of supported conversions
60
+ #
61
+ # @return [Array<Array<Symbol>>] Supported conversion pairs
62
+ def supported_conversions
63
+ [
64
+ %i[ttf svg],
65
+ %i[otf svg],
66
+ ]
67
+ end
68
+
69
+ # Validate that conversion is possible
70
+ #
71
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
72
+ # @param target_format [Symbol] Target format
73
+ # @return [Boolean] True if valid
74
+ # @raise [Error] If conversion is not possible
75
+ def validate(font, target_format)
76
+ unless target_format == :svg
77
+ raise Fontisan::Error,
78
+ "SvgGenerator only supports conversion to svg, " \
79
+ "got: #{target_format}"
80
+ end
81
+
82
+ # Verify font has required tables
83
+ required_tables = %w[head hhea maxp cmap]
84
+ required_tables.each do |tag|
85
+ unless font.table(tag)
86
+ raise Fontisan::Error,
87
+ "Font is missing required table: #{tag}"
88
+ end
89
+ end
90
+
91
+ # Verify font has either glyf or CFF table
92
+ unless font.has_table?("glyf") || font.has_table?("CFF ") || font.has_table?("CFF2")
93
+ raise Fontisan::Error,
94
+ "Font must have either glyf or CFF/CFF2 table"
95
+ end
96
+
97
+ true
98
+ end
99
+
100
+ private
101
+
102
+ # Extract glyph data from font
103
+ #
104
+ # @param font [TrueTypeFont, OpenTypeFont] Source font
105
+ # @param options [Hash] Extraction options
106
+ # @return [Hash] Glyph data map (glyph_id => {outline, unicode, name, advance})
107
+ def extract_glyph_data(font, options = {})
108
+ extractor = OutlineExtractor.new(font)
109
+ cmap = font.table("cmap")
110
+ hmtx = font.table("hmtx")
111
+ post = font.table("post")
112
+ maxp = font.table("maxp")
113
+
114
+ glyph_data = {}
115
+ num_glyphs = maxp&.num_glyphs || 0
116
+ max_glyphs = options[:max_glyphs] || num_glyphs
117
+
118
+ # Get unicode mappings
119
+ unicode_map = build_unicode_map(cmap)
120
+
121
+ # Extract specified or all glyphs
122
+ glyph_ids = options[:glyph_ids] || (0...num_glyphs).to_a
123
+ glyph_ids = glyph_ids.take(max_glyphs) if max_glyphs
124
+
125
+ glyph_ids.each do |glyph_id|
126
+ next if glyph_id >= num_glyphs
127
+
128
+ # Extract outline
129
+ outline = extractor.extract(glyph_id)
130
+
131
+ # Get advance width
132
+ advance = extract_advance_width(hmtx, glyph_id)
133
+
134
+ # Get unicode character
135
+ unicode = unicode_map[glyph_id]
136
+
137
+ # Get glyph name
138
+ glyph_name = extract_glyph_name(post, glyph_id)
139
+
140
+ glyph_data[glyph_id] = {
141
+ outline: outline,
142
+ unicode: unicode,
143
+ name: glyph_name,
144
+ advance: advance,
145
+ }
146
+ rescue StandardError => e
147
+ warn "Failed to extract glyph #{glyph_id}: #{e.message}"
148
+ next
149
+ end
150
+
151
+ glyph_data
152
+ end
153
+
154
+ # Build unicode to glyph ID map from cmap table
155
+ #
156
+ # @param cmap [Tables::Cmap, nil] Cmap table
157
+ # @return [Hash<Integer, String>] Map of glyph_id to unicode character
158
+ def build_unicode_map(cmap)
159
+ return {} unless cmap
160
+
161
+ unicode_map = {}
162
+
163
+ # Get best cmap subtable (prefer Unicode BMP or full)
164
+ subtable = find_best_cmap_subtable(cmap)
165
+ return {} unless subtable
166
+
167
+ # Build reverse map: glyph_id => unicode
168
+ subtable.each do |code_point, glyph_id|
169
+ # Store first unicode for each glyph
170
+ next if unicode_map[glyph_id]
171
+
172
+ unicode_map[glyph_id] = [code_point].pack("U")
173
+ rescue StandardError
174
+ # Skip invalid code points
175
+ next
176
+ end
177
+
178
+ unicode_map
179
+ rescue StandardError => e
180
+ warn "Failed to build unicode map: #{e.message}"
181
+ {}
182
+ end
183
+
184
+ # Find best cmap subtable for unicode mapping
185
+ #
186
+ # @param cmap [Tables::Cmap] Cmap table
187
+ # @return [Hash, nil] Subtable or nil
188
+ def find_best_cmap_subtable(cmap)
189
+ # Try Unicode BMP (platform 3, encoding 1) - Windows Unicode BMP
190
+ subtable = cmap.subtable(3, 1)
191
+ return subtable if subtable
192
+
193
+ # Try Unicode full (platform 3, encoding 10) - Windows Unicode full
194
+ subtable = cmap.subtable(3, 10)
195
+ return subtable if subtable
196
+
197
+ # Try Unicode (platform 0, encoding 3) - Unicode 2.0+ BMP
198
+ subtable = cmap.subtable(0, 3)
199
+ return subtable if subtable
200
+
201
+ # Try Unicode (platform 0, encoding 4) - Unicode 2.0+ full
202
+ subtable = cmap.subtable(0, 4)
203
+ return subtable if subtable
204
+
205
+ # Fallback to any available subtable
206
+ cmap.subtables.first
207
+ rescue StandardError
208
+ nil
209
+ end
210
+
211
+ # Extract advance width for glyph
212
+ #
213
+ # @param hmtx [Tables::Hmtx, nil] Hmtx table
214
+ # @param glyph_id [Integer] Glyph ID
215
+ # @return [Integer] Advance width
216
+ def extract_advance_width(hmtx, glyph_id)
217
+ return 0 unless hmtx
218
+
219
+ advance = hmtx.advance_width_for(glyph_id)
220
+ return 0 unless advance
221
+
222
+ advance
223
+ rescue StandardError
224
+ 0
225
+ end
226
+
227
+ # Extract glyph name from post table
228
+ #
229
+ # @param post [Tables::Post, nil] Post table
230
+ # @param glyph_id [Integer] Glyph ID
231
+ # @return [String, nil] Glyph name or nil
232
+ def extract_glyph_name(post, glyph_id)
233
+ return nil unless post
234
+
235
+ name = post.glyph_name_for(glyph_id)
236
+ return nil if name.nil? || name.empty?
237
+
238
+ name
239
+ rescue StandardError
240
+ nil
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "conversion_strategy"
4
+
5
+ module Fontisan
6
+ module Converters
7
+ # Strategy for same-format font operations (copy/optimize)
8
+ #
9
+ # [`TableCopier`](lib/fontisan/converters/table_copier.rb) handles
10
+ # conversions where the source and target formats are the same.
11
+ # This is useful for:
12
+ # - Creating a clean copy of a font
13
+ # - Re-ordering tables
14
+ # - Removing corruption
15
+ # - Normalizing structure
16
+ #
17
+ # The strategy simply copies all tables from the source font
18
+ # and reassembles them with proper checksums and offsets.
19
+ #
20
+ # @example Using TableCopier
21
+ # copier = Fontisan::Converters::TableCopier.new
22
+ # tables = copier.convert(font)
23
+ # binary = FontWriter.write_font(tables)
24
+ class TableCopier
25
+ include ConversionStrategy
26
+
27
+ # Convert font by copying all tables
28
+ #
29
+ # @param font [TrueTypeFont, OpenTypeFont] Source font
30
+ # @param options [Hash] Conversion options (currently unused)
31
+ # @return [Hash<String, String>] Map of table tags to binary data
32
+ def convert(font, _options = {})
33
+ raise ArgumentError, "Font cannot be nil" if font.nil?
34
+
35
+ unless font.respond_to?(:tables)
36
+ raise ArgumentError, "Font must respond to :tables"
37
+ end
38
+
39
+ unless font.respond_to?(:table_data)
40
+ raise ArgumentError, "Font must respond to :table_data"
41
+ end
42
+
43
+ target_format = detect_format(font)
44
+ validate(font, target_format)
45
+
46
+ tables = {}
47
+
48
+ # Copy all tables from source font
49
+ font.table_data.each do |tag, data|
50
+ tables[tag] = data if data
51
+ end
52
+
53
+ tables
54
+ end
55
+
56
+ # Get supported conversions
57
+ #
58
+ # Supports same-format conversions for TTF and OTF
59
+ #
60
+ # @return [Array<Array<Symbol>>] Supported conversion pairs
61
+ def supported_conversions
62
+ [
63
+ %i[ttf ttf],
64
+ %i[otf otf],
65
+ ]
66
+ end
67
+
68
+ # Validate font for copying
69
+ #
70
+ # @param font [TrueTypeFont, OpenTypeFont] Font to validate
71
+ # @param target_format [Symbol] Target format (same as source for copier)
72
+ # @return [Boolean] True if valid
73
+ # @raise [ArgumentError] If font is invalid
74
+ # @raise [Error] If formats don't match
75
+ def validate(font, target_format)
76
+ raise ArgumentError, "Font cannot be nil" if font.nil?
77
+
78
+ unless font.respond_to?(:tables)
79
+ raise ArgumentError, "Font must respond to :tables"
80
+ end
81
+
82
+ unless font.respond_to?(:table_data)
83
+ raise ArgumentError, "Font must respond to :table_data"
84
+ end
85
+
86
+ # Detect source format and verify it matches target
87
+ source_format = detect_format(font)
88
+ unless source_format == target_format
89
+ raise Fontisan::Error,
90
+ "TableCopier requires source and target formats to match. " \
91
+ "Got source: #{source_format}, target: #{target_format}"
92
+ end
93
+
94
+ true
95
+ end
96
+
97
+ private
98
+
99
+ # Detect font format from tables
100
+ #
101
+ # @param font [TrueTypeFont, OpenTypeFont] Font to detect
102
+ # @return [Symbol] Format (:ttf or :otf)
103
+ def detect_format(font)
104
+ # Check for CFF/CFF2 tables (OpenType/CFF)
105
+ if font.has_table?("CFF ") || font.has_table?("CFF2")
106
+ :otf
107
+ # Check for glyf table (TrueType)
108
+ elsif font.has_table?("glyf")
109
+ :ttf
110
+ else
111
+ raise Fontisan::Error,
112
+ "Cannot detect font format: missing both CFF and glyf tables"
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end