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,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../font_writer"
4
+
5
+ module Fontisan
6
+ module Pipeline
7
+ # Handles writing font tables to various output formats
8
+ #
9
+ # This class abstracts the complexity of writing different font formats:
10
+ # - SFNT formats (TTF, OTF) via FontWriter
11
+ # - WOFF via WoffWriter
12
+ # - WOFF2 via Woff2Encoder
13
+ #
14
+ # Single Responsibility: Coordinate output writing for different formats
15
+ #
16
+ # @example Write TTF font
17
+ # writer = OutputWriter.new("output.ttf", :ttf)
18
+ # writer.write(tables)
19
+ #
20
+ # @example Write OTF font
21
+ # writer = OutputWriter.new("output.otf", :otf)
22
+ # writer.write(tables)
23
+ class OutputWriter
24
+ # @return [String] Output file path
25
+ attr_reader :output_path
26
+
27
+ # @return [Symbol] Target format
28
+ attr_reader :format
29
+
30
+ # @return [Hash] Writing options
31
+ attr_reader :options
32
+
33
+ # Initialize output writer
34
+ #
35
+ # @param output_path [String] Path to write output
36
+ # @param format [Symbol] Target format (:ttf, :otf, :woff, :woff2)
37
+ # @param options [Hash] Writing options
38
+ def initialize(output_path, format, options = {})
39
+ @output_path = output_path
40
+ @format = format
41
+ @options = options
42
+ end
43
+
44
+ # Write font tables to output file
45
+ #
46
+ # @param tables [Hash<String, String>, Hash] Font tables (tag => binary data) or special format result
47
+ # @return [Integer] Number of bytes written
48
+ # @raise [ArgumentError] If format is unsupported
49
+ def write(tables)
50
+ case @format
51
+ when :ttf, :otf
52
+ write_sfnt(tables)
53
+ when :woff
54
+ write_woff(tables)
55
+ when :woff2
56
+ write_woff2(tables)
57
+ when :svg
58
+ write_svg(tables)
59
+ else
60
+ raise ArgumentError, "Unsupported output format: #{@format}"
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # Write SVG format
67
+ #
68
+ # @param result [Hash] Result with :svg_xml key
69
+ # @return [Integer] Number of bytes written
70
+ def write_svg(result)
71
+ svg_xml = result[:svg_xml] || result["svg_xml"]
72
+ raise ArgumentError, "SVG result must contain :svg_xml key" unless svg_xml
73
+
74
+ File.write(@output_path, svg_xml)
75
+ end
76
+
77
+ # Write SFNT format (TTF or OTF)
78
+ #
79
+ # @param tables [Hash<String, String>] Font tables
80
+ # @return [Integer] Number of bytes written
81
+ def write_sfnt(tables)
82
+ sfnt_version = determine_sfnt_version
83
+ FontWriter.write_to_file(tables, @output_path, sfnt_version: sfnt_version)
84
+ end
85
+
86
+ # Write WOFF format
87
+ #
88
+ # @param tables [Hash<String, String>] Font tables
89
+ # @return [Integer] Number of bytes written
90
+ def write_woff(tables)
91
+ require_relative "../converters/woff_writer"
92
+
93
+ writer = Converters::WoffWriter.new
94
+ font = build_font_from_tables(tables)
95
+ result = writer.convert(font, @options)
96
+
97
+ File.binwrite(@output_path, result[:woff_data])
98
+ end
99
+
100
+ # Write WOFF2 format
101
+ #
102
+ # @param tables [Hash<String, String>] Font tables
103
+ # @return [Integer] Number of bytes written
104
+ def write_woff2(tables)
105
+ require_relative "../converters/woff2_encoder"
106
+
107
+ encoder = Converters::Woff2Encoder.new
108
+ font = build_font_from_tables(tables)
109
+ result = encoder.convert(font, @options)
110
+
111
+ File.binwrite(@output_path, result[:woff2_binary])
112
+ end
113
+
114
+ # Determine SFNT version based on format and tables
115
+ #
116
+ # @return [Integer] SFNT version (0x00010000 for TTF, 0x4F54544F for OTF)
117
+ def determine_sfnt_version
118
+ case @format
119
+ when :ttf, :woff, :woff2 then 0x00010000
120
+ when :otf then 0x4F54544F # 'OTTO'
121
+ else raise ArgumentError, "Unsupported format: #{@format}"
122
+ end
123
+ end
124
+
125
+ # Build font object from tables
126
+ #
127
+ # Helper to create font object from tables for converters that need it.
128
+ #
129
+ # @param tables [Hash<String, String>] Font tables
130
+ # @return [Font] Font object
131
+ def build_font_from_tables(tables)
132
+ # Detect font type from tables
133
+ has_cff = tables.key?("CFF ") || tables.key?("CFF2")
134
+ has_glyf = tables.key?("glyf")
135
+
136
+ if has_cff
137
+ OpenTypeFont.from_tables(tables)
138
+ elsif has_glyf
139
+ TrueTypeFont.from_tables(tables)
140
+ else
141
+ # Default based on format
142
+ case @format
143
+ when :ttf, :woff, :woff2
144
+ TrueTypeFont.from_tables(tables)
145
+ when :otf
146
+ OpenTypeFont.from_tables(tables)
147
+ else
148
+ raise ArgumentError, "Cannot determine font type for format: #{@format}"
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Pipeline
5
+ module Strategies
6
+ # Base class for variation resolution strategies
7
+ #
8
+ # This abstract class defines the interface that all variation resolution
9
+ # strategies must implement. It follows the Strategy pattern to allow
10
+ # different approaches to handling variable font data during conversion.
11
+ #
12
+ # Subclasses must implement:
13
+ # - resolve(font): Process the font and return tables
14
+ # - preserves_variation?: Indicate if variation data is preserved
15
+ # - strategy_name: Return the strategy identifier
16
+ #
17
+ # @example Implementing a strategy
18
+ # class MyStrategy < BaseStrategy
19
+ # def resolve(font)
20
+ # # Implementation
21
+ # end
22
+ #
23
+ # def preserves_variation?
24
+ # false
25
+ # end
26
+ #
27
+ # def strategy_name
28
+ # :my_strategy
29
+ # end
30
+ # end
31
+ class BaseStrategy
32
+ # @return [Hash] Strategy options
33
+ attr_reader :options
34
+
35
+ # Initialize strategy with options
36
+ #
37
+ # @param options [Hash] Strategy-specific options
38
+ def initialize(options = {})
39
+ @options = options
40
+ end
41
+
42
+ # Resolve variation data
43
+ #
44
+ # This method must be implemented by subclasses to process the font
45
+ # and return the appropriate tables based on the strategy.
46
+ #
47
+ # @param font [TrueTypeFont, OpenTypeFont] Font to process
48
+ # @return [Hash<String, String>] Map of table tags to binary data
49
+ # @raise [NotImplementedError] If not implemented by subclass
50
+ def resolve(font)
51
+ raise NotImplementedError,
52
+ "#{self.class.name} must implement #resolve"
53
+ end
54
+
55
+ # Check if strategy preserves variation data
56
+ #
57
+ # @return [Boolean] True if variation data is preserved
58
+ # @raise [NotImplementedError] If not implemented by subclass
59
+ def preserves_variation?
60
+ raise NotImplementedError,
61
+ "#{self.class.name} must implement #preserves_variation?"
62
+ end
63
+
64
+ # Get strategy name
65
+ #
66
+ # @return [Symbol] Strategy identifier
67
+ # @raise [NotImplementedError] If not implemented by subclass
68
+ def strategy_name
69
+ raise NotImplementedError,
70
+ "#{self.class.name} must implement #strategy_name"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy"
4
+ require_relative "../../variation/instance_generator"
5
+ require_relative "../../variation/variation_context"
6
+
7
+ module Fontisan
8
+ module Pipeline
9
+ module Strategies
10
+ # Strategy for generating static instances from variable fonts
11
+ #
12
+ # This strategy creates a static font instance at specific design space
13
+ # coordinates by applying variation deltas and removing variation tables.
14
+ # It's used for:
15
+ # - Variable TTF → Static TTF at specific weight
16
+ # - Variable OTF → Static OTF at specific coordinates
17
+ # - Variable → Static for any format conversion
18
+ #
19
+ # The strategy uses the InstanceGenerator to:
20
+ # 1. Apply variation deltas (gvar or CFF2 blend)
21
+ # 2. Apply metrics variations (HVAR, VVAR, MVAR)
22
+ # 3. Remove variation tables (fvar, gvar, CFF2, avar, etc.)
23
+ #
24
+ # If no coordinates are provided, uses default coordinates (axis default values).
25
+ #
26
+ # @example Generate instance at specific weight
27
+ # strategy = InstanceStrategy.new(coordinates: { "wght" => 700.0 })
28
+ # tables = strategy.resolve(variable_font)
29
+ # # tables has no variation tables
30
+ #
31
+ # @example Generate instance at default coordinates
32
+ # strategy = InstanceStrategy.new
33
+ # tables = strategy.resolve(variable_font)
34
+ class InstanceStrategy < BaseStrategy
35
+ # @return [Hash<String, Float>] Design space coordinates
36
+ attr_reader :coordinates
37
+
38
+ # Initialize strategy with coordinates
39
+ #
40
+ # @param options [Hash] Strategy options
41
+ # @option options [Hash<String, Float>] :coordinates Design space coordinates
42
+ # (axis tag => value). If not provided, uses default coordinates.
43
+ def initialize(options = {})
44
+ super
45
+ @coordinates = options[:coordinates] || {}
46
+ end
47
+
48
+ # Resolve by generating static instance
49
+ #
50
+ # Creates a static font instance at the specified coordinates using
51
+ # the InstanceGenerator. If coordinates are not provided, uses the
52
+ # default coordinates from the font's axes.
53
+ #
54
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font
55
+ # @return [Hash<String, String>] Static font tables
56
+ # @raise [Variation::InvalidCoordinatesError] If coordinates out of range
57
+ def resolve(font)
58
+ # Validate coordinates if provided
59
+ validate_coordinates(font) unless @coordinates.empty?
60
+
61
+ # Use InstanceGenerator to create static instance
62
+ generator = Variation::InstanceGenerator.new(font, @coordinates)
63
+ generator.generate
64
+ end
65
+
66
+ # Check if strategy preserves variation data
67
+ #
68
+ # @return [Boolean] Always false for this strategy
69
+ def preserves_variation?
70
+ false
71
+ end
72
+
73
+ # Get strategy name
74
+ #
75
+ # @return [Symbol] :instance
76
+ def strategy_name
77
+ :instance
78
+ end
79
+
80
+ private
81
+
82
+ # Validate coordinates against font axes
83
+ #
84
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font
85
+ # @raise [Variation::InvalidCoordinatesError] If invalid
86
+ def validate_coordinates(font)
87
+ context = Variation::VariationContext.new(font)
88
+ context.validate_coordinates(@coordinates)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy"
4
+ require_relative "instance_strategy"
5
+ require_relative "../../variation/variation_context"
6
+
7
+ module Fontisan
8
+ module Pipeline
9
+ module Strategies
10
+ # Strategy for generating instances from named instances
11
+ #
12
+ # This strategy creates a static font instance using coordinates from
13
+ # a named instance defined in the fvar table. It extracts the coordinates
14
+ # from the specified instance and delegates to InstanceStrategy for
15
+ # actual generation.
16
+ #
17
+ # Named instances are predefined design space coordinates stored in the
18
+ # fvar table, typically representing common styles like "Bold", "Light",
19
+ # "Condensed", etc.
20
+ #
21
+ # @example Generate "Bold" instance
22
+ # strategy = NamedStrategy.new(instance_index: 0)
23
+ # tables = strategy.resolve(variable_font)
24
+ #
25
+ # @example Use specific named instance
26
+ # # Find instance by name first, then use index
27
+ # fvar = font.table("fvar")
28
+ # bold_index = fvar.instances.find_index { |i| i[:name] =~ /Bold/ }
29
+ # strategy = NamedStrategy.new(instance_index: bold_index)
30
+ # tables = strategy.resolve(variable_font)
31
+ class NamedStrategy < BaseStrategy
32
+ # @return [Integer] Named instance index
33
+ attr_reader :instance_index
34
+
35
+ # Initialize strategy with instance index
36
+ #
37
+ # @param options [Hash] Strategy options
38
+ # @option options [Integer] :instance_index Index of named instance in fvar
39
+ # @raise [ArgumentError] If instance_index not provided
40
+ def initialize(options = {})
41
+ super
42
+ @instance_index = options[:instance_index]
43
+
44
+ if @instance_index.nil?
45
+ raise ArgumentError, "instance_index is required for NamedStrategy"
46
+ end
47
+ end
48
+
49
+ # Resolve by using named instance coordinates
50
+ #
51
+ # Extracts coordinates from the fvar table's named instance and
52
+ # delegates to InstanceStrategy for actual instance generation.
53
+ #
54
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font
55
+ # @return [Hash<String, String>] Static font tables
56
+ # @raise [ArgumentError] If instance index is invalid
57
+ def resolve(font)
58
+ # Extract coordinates from named instance
59
+ coordinates = extract_coordinates(font)
60
+
61
+ # Use InstanceStrategy to generate instance
62
+ instance_strategy = InstanceStrategy.new(coordinates: coordinates)
63
+ instance_strategy.resolve(font)
64
+ end
65
+
66
+ # Check if strategy preserves variation data
67
+ #
68
+ # @return [Boolean] Always false for this strategy
69
+ def preserves_variation?
70
+ false
71
+ end
72
+
73
+ # Get strategy name
74
+ #
75
+ # @return [Symbol] :named
76
+ def strategy_name
77
+ :named
78
+ end
79
+
80
+ private
81
+
82
+ # Extract coordinates from named instance in fvar table
83
+ #
84
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font
85
+ # @return [Hash<String, Float>] Design space coordinates
86
+ # @raise [ArgumentError] If instance index is invalid
87
+ def extract_coordinates(font)
88
+ context = Variation::VariationContext.new(font)
89
+
90
+ unless context.fvar
91
+ raise ArgumentError, "Font is not a variable font (no fvar table)"
92
+ end
93
+
94
+ instances = context.fvar.instances
95
+ if @instance_index.negative? || @instance_index >= instances.length
96
+ raise ArgumentError,
97
+ "Invalid instance index #{@instance_index}. " \
98
+ "Font has #{instances.length} named instances."
99
+ end
100
+
101
+ instance = instances[@instance_index]
102
+ axes = context.axes
103
+
104
+ # Map instance coordinates to axis tags
105
+ coordinates = {}
106
+ instance[:coordinates].each_with_index do |value, i|
107
+ next if i >= axes.length
108
+
109
+ axis = axes[i]
110
+ coordinates[axis.axis_tag] = value
111
+ end
112
+
113
+ coordinates
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy"
4
+
5
+ module Fontisan
6
+ module Pipeline
7
+ module Strategies
8
+ # Strategy for preserving variation data during conversion
9
+ #
10
+ # This strategy maintains all variation tables intact, making it suitable
11
+ # for conversions between compatible formats:
12
+ # - Variable TTF → Variable TTF (same format)
13
+ # - Variable OTF → Variable OTF (same format)
14
+ # - Variable TTF → Variable WOFF/WOFF2 (packaging change only)
15
+ # - Variable OTF → Variable WOFF/WOFF2 (packaging change only)
16
+ #
17
+ # The strategy copies all font tables including:
18
+ # - Variation tables: fvar, gvar/CFF2, avar, HVAR, VVAR, MVAR
19
+ # - Base tables: All non-variation tables
20
+ #
21
+ # @example Preserve variation data
22
+ # strategy = PreserveStrategy.new
23
+ # tables = strategy.resolve(variable_font)
24
+ # # tables includes fvar, gvar, etc.
25
+ class PreserveStrategy < BaseStrategy
26
+ # Resolve by preserving all variation data
27
+ #
28
+ # Returns all font tables including variation tables. This is a simple
29
+ # copy operation that maintains the variable font's full capabilities.
30
+ #
31
+ # @param font [TrueTypeFont, OpenTypeFont] Variable font
32
+ # @return [Hash<String, String>] All font tables
33
+ def resolve(font)
34
+ # Return a copy of all font tables
35
+ # This preserves variation tables (fvar, gvar, CFF2, avar, HVAR, etc.)
36
+ # and all base tables
37
+ font.table_data.dup
38
+ end
39
+
40
+ # Check if strategy preserves variation data
41
+ #
42
+ # @return [Boolean] Always true for this strategy
43
+ def preserves_variation?
44
+ true
45
+ end
46
+
47
+ # Get strategy name
48
+ #
49
+ # @return [Symbol] :preserve
50
+ def strategy_name
51
+ :preserve
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end