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,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../models/hint"
4
+
5
+ module Fontisan
6
+ module Hints
7
+ # Applies rendering hints to PostScript/CFF CharString data
8
+ #
9
+ # This applier converts universal Hint objects into PostScript hint
10
+ # operators and integrates them into CharString data. It ensures proper
11
+ # operator placement and maintains CharString validity.
12
+ #
13
+ # **PostScript Hint Placement:**
14
+ #
15
+ # - Stem hints (hstem/vstem) must appear at the beginning
16
+ # - Hintmask operators can appear throughout the CharString
17
+ # - Hints affect all subsequent path operations
18
+ #
19
+ # @example Apply hints to a CharString
20
+ # applier = PostScriptHintApplier.new
21
+ # charstring_with_hints = applier.apply(charstring, hints)
22
+ class PostScriptHintApplier
23
+ # CFF CharString operators
24
+ HSTEM = 1
25
+ VSTEM = 3
26
+ HINTMASK = 19
27
+ CNTRMASK = 20
28
+ HSTEM3 = [12, 2]
29
+ VSTEM3 = [12, 1]
30
+
31
+ # Apply hints to CharString
32
+ #
33
+ # @param charstring [String] Original CharString bytes
34
+ # @param hints [Array<Hint>] Hints to apply
35
+ # @return [String] CharString with applied hints
36
+ def apply(charstring, hints)
37
+ return charstring if hints.nil? || hints.empty?
38
+ return charstring if charstring.nil? || charstring.empty?
39
+
40
+ # Build hint operators
41
+ hint_ops = build_hint_operators(hints)
42
+
43
+ # Insert hints at the beginning of CharString
44
+ # (simplified - real implementation would analyze existing structure)
45
+ hint_ops + charstring
46
+ end
47
+
48
+ private
49
+
50
+ # Build hint operators from hints
51
+ #
52
+ # @param hints [Array<Hint>] Hints to convert
53
+ # @return [String] Hint operator bytes
54
+ def build_hint_operators(hints)
55
+ operators = "".b
56
+
57
+ # Group hints by type for proper ordering
58
+ stem_hints = hints.select { |h| h.type == :stem }
59
+ stem3_hints = hints.select { |h| h.type == :stem3 }
60
+ mask_hints = hints.select { |h| %i[hint_replacement counter].include?(h.type) }
61
+
62
+ # Add stem hints first
63
+ stem_hints.each do |hint|
64
+ operators << encode_stem_hint(hint)
65
+ end
66
+
67
+ # Add stem3 hints
68
+ stem3_hints.each do |hint|
69
+ operators << encode_stem3_hint(hint)
70
+ end
71
+
72
+ # Add mask hints
73
+ mask_hints.each do |hint|
74
+ operators << encode_mask_hint(hint)
75
+ end
76
+
77
+ operators
78
+ end
79
+
80
+ # Encode stem hint as CharString bytes
81
+ #
82
+ # @param hint [Hint] Stem hint
83
+ # @return [String] Encoded bytes
84
+ def encode_stem_hint(hint)
85
+ data = hint.to_postscript
86
+ return "".b if data.empty?
87
+
88
+ args = data[:args] || []
89
+ operator = data[:operator]
90
+
91
+ # Encode arguments as CFF integers
92
+ bytes = args.map { |arg| encode_cff_integer(arg) }.join
93
+
94
+ # Add operator
95
+ bytes << if operator == :vstem
96
+ [VSTEM].pack("C")
97
+ else
98
+ [HSTEM].pack("C")
99
+ end
100
+
101
+ bytes
102
+ end
103
+
104
+ # Encode stem3 hint as CharString bytes
105
+ #
106
+ # @param hint [Hint] Stem3 hint
107
+ # @return [String] Encoded bytes
108
+ def encode_stem3_hint(hint)
109
+ data = hint.to_postscript
110
+ return "".b if data.empty?
111
+
112
+ args = data[:args] || []
113
+ operator = data[:operator]
114
+
115
+ # Encode arguments
116
+ bytes = args.map { |arg| encode_cff_integer(arg) }.join
117
+
118
+ # Add two-byte operator (12 followed by subop)
119
+ bytes << if operator == :vstem3
120
+ VSTEM3.pack("C*")
121
+ else
122
+ HSTEM3.pack("C*")
123
+ end
124
+
125
+ bytes
126
+ end
127
+
128
+ # Encode mask hint as CharString bytes
129
+ #
130
+ # @param hint [Hint] Mask hint
131
+ # @return [String] Encoded bytes
132
+ def encode_mask_hint(hint)
133
+ operator = hint.type == :hint_replacement ? HINTMASK : CNTRMASK
134
+ mask = hint.data[:mask] || []
135
+
136
+ # Encode mask bytes
137
+ bytes = mask.map { |b| [b].pack("C") }.join
138
+
139
+ # Add operator
140
+ bytes + [operator].pack("C")
141
+ end
142
+
143
+ # Encode integer as CFF CharString number
144
+ #
145
+ # @param num [Integer] Number to encode
146
+ # @return [String] Encoded bytes
147
+ def encode_cff_integer(num)
148
+ # Range 1: -107 to 107 (single byte)
149
+ if num >= -107 && num <= 107
150
+ return [32 + num].pack("c")
151
+ end
152
+
153
+ # Range 2: 108 to 1131 (two bytes)
154
+ if num >= 108 && num <= 1131
155
+ b0 = 247 + ((num - 108) >> 8)
156
+ b1 = (num - 108) & 0xff
157
+ return [b0, b1].pack("C*")
158
+ end
159
+
160
+ # Range 3: -1131 to -108 (two bytes)
161
+ if num >= -1131 && num <= -108
162
+ b0 = 251 - ((num + 108) >> 8)
163
+ b1 = -(num + 108) & 0xff
164
+ return [b0, b1].pack("C*")
165
+ end
166
+
167
+ # Range 4: -32768 to 32767 (three bytes)
168
+ if num >= -32_768 && num <= 32_767
169
+ bytes = [28, (num >> 8) & 0xff, num & 0xff]
170
+ return bytes.pack("C*")
171
+ end
172
+
173
+ # Range 5: Larger numbers (five bytes)
174
+ bytes = [
175
+ 255,
176
+ (num >> 24) & 0xff,
177
+ (num >> 16) & 0xff,
178
+ (num >> 8) & 0xff,
179
+ num & 0xff
180
+ ]
181
+ bytes.pack("C*")
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../models/hint"
4
+
5
+ module Fontisan
6
+ module Hints
7
+ # Extracts rendering hints from PostScript/CFF CharString data
8
+ #
9
+ # PostScript Type 1 and CFF fonts embed hints directly in the
10
+ # CharString data as operators. This extractor parses CharString
11
+ # sequences to identify and extract hint operators.
12
+ #
13
+ # **Supported PostScript Hint Operators:**
14
+ #
15
+ # - hstem/vstem - Horizontal/vertical stem hints
16
+ # - hstem3/vstem3 - Multiple stem hints
17
+ # - hintmask - Hint replacement masks
18
+ # - cntrmask - Counter control masks
19
+ #
20
+ # @example Extract hints from a CharString
21
+ # extractor = PostScriptHintExtractor.new
22
+ # hints = extractor.extract(charstring)
23
+ class PostScriptHintExtractor
24
+ # CFF CharString operators
25
+ HSTEM = 1
26
+ VSTEM = 3
27
+ HINTMASK = 19
28
+ CNTRMASK = 20
29
+ HSTEM3 = 12 << 8 | 2
30
+ VSTEM3 = 12 << 8 | 1
31
+
32
+ # Extract hints from CFF CharString
33
+ #
34
+ # @param charstring [CharString, String] CFF CharString object or bytes
35
+ # @return [Array<Hint>] Extracted hints
36
+ def extract(charstring)
37
+ return [] if charstring.nil?
38
+
39
+ # Get CharString bytes
40
+ bytes = if charstring.respond_to?(:data)
41
+ charstring.data
42
+ elsif charstring.respond_to?(:bytes)
43
+ charstring.bytes
44
+ elsif charstring.is_a?(String)
45
+ charstring.bytes
46
+ else
47
+ return []
48
+ end
49
+
50
+ return [] if bytes.empty?
51
+
52
+ parse_charstring(bytes)
53
+ end
54
+
55
+ private
56
+
57
+ # Parse CharString bytes to extract hints
58
+ #
59
+ # @param bytes [Array<Integer>] CharString bytes
60
+ # @return [Array<Hint>] Extracted hints
61
+ def parse_charstring(bytes)
62
+ hints = []
63
+ stack = []
64
+ i = 0
65
+
66
+ while i < bytes.length
67
+ byte = bytes[i]
68
+
69
+ if operator?(byte)
70
+ # Process operator
71
+ operator = if byte == 12
72
+ # Two-byte operator
73
+ i += 1
74
+ (12 << 8) | bytes[i]
75
+ else
76
+ byte
77
+ end
78
+
79
+ hint = process_operator(operator, stack)
80
+ hints << hint if hint
81
+
82
+ # Clear stack after operator
83
+ stack.clear
84
+ i += 1
85
+ else
86
+ # Number - push to stack
87
+ num, consumed = decode_number(bytes, i)
88
+ stack << num if num
89
+ i += consumed
90
+ end
91
+ end
92
+
93
+ hints
94
+ end
95
+
96
+ # Check if byte is an operator
97
+ #
98
+ # @param byte [Integer] Byte value
99
+ # @return [Boolean] True if operator
100
+ def operator?(byte)
101
+ byte <= 31 || byte == 255
102
+ end
103
+
104
+ # Decode a number from CharString
105
+ #
106
+ # @param bytes [Array<Integer>] CharString bytes
107
+ # @param index [Integer] Starting position
108
+ # @return [Array<Integer, Integer>] [number, bytes_consumed]
109
+ def decode_number(bytes, index)
110
+ byte = bytes[index]
111
+ return [nil, 1] if byte.nil?
112
+
113
+ case byte
114
+ when 28
115
+ # 3-byte signed integer
116
+ if index + 2 < bytes.length
117
+ num = (bytes[index + 1] << 8) | bytes[index + 2]
118
+ num = num - 65536 if num > 32767
119
+ [num, 3]
120
+ else
121
+ [nil, 1]
122
+ end
123
+ when 32..246
124
+ # Single byte integer
125
+ [byte - 139, 1]
126
+ when 247..250
127
+ # Positive 2-byte integer
128
+ if index + 1 < bytes.length
129
+ num = (byte - 247) * 256 + bytes[index + 1] + 108
130
+ [num, 2]
131
+ else
132
+ [nil, 1]
133
+ end
134
+ when 251..254
135
+ # Negative 2-byte integer
136
+ if index + 1 < bytes.length
137
+ num = -(byte - 251) * 256 - bytes[index + 1] - 108
138
+ [num, 2]
139
+ else
140
+ [nil, 1]
141
+ end
142
+ when 255
143
+ # 5-byte signed integer
144
+ if index + 4 < bytes.length
145
+ num = (bytes[index + 1] << 24) | (bytes[index + 2] << 16) |
146
+ (bytes[index + 3] << 8) | bytes[index + 4]
147
+ num = num - 4294967296 if num > 2147483647
148
+ [num, 5]
149
+ else
150
+ [nil, 1]
151
+ end
152
+ else
153
+ [nil, 1]
154
+ end
155
+ end
156
+
157
+ # Process hint operator and create Hint object
158
+ #
159
+ # @param operator [Integer] Operator code
160
+ # @param stack [Array<Integer>] Current operand stack
161
+ # @return [Hint, nil] Hint object if operator is a hint
162
+ def process_operator(operator, stack)
163
+ case operator
164
+ when HSTEM
165
+ # Horizontal stem hint
166
+ extract_stem_hint(stack, :horizontal)
167
+
168
+ when VSTEM
169
+ # Vertical stem hint
170
+ extract_stem_hint(stack, :vertical)
171
+
172
+ when HSTEM3
173
+ # Multiple horizontal stems
174
+ extract_stem3_hint(stack, :horizontal)
175
+
176
+ when VSTEM3
177
+ # Multiple vertical stems
178
+ extract_stem3_hint(stack, :vertical)
179
+
180
+ when HINTMASK
181
+ # Hint replacement mask
182
+ Models::Hint.new(
183
+ type: :hint_replacement,
184
+ data: { mask: stack.dup },
185
+ source_format: :postscript
186
+ )
187
+
188
+ when CNTRMASK
189
+ # Counter control mask
190
+ Models::Hint.new(
191
+ type: :counter,
192
+ data: { zones: stack.dup },
193
+ source_format: :postscript
194
+ )
195
+
196
+ else
197
+ nil
198
+ end
199
+ end
200
+
201
+ # Extract stem hint from stack
202
+ #
203
+ # @param stack [Array<Integer>] Operand stack
204
+ # @param orientation [Symbol] :horizontal or :vertical
205
+ # @return [Hint] Stem hint
206
+ def extract_stem_hint(stack, orientation)
207
+ # Stack should have pairs of [position, width]
208
+ return nil if stack.empty? || stack.length < 2
209
+
210
+ # Take first pair
211
+ position = stack[0]
212
+ width = stack[1]
213
+
214
+ Models::Hint.new(
215
+ type: :stem,
216
+ data: {
217
+ position: position,
218
+ width: width,
219
+ orientation: orientation
220
+ },
221
+ source_format: :postscript
222
+ )
223
+ end
224
+
225
+ # Extract stem3 hint from stack
226
+ #
227
+ # @param stack [Array<Integer>] Operand stack
228
+ # @param orientation [Symbol] :horizontal or :vertical
229
+ # @return [Hint] Stem3 hint
230
+ def extract_stem3_hint(stack, orientation)
231
+ # Stack should have 6 values: 3 pairs of [position, width]
232
+ return nil if stack.length < 6
233
+
234
+ stems = []
235
+ (0..2).each do |i|
236
+ pos_idx = i * 2
237
+ stems << {
238
+ position: stack[pos_idx],
239
+ width: stack[pos_idx + 1]
240
+ }
241
+ end
242
+
243
+ Models::Hint.new(
244
+ type: :stem3,
245
+ data: {
246
+ stems: stems,
247
+ orientation: orientation
248
+ },
249
+ source_format: :postscript
250
+ )
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../models/hint"
4
+
5
+ module Fontisan
6
+ module Hints
7
+ # Applies rendering hints to TrueType glyph data
8
+ #
9
+ # This applier converts universal Hint objects into TrueType bytecode
10
+ # instructions and integrates them into glyph data. It ensures proper
11
+ # instruction sequencing and maintains compatibility with TrueType
12
+ # instruction execution model.
13
+ #
14
+ # @example Apply hints to a glyph
15
+ # applier = TrueTypeHintApplier.new
16
+ # glyph_with_hints = applier.apply(glyph, hints)
17
+ class TrueTypeHintApplier
18
+ # Apply hints to TrueType glyph
19
+ #
20
+ # @param glyph [Glyph] Target glyph
21
+ # @param hints [Array<Hint>] Hints to apply
22
+ # @return [Glyph] Glyph with applied hints
23
+ def apply(glyph, hints)
24
+ return glyph if hints.nil? || hints.empty?
25
+ return glyph if glyph.nil?
26
+
27
+ # Convert hints to TrueType instructions
28
+ instructions = build_instructions(hints)
29
+
30
+ # Apply to glyph (this is a simplified version)
31
+ # In a real implementation, we would need to:
32
+ # 1. Analyze existing glyph structure
33
+ # 2. Insert instructions at appropriate points
34
+ # 3. Update glyph instruction data
35
+
36
+ # For now, we just return the glyph as-is since
37
+ # this is a complex operation requiring deep integration
38
+ # with the glyph structure
39
+ glyph
40
+ end
41
+
42
+ private
43
+
44
+ # Build TrueType instruction sequence from hints
45
+ #
46
+ # @param hints [Array<Hint>] Hints to convert
47
+ # @return [Array<Integer>] Instruction bytes
48
+ def build_instructions(hints)
49
+ instructions = []
50
+
51
+ hints.each do |hint|
52
+ hint_instructions = hint.to_truetype
53
+ instructions.concat(hint_instructions) if hint_instructions
54
+ end
55
+
56
+ instructions
57
+ end
58
+
59
+ # Validate instruction sequence
60
+ #
61
+ # @param instructions [Array<Integer>] Instructions to validate
62
+ # @return [Boolean] True if valid
63
+ def valid_instructions?(instructions)
64
+ return true if instructions.empty?
65
+
66
+ # Basic validation - check for valid opcodes
67
+ instructions.all? { |byte| byte >= 0 && byte <= 255 }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../models/hint"
4
+
5
+ module Fontisan
6
+ module Hints
7
+ # Extracts rendering hints from TrueType glyph data
8
+ #
9
+ # TrueType uses bytecode instructions for hinting. This extractor
10
+ # analyzes glyph instruction sequences and converts them into
11
+ # universal Hint objects for format-agnostic representation.
12
+ #
13
+ # **Supported TrueType Instructions:**
14
+ #
15
+ # - MDAP - Move Direct Absolute Point (stem positioning)
16
+ # - MDRP - Move Direct Relative Point (stem width)
17
+ # - IUP - Interpolate Untouched Points (smooth interpolation)
18
+ # - SHP - Shift Point (point adjustments)
19
+ # - ALIGNRP - Align to Reference Point (alignment)
20
+ # - DELTA - Delta instructions (pixel-level adjustments)
21
+ #
22
+ # @example Extract hints from a glyph
23
+ # extractor = TrueTypeHintExtractor.new
24
+ # hints = extractor.extract(glyph)
25
+ class TrueTypeHintExtractor
26
+ # TrueType instruction opcodes
27
+ MDAP_RND = 0x2E
28
+ MDAP_NORND = 0x2F
29
+ MDRP_MIN_RND_BLACK = 0xC0
30
+ IUP_Y = 0x30
31
+ IUP_X = 0x31
32
+ SHP = [0x32, 0x33]
33
+ ALIGNRP = 0x3C
34
+ DELTAP1 = 0x5D
35
+ DELTAP2 = 0x71
36
+ DELTAP3 = 0x72
37
+
38
+ # Extract hints from TrueType glyph
39
+ #
40
+ # @param glyph [Glyph] TrueType glyph with instructions
41
+ # @return [Array<Hint>] Extracted hints
42
+ def extract(glyph)
43
+ return [] if glyph.nil? || glyph.empty?
44
+ return [] unless glyph.respond_to?(:instructions)
45
+
46
+ instructions = glyph.instructions || []
47
+ return [] if instructions.empty?
48
+
49
+ parse_instructions(instructions)
50
+ end
51
+
52
+ private
53
+
54
+ # Parse TrueType instruction bytes into Hint objects
55
+ #
56
+ # @param instructions [String, Array<Integer>] Instruction bytes
57
+ # @return [Array<Hint>] Parsed hints
58
+ def parse_instructions(instructions)
59
+ hints = []
60
+ bytes = instructions.is_a?(String) ? instructions.bytes : instructions
61
+ i = 0
62
+
63
+ while i < bytes.length
64
+ opcode = bytes[i]
65
+
66
+ case opcode
67
+ when MDAP_RND, MDAP_NORND
68
+ # Stem positioning hint
69
+ hint = extract_stem_hint(bytes, i)
70
+ hints << hint if hint
71
+ i += 1
72
+
73
+ when MDRP_MIN_RND_BLACK
74
+ # Stem width hint (usually follows MDAP)
75
+ # This is typically part of a stem hint pair
76
+ i += 1
77
+
78
+ when IUP_Y, IUP_X
79
+ # Interpolation hint
80
+ hints << Models::Hint.new(
81
+ type: :interpolate,
82
+ data: { axis: opcode == IUP_Y ? :y : :x },
83
+ source_format: :truetype
84
+ )
85
+ i += 1
86
+
87
+ when *SHP
88
+ # Shift point hint
89
+ hints << Models::Hint.new(
90
+ type: :shift,
91
+ data: { instructions: [opcode] },
92
+ source_format: :truetype
93
+ )
94
+ i += 1
95
+
96
+ when ALIGNRP
97
+ # Alignment hint
98
+ hints << Models::Hint.new(
99
+ type: :align,
100
+ data: {},
101
+ source_format: :truetype
102
+ )
103
+ i += 1
104
+
105
+ when DELTAP1, DELTAP2, DELTAP3
106
+ # Delta hint - pixel-level adjustments
107
+ # Next byte is the count
108
+ i += 1
109
+ if i < bytes.length
110
+ count = bytes[i]
111
+ delta_data = bytes[i + 1, count * 2] || []
112
+ hints << Models::Hint.new(
113
+ type: :delta,
114
+ data: {
115
+ instructions: [opcode] + [count] + delta_data,
116
+ count: count
117
+ },
118
+ source_format: :truetype
119
+ )
120
+ i += count * 2 + 1
121
+ end
122
+
123
+ else
124
+ # Unknown or data bytes - skip
125
+ i += 1
126
+ end
127
+ end
128
+
129
+ hints
130
+ end
131
+
132
+ # Extract stem hint from MDAP instruction
133
+ #
134
+ # @param bytes [Array<Integer>] Instruction bytes
135
+ # @param index [Integer] Current position
136
+ # @return [Hint, nil] Stem hint if found
137
+ def extract_stem_hint(bytes, index)
138
+ # In TrueType, stem hints are inferred from MDAP + MDRP pairs
139
+ # This is a simplified extraction - real implementation would
140
+ # need to track the graphics state and point references
141
+
142
+ # Check if next instruction is MDRP (stem width)
143
+ has_width = index + 1 < bytes.length &&
144
+ bytes[index + 1] == MDRP_MIN_RND_BLACK
145
+
146
+ if has_width
147
+ Models::Hint.new(
148
+ type: :stem,
149
+ data: {
150
+ position: 0, # Would be extracted from graphics state
151
+ width: 0, # Would be calculated from MDRP
152
+ orientation: :vertical # Inferred from instruction context
153
+ },
154
+ source_format: :truetype
155
+ )
156
+ else
157
+ nil
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end