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,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ # Loading modes module that defines which tables are loaded in each mode.
5
+ #
6
+ # This module provides a MECE (Mutually Exclusive, Collectively Exhaustive)
7
+ # architecture for font loading modes. Each mode defines a specific set of
8
+ # tables to load, enabling efficient parsing for different use cases.
9
+ #
10
+ # @example Using metadata mode
11
+ # mode = LoadingModes::METADATA
12
+ # tables = LoadingModes.tables_for(mode) # => ["name", "head", "hhea", "maxp", "OS/2", "post"]
13
+ #
14
+ # @example Checking table availability
15
+ # LoadingModes.table_allowed?(:metadata, "GSUB") # => false
16
+ # LoadingModes.table_allowed?(:full, "GSUB") # => true
17
+ module LoadingModes
18
+ # Metadata mode: loads only tables needed for font identification and metrics
19
+ # Equivalent to otfinfo functionality
20
+ METADATA = :metadata
21
+
22
+ # Full mode: loads all tables in the font
23
+ FULL = :full
24
+
25
+ # Mode definitions with their respective table lists
26
+ MODES = {
27
+ METADATA => {
28
+ tables: %w[name head hhea maxp OS/2 post].freeze,
29
+ description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)"
30
+ }.freeze,
31
+ FULL => {
32
+ tables: :all,
33
+ description: "Full mode - loads all tables in the font"
34
+ }.freeze
35
+ }.freeze
36
+
37
+ # Pre-computed Set for O(1) lookup of metadata tables
38
+ # This constant avoids recreating the Set on every font load
39
+ METADATA_TABLES_SET = MODES[METADATA][:tables].to_set.freeze
40
+
41
+ # Get the list of tables allowed for a given mode
42
+ #
43
+ # @param mode [Symbol] The loading mode (:metadata or :full)
44
+ # @return [Array<String>, Symbol] Array of table tags or :all for full mode
45
+ # @raise [ArgumentError] if mode is invalid
46
+ def self.tables_for(mode)
47
+ validate_mode!(mode)
48
+ MODES[mode][:tables]
49
+ end
50
+
51
+ # Check if a table is allowed in a given mode
52
+ #
53
+ # @param mode [Symbol] The loading mode (:metadata or :full)
54
+ # @param tag [String] The table tag to check
55
+ # @return [Boolean] true if table is allowed in the mode
56
+ # @raise [ArgumentError] if mode is invalid
57
+ def self.table_allowed?(mode, tag)
58
+ validate_mode!(mode)
59
+
60
+ tables = MODES[mode][:tables]
61
+ return true if tables == :all
62
+
63
+ tables.include?(tag)
64
+ end
65
+
66
+ # Validate that a mode is valid
67
+ #
68
+ # @param mode [Symbol] The mode to validate
69
+ # @return [Boolean] true if mode is valid
70
+ def self.valid_mode?(mode)
71
+ MODES.key?(mode)
72
+ end
73
+
74
+ # Get the default lazy loading setting for a mode
75
+ #
76
+ # @param mode [Symbol] The loading mode
77
+ # @return [Boolean] true if lazy loading is recommended for this mode
78
+ # @raise [ArgumentError] if mode is invalid
79
+ def self.default_lazy?(mode)
80
+ validate_mode!(mode)
81
+ true # Lazy loading is recommended for all modes
82
+ end
83
+
84
+ # Get mode description
85
+ #
86
+ # @param mode [Symbol] The loading mode
87
+ # @return [String] Description of the mode
88
+ # @raise [ArgumentError] if mode is invalid
89
+ def self.description(mode)
90
+ validate_mode!(mode)
91
+ MODES[mode][:description]
92
+ end
93
+
94
+ # Get all available modes
95
+ #
96
+ # @return [Array<Symbol>] List of all mode symbols
97
+ def self.all_modes
98
+ MODES.keys
99
+ end
100
+
101
+ # Validate mode and raise error if invalid
102
+ #
103
+ # @param mode [Symbol] The mode to validate
104
+ # @return [void]
105
+ # @raise [ArgumentError] if mode is invalid
106
+ def self.validate_mode!(mode)
107
+ return if valid_mode?(mode)
108
+
109
+ raise ArgumentError,
110
+ "Invalid mode: #{mode.inspect}. Valid modes are: #{all_modes.map(&:inspect).join(', ')}"
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+
5
+ module Fontisan
6
+ # High-level utility class for accessing font metrics
7
+ #
8
+ # MetricsCalculator provides a convenient API for querying font metrics from
9
+ # multiple OpenType tables without needing to work with the low-level table
10
+ # structures directly. It wraps access to hhea, hmtx, head, maxp, and cmap
11
+ # tables.
12
+ #
13
+ # The calculator handles missing tables gracefully and provides both
14
+ # individual glyph metrics and string-level calculations.
15
+ #
16
+ # @example Basic usage
17
+ # font = FontLoader.from_file("path/to/font.ttf")
18
+ # calc = MetricsCalculator.new(font)
19
+ #
20
+ # puts calc.ascent # => 2048
21
+ # puts calc.descent # => -512
22
+ # puts calc.line_height # => 2650
23
+ # puts calc.units_per_em # => 2048
24
+ #
25
+ # @example Glyph metrics
26
+ # width = calc.glyph_width(42)
27
+ # lsb = calc.glyph_left_side_bearing(42)
28
+ #
29
+ # @example String width calculation
30
+ # width = calc.string_width("Hello")
31
+ #
32
+ # @example Checking for metrics support
33
+ # if calc.has_metrics?
34
+ # puts "Font has complete horizontal metrics"
35
+ # end
36
+ class MetricsCalculator
37
+ # The font object this calculator operates on
38
+ #
39
+ # @return [OpenTypeFont, TrueTypeFont] The font instance
40
+ attr_reader :font
41
+
42
+ # Initialize a new MetricsCalculator
43
+ #
44
+ # @param font [OpenTypeFont, TrueTypeFont] Font instance to calculate metrics for
45
+ # @raise [ArgumentError] if font is nil
46
+ def initialize(font)
47
+ raise ArgumentError, "Font cannot be nil" if font.nil?
48
+
49
+ @font = font
50
+ @hhea_table = nil
51
+ @hmtx_table = nil
52
+ @head_table = nil
53
+ @maxp_table = nil
54
+ @cmap_table = nil
55
+ @hmtx_parsed = false
56
+ end
57
+
58
+ # Get typographic ascent from hhea table
59
+ #
60
+ # The ascent is the distance from the baseline to the highest ascender.
61
+ # It is a positive value in font units (FUnits).
62
+ #
63
+ # @return [Integer, nil] Ascent value in FUnits, or nil if hhea table is missing
64
+ #
65
+ # @example
66
+ # calc.ascent # => 2048
67
+ def ascent
68
+ hhea&.ascent
69
+ end
70
+
71
+ # Get typographic descent from hhea table
72
+ #
73
+ # The descent is the distance from the baseline to the lowest descender.
74
+ # It is typically a negative value in font units (FUnits).
75
+ #
76
+ # @return [Integer, nil] Descent value in FUnits, or nil if hhea table is missing
77
+ #
78
+ # @example
79
+ # calc.descent # => -512
80
+ def descent
81
+ hhea&.descent
82
+ end
83
+
84
+ # Get line gap from hhea table
85
+ #
86
+ # The line gap is additional vertical space between lines of text.
87
+ # It is a non-negative value in font units (FUnits).
88
+ #
89
+ # @return [Integer, nil] Line gap value in FUnits, or nil if hhea table is missing
90
+ #
91
+ # @example
92
+ # calc.line_gap # => 90
93
+ def line_gap
94
+ hhea&.line_gap
95
+ end
96
+
97
+ # Get units per em from head table
98
+ #
99
+ # This value defines the font's coordinate system scale. Common values
100
+ # are 1000 (PostScript fonts) or 2048 (TrueType fonts).
101
+ #
102
+ # @return [Integer, nil] Units per em value, or nil if head table is missing
103
+ #
104
+ # @example
105
+ # calc.units_per_em # => 2048
106
+ def units_per_em
107
+ head&.units_per_em
108
+ end
109
+
110
+ # Get advance width for a specific glyph
111
+ #
112
+ # The advance width is the horizontal distance to advance the pen position
113
+ # after rendering this glyph. It is in font units (FUnits).
114
+ #
115
+ # @param glyph_id [Integer] The glyph ID (0-based)
116
+ # @return [Integer, nil] Advance width in FUnits, or nil if not available
117
+ #
118
+ # @example
119
+ # calc.glyph_width(42) # => 1234
120
+ def glyph_width(glyph_id)
121
+ ensure_hmtx_parsed
122
+ return nil unless hmtx
123
+
124
+ metric = hmtx.metric_for(glyph_id)
125
+ metric&.dig(:advance_width)
126
+ end
127
+
128
+ # Alias for {#glyph_width}
129
+ #
130
+ # @param glyph_id [Integer] The glyph ID (0-based)
131
+ # @return [Integer, nil] Advance width in FUnits, or nil if not available
132
+ alias glyph_advance_width glyph_width
133
+
134
+ # Get left side bearing for a specific glyph
135
+ #
136
+ # The left side bearing (LSB) is the horizontal distance from the pen
137
+ # position to the leftmost point of the glyph. It can be negative if
138
+ # the glyph extends to the left of the pen position.
139
+ #
140
+ # @param glyph_id [Integer] The glyph ID (0-based)
141
+ # @return [Integer, nil] Left side bearing in FUnits, or nil if not available
142
+ #
143
+ # @example
144
+ # calc.glyph_left_side_bearing(42) # => 50
145
+ def glyph_left_side_bearing(glyph_id)
146
+ ensure_hmtx_parsed
147
+ return nil unless hmtx
148
+
149
+ metric = hmtx.metric_for(glyph_id)
150
+ metric&.dig(:lsb)
151
+ end
152
+
153
+ # Calculate total width for a string
154
+ #
155
+ # Calculates the sum of advance widths for all characters in the string.
156
+ # This is a simplified calculation that does not account for kerning,
157
+ # ligatures, or other advanced typography features.
158
+ #
159
+ # Characters not mapped in the font are skipped.
160
+ #
161
+ # @param string [String] The string to measure
162
+ # @return [Integer, nil] Total width in FUnits, or nil if metrics unavailable
163
+ #
164
+ # @example
165
+ # calc.string_width("Hello") # => 5420
166
+ def string_width(string)
167
+ return nil unless has_metrics?
168
+ return 0 if string.nil? || string.empty?
169
+
170
+ total_width = 0
171
+ string.each_codepoint do |codepoint|
172
+ glyph_id = codepoint_to_glyph_id(codepoint)
173
+ next unless glyph_id
174
+
175
+ width = glyph_width(glyph_id)
176
+ total_width += width if width
177
+ end
178
+
179
+ total_width
180
+ end
181
+
182
+ # Calculate line height
183
+ #
184
+ # Line height is calculated as: ascent - descent + line_gap
185
+ # This represents the recommended spacing between consecutive baselines.
186
+ #
187
+ # @return [Integer, nil] Line height in FUnits, or nil if hhea table is missing
188
+ #
189
+ # @example
190
+ # calc.line_height # => 2650 (when ascent=2048, descent=-512, line_gap=90)
191
+ def line_height
192
+ return nil unless hhea
193
+
194
+ ascent - descent + line_gap
195
+ end
196
+
197
+ # Alias for {#units_per_em}
198
+ #
199
+ # @return [Integer, nil] Units per em value, or nil if head table is missing
200
+ alias em_height units_per_em
201
+
202
+ # Check if font has complete horizontal metrics
203
+ #
204
+ # Returns true if the font has all required tables for horizontal metrics:
205
+ # hhea, hmtx, head, and maxp tables.
206
+ #
207
+ # @return [Boolean] True if all metrics tables are present
208
+ #
209
+ # @example
210
+ # calc.has_metrics? # => true
211
+ def has_metrics?
212
+ !hhea.nil? && !hmtx.nil? && !head.nil? && !maxp.nil?
213
+ end
214
+
215
+ private
216
+
217
+ # Get hhea table, caching the result
218
+ #
219
+ # @return [Tables::Hhea, nil] The hhea table or nil
220
+ def hhea
221
+ @hhea ||= font.table(Constants::HHEA_TAG)
222
+ end
223
+
224
+ # Get hmtx table, caching the result
225
+ #
226
+ # @return [Tables::Hmtx, nil] The hmtx table or nil
227
+ def hmtx
228
+ @hmtx ||= font.table(Constants::HMTX_TAG)
229
+ end
230
+
231
+ # Get head table, caching the result
232
+ #
233
+ # @return [Tables::Head, nil] The head table or nil
234
+ def head
235
+ @head ||= font.table(Constants::HEAD_TAG)
236
+ end
237
+
238
+ # Get maxp table, caching the result
239
+ #
240
+ # @return [Tables::Maxp, nil] The maxp table or nil
241
+ def maxp
242
+ @maxp ||= font.table(Constants::MAXP_TAG)
243
+ end
244
+
245
+ # Get cmap table, caching the result
246
+ #
247
+ # @return [Tables::Cmap, nil] The cmap table or nil
248
+ def cmap
249
+ @cmap ||= font.table(Constants::CMAP_TAG)
250
+ end
251
+
252
+ # Ensure hmtx table is parsed with context
253
+ #
254
+ # The hmtx table requires numberOfHMetrics from hhea and numGlyphs from maxp
255
+ # to be parsed correctly. This method ensures parsing happens lazily on first use.
256
+ #
257
+ # @return [void]
258
+ def ensure_hmtx_parsed
259
+ return if @hmtx_parsed
260
+ return unless hmtx && hhea && maxp
261
+
262
+ hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
263
+ @hmtx_parsed = true
264
+ end
265
+
266
+ # Map Unicode codepoint to glyph ID using cmap table
267
+ #
268
+ # @param codepoint [Integer] Unicode codepoint
269
+ # @return [Integer, nil] Glyph ID or nil if not mapped
270
+ def codepoint_to_glyph_id(codepoint)
271
+ return nil unless cmap
272
+
273
+ mappings = cmap.unicode_mappings
274
+ mappings[codepoint]
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # Model for individual font summary within a collection
8
+ #
9
+ # Represents basic metadata for a single font in a TTC/OTC collection.
10
+ # Used by CollectionListInfo to provide per-font summaries.
11
+ #
12
+ # @example Creating a font summary
13
+ # summary = CollectionFontSummary.new(
14
+ # index: 0,
15
+ # family_name: "Helvetica",
16
+ # subfamily_name: "Regular",
17
+ # postscript_name: "Helvetica-Regular",
18
+ # font_format: "TrueType",
19
+ # num_glyphs: 268,
20
+ # num_tables: 14
21
+ # )
22
+ class CollectionFontSummary < Lutaml::Model::Serializable
23
+ attribute :index, :integer
24
+ attribute :family_name, :string
25
+ attribute :subfamily_name, :string
26
+ attribute :postscript_name, :string
27
+ attribute :font_format, :string
28
+ attribute :num_glyphs, :integer
29
+ attribute :num_tables, :integer
30
+
31
+ yaml do
32
+ map "index", to: :index
33
+ map "family_name", to: :family_name
34
+ map "subfamily_name", to: :subfamily_name
35
+ map "postscript_name", to: :postscript_name
36
+ map "font_format", to: :font_format
37
+ map "num_glyphs", to: :num_glyphs
38
+ map "num_tables", to: :num_tables
39
+ end
40
+
41
+ json do
42
+ map "index", to: :index
43
+ map "family_name", to: :family_name
44
+ map "subfamily_name", to: :subfamily_name
45
+ map "postscript_name", to: :postscript_name
46
+ map "font_format", to: :font_format
47
+ map "num_glyphs", to: :num_glyphs
48
+ map "num_tables", to: :num_tables
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "table_sharing_info"
5
+
6
+ module Fontisan
7
+ module Models
8
+ # Model for collection metadata
9
+ #
10
+ # Represents comprehensive information about a TTC/OTC collection.
11
+ # Used by InfoCommand when operating on collection files.
12
+ #
13
+ # @example Creating collection info
14
+ # info = CollectionInfo.new(
15
+ # collection_path: "fonts.ttc",
16
+ # collection_format: "TTC",
17
+ # ttc_tag: "ttcf",
18
+ # major_version: 2,
19
+ # minor_version: 0,
20
+ # num_fonts: 6,
21
+ # font_offsets: [48, 380, 712, 1044, 1376, 1676],
22
+ # file_size_bytes: 2240000,
23
+ # table_sharing: table_sharing_obj
24
+ # )
25
+ class CollectionInfo < Lutaml::Model::Serializable
26
+ attribute :collection_path, :string
27
+ attribute :collection_format, :string
28
+ attribute :ttc_tag, :string
29
+ attribute :major_version, :integer
30
+ attribute :minor_version, :integer
31
+ attribute :num_fonts, :integer
32
+ attribute :font_offsets, :integer, collection: true
33
+ attribute :file_size_bytes, :integer
34
+ attribute :table_sharing, TableSharingInfo
35
+
36
+ yaml do
37
+ map "collection_path", to: :collection_path
38
+ map "collection_format", to: :collection_format
39
+ map "ttc_tag", to: :ttc_tag
40
+ map "major_version", to: :major_version
41
+ map "minor_version", to: :minor_version
42
+ map "num_fonts", to: :num_fonts
43
+ map "font_offsets", to: :font_offsets
44
+ map "file_size_bytes", to: :file_size_bytes
45
+ map "table_sharing", to: :table_sharing
46
+ end
47
+
48
+ json do
49
+ map "collection_path", to: :collection_path
50
+ map "collection_format", to: :collection_format
51
+ map "ttc_tag", to: :ttc_tag
52
+ map "major_version", to: :major_version
53
+ map "minor_version", to: :minor_version
54
+ map "num_fonts", to: :num_fonts
55
+ map "font_offsets", to: :font_offsets
56
+ map "file_size_bytes", to: :file_size_bytes
57
+ map "table_sharing", to: :table_sharing
58
+ end
59
+
60
+ # Get version as a formatted string
61
+ #
62
+ # @return [String] Version string (e.g., "2.0")
63
+ def version_string
64
+ "#{major_version}.#{minor_version}"
65
+ end
66
+
67
+ # Get version as a hexadecimal string
68
+ #
69
+ # @return [String] Hex version (e.g., "0x00020000")
70
+ def version_hex
71
+ version_int = (major_version << 16) | minor_version
72
+ format("0x%08X", version_int)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "collection_font_summary"
5
+
6
+ module Fontisan
7
+ module Models
8
+ # Model for collection font listing
9
+ #
10
+ # Represents a list of fonts within a TTC/OTC collection.
11
+ # Used by LsCommand when operating on collection files.
12
+ #
13
+ # @example Creating a collection list
14
+ # list = CollectionListInfo.new(
15
+ # collection_path: "fonts.ttc",
16
+ # num_fonts: 6,
17
+ # fonts: [summary1, summary2, ...]
18
+ # )
19
+ class CollectionListInfo < Lutaml::Model::Serializable
20
+ attribute :collection_path, :string
21
+ attribute :num_fonts, :integer
22
+ attribute :fonts, CollectionFontSummary, collection: true
23
+
24
+ yaml do
25
+ map "collection_path", to: :collection_path
26
+ map "num_fonts", to: :num_fonts
27
+ map "fonts", to: :fonts
28
+ end
29
+
30
+ json do
31
+ map "collection_path", to: :collection_path
32
+ map "num_fonts", to: :num_fonts
33
+ map "fonts", to: :fonts
34
+ end
35
+ end
36
+ end
37
+ end