fontisan 0.1.0 → 0.2.1

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 (214) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +672 -69
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1477 -297
  6. data/Rakefile +63 -41
  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 +364 -4
  12. data/lib/fontisan/collection/builder.rb +341 -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 +317 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +24 -1
  18. data/lib/fontisan/commands/convert_command.rb +218 -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 +286 -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 +203 -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 +79 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +408 -0
  39. data/lib/fontisan/converters/outline_converter.rb +998 -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 +122 -15
  57. data/lib/fontisan/font_writer.rb +302 -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 +310 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
  65. data/lib/fontisan/loading_modes.rb +115 -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 +405 -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 +321 -19
  88. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  89. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  90. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  91. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  92. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  93. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  94. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  95. data/lib/fontisan/outline_extractor.rb +423 -0
  96. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  97. data/lib/fontisan/pipeline/output_writer.rb +154 -0
  98. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  99. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  100. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  101. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  102. data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
  103. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  104. data/lib/fontisan/subset/builder.rb +268 -0
  105. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  106. data/lib/fontisan/subset/options.rb +142 -0
  107. data/lib/fontisan/subset/profile.rb +152 -0
  108. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  109. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  110. data/lib/fontisan/svg/font_generator.rb +264 -0
  111. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  112. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  113. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  114. data/lib/fontisan/tables/cff/charset.rb +282 -0
  115. data/lib/fontisan/tables/cff/charstring.rb +934 -0
  116. data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
  117. data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
  118. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  119. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  120. data/lib/fontisan/tables/cff/dict.rb +351 -0
  121. data/lib/fontisan/tables/cff/dict_builder.rb +257 -0
  122. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  123. data/lib/fontisan/tables/cff/header.rb +102 -0
  124. data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
  125. data/lib/fontisan/tables/cff/index.rb +237 -0
  126. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  127. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  128. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  129. data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
  130. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  131. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  132. data/lib/fontisan/tables/cff.rb +489 -0
  133. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  134. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  135. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  136. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
  137. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  138. data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
  139. data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
  140. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  141. data/lib/fontisan/tables/cff2.rb +346 -0
  142. data/lib/fontisan/tables/cvar.rb +203 -0
  143. data/lib/fontisan/tables/fvar.rb +2 -2
  144. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  145. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  146. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  147. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  148. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  149. data/lib/fontisan/tables/glyf.rb +235 -0
  150. data/lib/fontisan/tables/gvar.rb +231 -0
  151. data/lib/fontisan/tables/hhea.rb +124 -0
  152. data/lib/fontisan/tables/hmtx.rb +287 -0
  153. data/lib/fontisan/tables/hvar.rb +191 -0
  154. data/lib/fontisan/tables/loca.rb +322 -0
  155. data/lib/fontisan/tables/maxp.rb +192 -0
  156. data/lib/fontisan/tables/mvar.rb +185 -0
  157. data/lib/fontisan/tables/name.rb +99 -30
  158. data/lib/fontisan/tables/variation_common.rb +346 -0
  159. data/lib/fontisan/tables/vvar.rb +234 -0
  160. data/lib/fontisan/true_type_collection.rb +156 -2
  161. data/lib/fontisan/true_type_font.rb +321 -20
  162. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  163. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  164. data/lib/fontisan/utilities/checksum_calculator.rb +60 -0
  165. data/lib/fontisan/utils/thread_pool.rb +134 -0
  166. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  167. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  168. data/lib/fontisan/validation/structure_validator.rb +198 -0
  169. data/lib/fontisan/validation/table_validator.rb +158 -0
  170. data/lib/fontisan/validation/validator.rb +152 -0
  171. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  172. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  173. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  174. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  175. data/lib/fontisan/variable/instancer.rb +344 -0
  176. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  177. data/lib/fontisan/variable/region_matcher.rb +208 -0
  178. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  179. data/lib/fontisan/variable/table_updater.rb +219 -0
  180. data/lib/fontisan/variation/blend_applier.rb +199 -0
  181. data/lib/fontisan/variation/cache.rb +298 -0
  182. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  183. data/lib/fontisan/variation/converter.rb +375 -0
  184. data/lib/fontisan/variation/data_extractor.rb +86 -0
  185. data/lib/fontisan/variation/delta_applier.rb +266 -0
  186. data/lib/fontisan/variation/delta_parser.rb +228 -0
  187. data/lib/fontisan/variation/inspector.rb +275 -0
  188. data/lib/fontisan/variation/instance_generator.rb +273 -0
  189. data/lib/fontisan/variation/instance_writer.rb +341 -0
  190. data/lib/fontisan/variation/interpolator.rb +231 -0
  191. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  192. data/lib/fontisan/variation/optimizer.rb +418 -0
  193. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  194. data/lib/fontisan/variation/region_matcher.rb +221 -0
  195. data/lib/fontisan/variation/subsetter.rb +463 -0
  196. data/lib/fontisan/variation/table_accessor.rb +105 -0
  197. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  198. data/lib/fontisan/variation/validator.rb +345 -0
  199. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  200. data/lib/fontisan/variation/variation_context.rb +211 -0
  201. data/lib/fontisan/variation/variation_preserver.rb +288 -0
  202. data/lib/fontisan/version.rb +1 -1
  203. data/lib/fontisan/version.rb.orig +9 -0
  204. data/lib/fontisan/woff2/directory.rb +257 -0
  205. data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
  206. data/lib/fontisan/woff2/header.rb +101 -0
  207. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  208. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  209. data/lib/fontisan/woff2_font.rb +717 -0
  210. data/lib/fontisan/woff_font.rb +488 -0
  211. data/lib/fontisan.rb +132 -0
  212. data/scripts/compare_stack_aware.rb +187 -0
  213. data/scripts/measure_optimization.rb +141 -0
  214. metadata +234 -4
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "variation_context"
4
+ require_relative "../error"
5
+
6
+ module Fontisan
7
+ module Variation
8
+ # Preserves variation data when converting between compatible font formats
9
+ #
10
+ # [`VariationPreserver`](lib/fontisan/variation/variation_preserver.rb)
11
+ # copies variation tables from source to target font during format
12
+ # conversion. It handles:
13
+ # - Common variation tables (fvar, avar, STAT) - shared by all variable fonts
14
+ # - Format-specific tables (gvar for TTF, CFF2 for OTF)
15
+ # - Metrics variation tables (HVAR, VVAR, MVAR)
16
+ # - Table checksum updates
17
+ # - Validation of table consistency
18
+ #
19
+ # **Use Cases:**
20
+ #
21
+ # 1. **Variable TTF → Variable WOFF**: Preserve all gvar-based variation
22
+ # 2. **Variable OTF → Variable WOFF**: Preserve all CFF2-based variation
23
+ # 3. **Variable TTF → Variable OTF**: Copy common tables (fvar, avar, STAT)
24
+ # but variation data conversion handled by Converter
25
+ # 4. **Variable OTF → Variable TTF**: Copy common tables (fvar, avar, STAT)
26
+ # but variation data conversion handled by Converter
27
+ #
28
+ # **Preserved Tables:**
29
+ #
30
+ # Common (all variable fonts):
31
+ # - fvar (Font Variations)
32
+ # - avar (Axis Variations, optional)
33
+ # - STAT (Style Attributes)
34
+ #
35
+ # TrueType-specific:
36
+ # - gvar (Glyph Variations)
37
+ # - cvar (CVT Variations, optional)
38
+ #
39
+ # CFF2-specific:
40
+ # - CFF2 (with blend operators)
41
+ #
42
+ # Metrics (optional):
43
+ # - HVAR (Horizontal Metrics Variations)
44
+ # - VVAR (Vertical Metrics Variations)
45
+ # - MVAR (Metrics Variations)
46
+ #
47
+ # @example Preserve variation when converting TTF to WOFF
48
+ # preserver = VariationPreserver.new(ttf_font, woff_tables)
49
+ # preserved_tables = preserver.preserve
50
+ #
51
+ # @example Preserve only common tables for outline conversion
52
+ # preserver = VariationPreserver.new(ttf_font, otf_tables,
53
+ # preserve_format_specific: false)
54
+ # preserved_tables = preserver.preserve
55
+ class VariationPreserver
56
+ # Common variation tables present in all variable fonts
57
+ COMMON_TABLES = %w[fvar avar STAT].freeze
58
+
59
+ # TrueType-specific variation tables
60
+ TRUETYPE_TABLES = %w[gvar cvar].freeze
61
+
62
+ # CFF2-specific variation tables
63
+ CFF2_TABLES = %w[CFF2].freeze
64
+
65
+ # Metrics variation tables
66
+ METRICS_TABLES = %w[HVAR VVAR MVAR].freeze
67
+
68
+ # All variation-related tables
69
+ ALL_VARIATION_TABLES = (COMMON_TABLES + TRUETYPE_TABLES +
70
+ CFF2_TABLES + METRICS_TABLES).freeze
71
+
72
+ # Preserve variation data from source to target
73
+ #
74
+ # @param source_font [TrueTypeFont, OpenTypeFont] Variable font
75
+ # @param target_tables [Hash<String, String>] Target font tables
76
+ # @param options [Hash] Preservation options
77
+ # @return [Hash<String, String>] Tables with variation data preserved
78
+ def self.preserve(source_font, target_tables, options = {})
79
+ new(source_font, target_tables, options).preserve
80
+ end
81
+
82
+ # @return [Object] Source font
83
+ attr_reader :source_font
84
+
85
+ # @return [Hash<String, String>] Target tables
86
+ attr_reader :target_tables
87
+
88
+ # @return [Hash] Preservation options
89
+ attr_reader :options
90
+
91
+ # Initialize preserver
92
+ #
93
+ # @param source_font [TrueTypeFont, OpenTypeFont] Variable font
94
+ # @param target_tables [Hash<String, String>] Target font tables
95
+ # @param options [Hash] Preservation options
96
+ # @option options [Boolean] :preserve_format_specific Preserve format-
97
+ # specific variation tables (default: true)
98
+ # @option options [Boolean] :preserve_metrics Preserve metrics variation
99
+ # tables (default: true)
100
+ # @option options [Boolean] :validate Validate table consistency
101
+ # (default: true)
102
+ def initialize(source_font, target_tables, options = {})
103
+ @source_font = source_font
104
+ @target_tables = target_tables.dup
105
+ @options = options
106
+
107
+ validate_source!
108
+ @context = VariationContext.new(source_font)
109
+ end
110
+
111
+ # Preserve variation tables
112
+ #
113
+ # @return [Hash<String, String>] Target tables with variation preserved
114
+ def preserve
115
+ # Copy common variation tables (fvar, avar, STAT)
116
+ copy_common_tables
117
+
118
+ # Copy format-specific variation tables if requested
119
+ if preserve_format_specific?
120
+ copy_format_specific_tables
121
+ end
122
+
123
+ # Copy metrics variation tables if requested
124
+ copy_metrics_tables if preserve_metrics?
125
+
126
+ # Validate consistency if requested
127
+ validate_consistency if validate?
128
+
129
+ @target_tables
130
+ end
131
+
132
+ # Check if source font is a variable font
133
+ #
134
+ # @return [Boolean] True if source has fvar table
135
+ def variable_font?
136
+ @context.variable_font?
137
+ end
138
+
139
+ # Get variation type of source font
140
+ #
141
+ # @return [Symbol, nil] :truetype, :cff2, or nil
142
+ def variation_type
143
+ @context.variation_type
144
+ end
145
+
146
+ private
147
+
148
+ # Validate source font
149
+ #
150
+ # @raise [ArgumentError] If source is invalid
151
+ def validate_source!
152
+ raise ArgumentError, "Source font cannot be nil" if @source_font.nil?
153
+
154
+ unless @source_font.respond_to?(:has_table?) &&
155
+ @source_font.respond_to?(:table_data)
156
+ raise ArgumentError,
157
+ "Source font must respond to :has_table? and :table_data"
158
+ end
159
+
160
+ raise ArgumentError, "Target tables cannot be nil" if @target_tables.nil?
161
+
162
+ unless @target_tables.is_a?(Hash)
163
+ raise ArgumentError,
164
+ "Target tables must be a Hash, got: #{@target_tables.class}"
165
+ end
166
+ end
167
+
168
+ # Copy common variation tables (fvar, avar, STAT)
169
+ #
170
+ # These tables are independent of outline format and can always be copied
171
+ def copy_common_tables
172
+ COMMON_TABLES.each do |tag|
173
+ copy_table(tag) if @source_font.has_table?(tag)
174
+ end
175
+ end
176
+
177
+ # Copy format-specific variation tables
178
+ #
179
+ # For TrueType: gvar, cvar
180
+ # For CFF2: CFF2 table
181
+ def copy_format_specific_tables
182
+ case variation_type
183
+ when :truetype
184
+ copy_truetype_variation_tables
185
+ when :postscript
186
+ copy_cff2_variation_tables
187
+ end
188
+ end
189
+
190
+ # Copy TrueType variation tables
191
+ def copy_truetype_variation_tables
192
+ TRUETYPE_TABLES.each do |tag|
193
+ copy_table(tag) if @source_font.has_table?(tag)
194
+ end
195
+ end
196
+
197
+ # Copy CFF2 variation tables
198
+ def copy_cff2_variation_tables
199
+ # CFF2 table contains both outlines and variation data
200
+ # Only copy if target doesn't already have CFF2 and source has it
201
+ return unless @source_font.has_table?("CFF2")
202
+ return if @target_tables.key?("CFF2")
203
+
204
+ copy_table("CFF2")
205
+ end
206
+
207
+ # Copy metrics variation tables (HVAR, VVAR, MVAR)
208
+ def copy_metrics_tables
209
+ METRICS_TABLES.each do |tag|
210
+ copy_table(tag) if @source_font.has_table?(tag)
211
+ end
212
+ end
213
+
214
+ # Copy a single table from source to target
215
+ #
216
+ # @param tag [String] Table tag
217
+ def copy_table(tag)
218
+ return unless @source_font.has_table?(tag)
219
+
220
+ table_data = @source_font.table_data[tag]
221
+ return unless table_data
222
+
223
+ @target_tables[tag] = table_data.dup
224
+ end
225
+
226
+ # Validate table consistency
227
+ #
228
+ # Ensures that copied variation tables are consistent with target font
229
+ # @raise [Error] If validation fails
230
+ def validate_consistency
231
+ # Must have fvar if we're preserving variations
232
+ unless @target_tables.key?("fvar")
233
+ raise Fontisan::Error,
234
+ "Cannot preserve variations: fvar table missing"
235
+ end
236
+
237
+ # If we have gvar, we must have glyf (TrueType outlines)
238
+ if @target_tables.key?("gvar") && !@target_tables.key?("glyf")
239
+ raise Fontisan::Error,
240
+ "Invalid variation preservation: gvar present without glyf"
241
+ end
242
+
243
+ # If we have CFF2, we shouldn't have glyf (CFF2 has CFF outlines)
244
+ # Check both source and target to catch conflicts
245
+ has_cff2 = @target_tables.key?("CFF2") ||
246
+ (@source_font.has_table?("CFF2") && preserve_format_specific?)
247
+ if has_cff2 && @target_tables.key?("glyf")
248
+ raise Fontisan::Error,
249
+ "Invalid variation preservation: CFF2 and glyf both present"
250
+ end
251
+
252
+ # Metrics variation tables require fvar
253
+ if metrics_tables_present? && !@target_tables.key?("fvar")
254
+ raise Fontisan::Error,
255
+ "Metrics variation tables require fvar table"
256
+ end
257
+ end
258
+
259
+ # Check if any metrics variation tables are present
260
+ #
261
+ # @return [Boolean] True if HVAR, VVAR, or MVAR present
262
+ def metrics_tables_present?
263
+ METRICS_TABLES.any? { |tag| @target_tables.key?(tag) }
264
+ end
265
+
266
+ # Get preserve_format_specific option
267
+ #
268
+ # @return [Boolean] True if format-specific tables should be preserved
269
+ def preserve_format_specific?
270
+ @options.fetch(:preserve_format_specific, true)
271
+ end
272
+
273
+ # Get preserve_metrics option
274
+ #
275
+ # @return [Boolean] True if metrics tables should be preserved
276
+ def preserve_metrics?
277
+ @options.fetch(:preserve_metrics, true)
278
+ end
279
+
280
+ # Get validate option
281
+ #
282
+ # @return [Boolean] True if consistency should be validated
283
+ def validate?
284
+ @options.fetch(:validate, true)
285
+ end
286
+ end
287
+ end
288
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ <<<<<<< HEAD
5
+ VERSION = "2.0.0"
6
+ =======
7
+ VERSION = "0.2.0"
8
+ >>>>>>> 4bcec10 (fix: update loading modes)
9
+ end
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Woff2
5
+ # WOFF2 Table Directory Entry
6
+ #
7
+ # [`Woff2::Directory`](lib/fontisan/woff2/directory.rb) represents
8
+ # a single table entry in the WOFF2 table directory. Unlike WOFF,
9
+ # WOFF2 uses variable-length encoding for sizes and supports table
10
+ # transformations for better compression.
11
+ #
12
+ # Each entry contains:
13
+ # - flags (1 byte): Contains tag index and transformation version
14
+ # - tag (0 or 4 bytes): Table tag (omitted if using known tag index)
15
+ # - origLength (UIntBase128): Original uncompressed table length
16
+ # - transformLength (UIntBase128, optional): Transformed data length
17
+ #
18
+ # Flags byte structure:
19
+ # - Bits 0-5: Table tag index (0-62 = known tags, 63 = custom tag)
20
+ # - Bits 6-7: Transformation version
21
+ #
22
+ # Reference: https://www.w3.org/TR/WOFF2/#table_dir_format
23
+ #
24
+ # @example Create entry for known table
25
+ # entry = Directory::Entry.new
26
+ # entry.tag = "glyf"
27
+ # entry.orig_length = 12000
28
+ # entry.flags = entry.calculate_flags
29
+ #
30
+ # @example Create entry for custom table
31
+ # entry = Directory::Entry.new
32
+ # entry.tag = "CUST"
33
+ # entry.orig_length = 5000
34
+ # entry.flags = 0x3F # Custom tag indicator
35
+ module Directory
36
+ # Known table tags with assigned indices (0-62)
37
+ # Index 63 (0x3F) indicates a custom tag follows
38
+ KNOWN_TAGS = [
39
+ "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
40
+ "cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
41
+ "EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea",
42
+ "vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
43
+ "CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
44
+ "bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
45
+ "gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop",
46
+ "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill"
47
+ ].freeze
48
+
49
+ # Transformation versions
50
+ TRANSFORM_NONE = 0
51
+ TRANSFORM_GLYF_LOCA = 0 # Applied to both glyf and loca
52
+ TRANSFORM_HMTX = 0 # Applied to hmtx
53
+
54
+ # Custom tag indicator
55
+ CUSTOM_TAG_INDEX = 0x3F
56
+
57
+ # WOFF2 Table Directory Entry
58
+ #
59
+ # Represents a single table in the WOFF2 font with all metadata
60
+ # needed for decompression and reconstruction.
61
+ class Entry
62
+ attr_accessor :tag, :flags, :orig_length, :transform_length, :offset # Calculated during encoding
63
+
64
+ def initialize
65
+ @tag = nil
66
+ @flags = 0
67
+ @orig_length = 0
68
+ @transform_length = nil
69
+ @offset = 0
70
+ end
71
+
72
+ # Calculate flags byte for this entry
73
+ #
74
+ # @return [Integer] Flags byte (0-255)
75
+ def calculate_flags
76
+ tag_index = KNOWN_TAGS.index(tag) || CUSTOM_TAG_INDEX
77
+ transform_version = determine_transform_version
78
+
79
+ # Combine tag index (bits 0-5) and transform version (bits 6-7)
80
+ (transform_version << 6) | tag_index
81
+ end
82
+
83
+ # Check if table uses a known tag
84
+ #
85
+ # @return [Boolean] True if known tag
86
+ def known_tag?
87
+ KNOWN_TAGS.include?(tag)
88
+ end
89
+
90
+ # Check if table is transformed
91
+ #
92
+ # @return [Boolean] True if transformed
93
+ def transformed?
94
+ transform_version != TRANSFORM_NONE && transform_length
95
+ end
96
+
97
+ # Get transformation version from flags
98
+ #
99
+ # @return [Integer] Transform version (0-3)
100
+ def transform_version
101
+ (flags >> 6) & 0x03
102
+ end
103
+
104
+ # Get tag index from flags
105
+ #
106
+ # @return [Integer] Tag index (0-63)
107
+ def tag_index
108
+ flags & 0x3F
109
+ end
110
+
111
+ # Determine if this table should be transformed
112
+ #
113
+ # For Phase 2 Milestone 2.1, we support transformation flags
114
+ # but don't implement the actual transformations yet.
115
+ #
116
+ # @return [Integer] Transform version
117
+ def determine_transform_version
118
+ # For this milestone, we don't apply transformations
119
+ # but we recognize which tables could be transformed
120
+ TRANSFORM_NONE
121
+ end
122
+
123
+ # Check if table can be transformed (glyf, loca, hmtx)
124
+ #
125
+ # @return [Boolean] True if transformable
126
+ def transformable?
127
+ %w[glyf loca hmtx].include?(tag)
128
+ end
129
+
130
+ # Calculate size of this entry when serialized
131
+ #
132
+ # @return [Integer] Size in bytes
133
+ def serialized_size
134
+ size = 1 # flags byte
135
+ size += 4 unless known_tag? # custom tag
136
+ size += uint_base128_size(orig_length)
137
+ size += uint_base128_size(transform_length) if transformed?
138
+ size
139
+ end
140
+
141
+ private
142
+
143
+ # Estimate size of UIntBase128 encoded value
144
+ #
145
+ # @param value [Integer] Value to encode
146
+ # @return [Integer] Size in bytes (1-5)
147
+ def uint_base128_size(value)
148
+ return 1 if value.nil? || value < 128
149
+
150
+ bytes = 0
151
+ v = value
152
+ while v.positive?
153
+ bytes += 1
154
+ v >>= 7
155
+ end
156
+ [bytes, 5].min # Max 5 bytes
157
+ end
158
+ end
159
+
160
+ # Encode an integer as UIntBase128
161
+ #
162
+ # Variable-length encoding where:
163
+ # - If value < 128, use 1 byte
164
+ # - Otherwise, use high bit to indicate continuation
165
+ #
166
+ # @param value [Integer] Value to encode
167
+ # @return [String] Binary encoded data
168
+ def self.encode_uint_base128(value)
169
+ return [value].pack("C") if value < 128
170
+
171
+ bytes = []
172
+ v = value
173
+
174
+ # Build bytes from least to most significant
175
+ loop do
176
+ bytes.unshift(v & 0x7F)
177
+ v >>= 7
178
+ break if v.zero?
179
+ end
180
+
181
+ # Set high bit on all but last byte
182
+ (0...bytes.length - 1).each do |i|
183
+ bytes[i] |= 0x80
184
+ end
185
+
186
+ bytes.pack("C*")
187
+ end
188
+
189
+ # Decode UIntBase128 from IO
190
+ #
191
+ # @param io [IO] Input stream
192
+ # @return [Integer] Decoded value
193
+ # @raise [Error] If encoding is invalid
194
+ def self.decode_uint_base128(io)
195
+ result = 0
196
+ 5.times do
197
+ byte = io.read(1)&.unpack1("C")
198
+ return nil unless byte
199
+
200
+ # Check if high bit is set (continuation)
201
+ if (byte & 0x80).zero?
202
+ return (result << 7) | byte
203
+ else
204
+ result = (result << 7) | (byte & 0x7F)
205
+ end
206
+ end
207
+
208
+ # If we're here, encoding is invalid (> 5 bytes)
209
+ raise Fontisan::Error, "Invalid UIntBase128 encoding"
210
+ end
211
+
212
+ # Encode 255UInt16 value
213
+ #
214
+ # Used in transformed glyf table:
215
+ # - 0-252: value itself (1 byte)
216
+ # - 253: next byte + 253 (2 bytes)
217
+ # - 254: next 2 bytes as big-endian (3 bytes)
218
+ # - 255: next 2 bytes + 506 (3 bytes)
219
+ #
220
+ # @param value [Integer] Value to encode (0-65535)
221
+ # @return [String] Binary encoded data
222
+ def self.encode_255_uint16(value)
223
+ if value < 253
224
+ [value].pack("C")
225
+ elsif value < 506
226
+ [253, value - 253].pack("C*")
227
+ elsif value < 65536
228
+ [254].pack("C") + [value].pack("n")
229
+ else
230
+ [255].pack("C") + [value - 506].pack("n")
231
+ end
232
+ end
233
+
234
+ # Decode 255UInt16 from IO
235
+ #
236
+ # @param io [IO] Input stream
237
+ # @return [Integer] Decoded value
238
+ def self.decode_255_uint16(io)
239
+ first = io.read(1)&.unpack1("C")
240
+ return nil unless first
241
+
242
+ case first
243
+ when 0..252
244
+ first
245
+ when 253
246
+ second = io.read(1)&.unpack1("C")
247
+ 253 + second
248
+ when 254
249
+ io.read(2)&.unpack1("n")
250
+ when 255
251
+ value = io.read(2)&.unpack1("n")
252
+ value + 506
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end