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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ class Cff
8
+ # CFF DICT (Dictionary) structure builder
9
+ #
10
+ # [`DictBuilder`](lib/fontisan/tables/cff/dict_builder.rb) constructs
11
+ # binary DICT structures from hash representations. DICTs in CFF use a
12
+ # compact operand-operator format similar to PostScript.
13
+ #
14
+ # The builder encodes operands in various compact formats and writes
15
+ # operators according to the CFF specification.
16
+ #
17
+ # Operand Encoding:
18
+ # - Small integers (-107 to +107): Single byte (32-246)
19
+ # - Medium integers (108 to 1131): Two bytes (247-250 + byte)
20
+ # - Medium integers (-1131 to -108): Two bytes (251-254 + byte)
21
+ # - Larger integers: Three bytes (28 + 2 bytes) or five bytes (29 + 4 bytes)
22
+ # - Real numbers: Nibble-encoded (30 + nibbles + 0xF terminator)
23
+ #
24
+ # Operators:
25
+ # - Single-byte: 0-21
26
+ # - Two-byte: 12 followed by second byte
27
+ #
28
+ # Reference: CFF specification section 4 "DICT Data"
29
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
30
+ #
31
+ # @example Building a DICT
32
+ # dict_hash = { version: 391, notice: 392, charset: 0 }
33
+ # dict_data = Fontisan::Tables::Cff::DictBuilder.build(dict_hash)
34
+ class DictBuilder
35
+ # Operator mapping (name => byte(s))
36
+ OPERATORS = {
37
+ version: 0,
38
+ notice: 1,
39
+ full_name: 2,
40
+ family_name: 3,
41
+ weight: 4,
42
+ charset: 15,
43
+ encoding: 16,
44
+ charstrings: 17,
45
+ private: 18,
46
+ copyright: [12, 0],
47
+ is_fixed_pitch: [12, 1],
48
+ italic_angle: [12, 2],
49
+ underline_position: [12, 3],
50
+ underline_thickness: [12, 4],
51
+ paint_type: [12, 5],
52
+ charstring_type: [12, 6],
53
+ font_matrix: [12, 7],
54
+ stroke_width: [12, 8],
55
+ font_bbox: 5,
56
+ synthetic_base: [12, 20],
57
+ postscript: [12, 21],
58
+ base_font_name: [12, 22],
59
+ base_font_blend: [12, 23],
60
+ # Private DICT operators
61
+ subrs: 19,
62
+ default_width_x: 20,
63
+ nominal_width_x: 21,
64
+ # Hint-related Private DICT operators
65
+ blue_values: 6,
66
+ other_blues: 7,
67
+ family_blues: 8,
68
+ family_other_blues: 9,
69
+ std_hw: 10,
70
+ std_vw: 11,
71
+ stem_snap_h: [12, 12],
72
+ stem_snap_v: [12, 13],
73
+ blue_scale: [12, 9],
74
+ blue_shift: [12, 10],
75
+ blue_fuzz: [12, 11],
76
+ force_bold: [12, 14],
77
+ language_group: [12, 17],
78
+ }.freeze
79
+
80
+ # Build DICT structure from hash
81
+ #
82
+ # @param dict_hash [Hash] Hash of operator => value pairs
83
+ # @return [String] Binary DICT data
84
+ # @raise [ArgumentError] If dict_hash is invalid
85
+ def self.build(dict_hash)
86
+ validate_dict!(dict_hash)
87
+
88
+ return "".b if dict_hash.empty?
89
+
90
+ output = StringIO.new("".b)
91
+
92
+ # Encode each operator with its operands
93
+ dict_hash.each do |operator_name, value|
94
+ # Get operator bytes
95
+ operator_bytes = operator_for_name(operator_name)
96
+ raise ArgumentError, "Unknown operator: #{operator_name}" unless operator_bytes
97
+
98
+ # Write operands (value can be single value or array)
99
+ if value.is_a?(Array)
100
+ value.each { |v| write_operand(output, v) }
101
+ else
102
+ write_operand(output, value)
103
+ end
104
+
105
+ # Write operator
106
+ write_operator(output, operator_bytes)
107
+ end
108
+
109
+ output.string
110
+ end
111
+
112
+ # Validate dict parameter
113
+ #
114
+ # @param dict_hash [Object] Dictionary to validate
115
+ # @raise [ArgumentError] If dict_hash is invalid
116
+ def self.validate_dict!(dict_hash)
117
+ unless dict_hash.is_a?(Hash)
118
+ raise ArgumentError,
119
+ "dict_hash must be Hash, got: #{dict_hash.class}"
120
+ end
121
+ end
122
+ private_class_method :validate_dict!
123
+
124
+ # Get operator bytes for operator name
125
+ #
126
+ # @param operator_name [Symbol] Operator name
127
+ # @return [Integer, Array<Integer>, nil] Operator byte(s) or nil
128
+ def self.operator_for_name(operator_name)
129
+ OPERATORS[operator_name]
130
+ end
131
+ private_class_method :operator_for_name
132
+
133
+ # Write an operand value to output
134
+ #
135
+ # @param io [StringIO] Output stream
136
+ # @param value [Integer, Float] Operand value
137
+ def self.write_operand(io, value)
138
+ if value.is_a?(Float)
139
+ write_real(io, value)
140
+ else
141
+ write_integer(io, value)
142
+ end
143
+ end
144
+ private_class_method :write_operand
145
+
146
+ # Write an integer operand
147
+ #
148
+ # @param io [StringIO] Output stream
149
+ # @param value [Integer] Integer value
150
+ def self.write_integer(io, value)
151
+ if value >= -107 && value <= 107
152
+ # Single byte: 32-246 represents -107 to +107
153
+ io.putc(value + 139)
154
+ elsif value >= 108 && value <= 1131
155
+ # Positive two-byte: 247-250
156
+ adjusted = value - 108
157
+ b0 = 247 + (adjusted / 256)
158
+ b1 = adjusted % 256
159
+ io.putc(b0)
160
+ io.putc(b1)
161
+ elsif value >= -1131 && value <= -108
162
+ # Negative two-byte: 251-254
163
+ adjusted = -value - 108
164
+ b0 = 251 + (adjusted / 256)
165
+ b1 = adjusted % 256
166
+ io.putc(b0)
167
+ io.putc(b1)
168
+ elsif value >= -32768 && value <= 32767
169
+ # Three-byte signed 16-bit
170
+ io.putc(28)
171
+ io.write([value].pack("s>")) # Signed 16-bit big-endian
172
+ else
173
+ # Five-byte signed 32-bit
174
+ io.putc(29)
175
+ io.write([value].pack("l>")) # Signed 32-bit big-endian
176
+ end
177
+ end
178
+ private_class_method :write_integer
179
+
180
+ # Write a real number operand
181
+ #
182
+ # Real numbers are encoded using nibbles (4-bit values).
183
+ # Each nibble represents a digit or special character.
184
+ #
185
+ # Nibble values:
186
+ # - 0-9: Decimal digits
187
+ # - a (10): Decimal point
188
+ # - b (11): Positive exponent (E)
189
+ # - c (12): Negative exponent (E-)
190
+ # - e (14): Minus sign
191
+ # - f (15): End of number
192
+ #
193
+ # @param io [StringIO] Output stream
194
+ # @param value [Float] Real number value
195
+ def self.write_real(io, value)
196
+ io.putc(30) # Real number marker
197
+
198
+ # Convert to string representation
199
+ str = value.to_s
200
+
201
+ # Handle special cases
202
+ str = "0" if str == "0.0"
203
+
204
+ # Convert string to nibbles
205
+ nibbles = []
206
+
207
+ str.each_char do |char|
208
+ case char
209
+ when "0".."9"
210
+ nibbles << char.to_i
211
+ when "."
212
+ nibbles << 0xa
213
+ when "-"
214
+ nibbles << 0xe
215
+ when "e", "E"
216
+ # Check if next char is minus
217
+ nibbles << 0xb # Default to positive exponent
218
+ end
219
+ end
220
+
221
+ # Handle negative exponent
222
+ if str.include?("e-") || str.include?("E-")
223
+ # Replace last 0xb with 0xc
224
+ exp_index = nibbles.rindex(0xb)
225
+ nibbles[exp_index] = 0xc if exp_index
226
+ end
227
+
228
+ # Add terminator
229
+ nibbles << 0xf
230
+
231
+ # Pack nibbles into bytes
232
+ nibbles.each_slice(2) do |high, low|
233
+ low ||= 0xf # Pad with terminator if odd number
234
+ byte = (high << 4) | low
235
+ io.putc(byte)
236
+ end
237
+ end
238
+ private_class_method :write_real
239
+
240
+ # Write an operator to output
241
+ #
242
+ # @param io [StringIO] Output stream
243
+ # @param operator_bytes [Integer, Array<Integer>] Operator byte(s)
244
+ def self.write_operator(io, operator_bytes)
245
+ if operator_bytes.is_a?(Array)
246
+ # Two-byte operator
247
+ operator_bytes.each { |byte| io.putc(byte) }
248
+ else
249
+ # Single-byte operator
250
+ io.putc(operator_bytes)
251
+ end
252
+ end
253
+ private_class_method :write_operator
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "../../binary/base_record"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ class Cff
9
+ # CFF Encoding structure
10
+ #
11
+ # Encoding maps character codes to glyph IDs (GIDs).
12
+ # GID 0 (.notdef) is not encoded.
13
+ #
14
+ # Three formats:
15
+ # - Format 0: Array of codes (one per glyph)
16
+ # - Format 1: Ranges of consecutive codes
17
+ # - Format 0/1 with supplement: Format 0 or 1 with additional mappings
18
+ #
19
+ # Predefined encodings:
20
+ # - 0: Standard encoding (Adobe standard character set)
21
+ # - 1: Expert encoding (Adobe expert character set)
22
+ #
23
+ # Reference: CFF specification section 14 "Encodings"
24
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
25
+ #
26
+ # @example Reading an Encoding
27
+ # encoding = Fontisan::Tables::Cff::Encoding.new(data, num_glyphs)
28
+ # puts encoding.glyph_id(65) # => GID for char code 65 ('A')
29
+ # puts encoding.char_code(5) # => char code for GID 5
30
+ class Encoding
31
+ # Predefined encoding identifiers
32
+ PREDEFINED = {
33
+ 0 => :standard,
34
+ 1 => :expert,
35
+ }.freeze
36
+
37
+ # Format mask to extract format type
38
+ FORMAT_MASK = 0x7F
39
+
40
+ # @return [Integer] Encoding format (0 or 1)
41
+ attr_reader :format_type
42
+
43
+ # @return [Hash<Integer, Integer>] Map from character code to GID
44
+ attr_reader :code_to_gid
45
+
46
+ # @return [Hash<Integer, Integer>] Map from GID to character code
47
+ attr_reader :gid_to_code
48
+
49
+ # Initialize an Encoding
50
+ #
51
+ # @param data [String, Integer] Binary data or predefined encoding ID
52
+ # @param num_glyphs [Integer] Number of glyphs in the font
53
+ def initialize(data, num_glyphs)
54
+ @num_glyphs = num_glyphs
55
+ @code_to_gid = {}
56
+ @gid_to_code = {}
57
+
58
+ # GID 0 (.notdef) is always at code 0
59
+ @code_to_gid[0] = 0
60
+ @gid_to_code[0] = 0
61
+
62
+ if data.is_a?(Integer) && PREDEFINED.key?(data)
63
+ load_predefined_encoding(data)
64
+ else
65
+ @data = data
66
+ parse!
67
+ end
68
+ end
69
+
70
+ # Get GID for a character code
71
+ #
72
+ # @param code [Integer] Character code (0-255)
73
+ # @return [Integer, nil] Glyph ID or nil if not mapped
74
+ def glyph_id(code)
75
+ @code_to_gid[code]
76
+ end
77
+
78
+ # Get character code for a GID
79
+ #
80
+ # @param gid [Integer] Glyph ID
81
+ # @return [Integer, nil] Character code or nil if not mapped
82
+ def char_code(gid)
83
+ @gid_to_code[gid]
84
+ end
85
+
86
+ # Get the format symbol
87
+ #
88
+ # @return [Symbol] Format identifier (:array, :range, or :predefined)
89
+ def format
90
+ return :predefined unless @format_type
91
+
92
+ @format_type.zero? ? :array : :range
93
+ end
94
+
95
+ # Check if encoding has supplement
96
+ #
97
+ # @return [Boolean] True if encoding has supplemental mappings
98
+ def has_supplement?
99
+ @has_supplement || false
100
+ end
101
+
102
+ private
103
+
104
+ # Parse the Encoding from binary data
105
+ def parse!
106
+ io = StringIO.new(@data)
107
+ format_byte = read_uint8(io)
108
+
109
+ # Extract format (lower 7 bits) and supplement flag (bit 7)
110
+ @format_type = format_byte & FORMAT_MASK
111
+ @has_supplement = (format_byte & 0x80) != 0
112
+
113
+ case @format_type
114
+ when 0
115
+ parse_format_0(io)
116
+ when 1
117
+ parse_format_1(io)
118
+ else
119
+ raise CorruptedTableError,
120
+ "Invalid Encoding format: #{@format_type}"
121
+ end
122
+
123
+ # Parse supplemental encoding if present
124
+ parse_supplement(io) if @has_supplement
125
+ rescue StandardError => e
126
+ raise CorruptedTableError,
127
+ "Failed to parse Encoding: #{e.message}"
128
+ end
129
+
130
+ # Parse Format 0: Array of codes
131
+ #
132
+ # Format 0 directly lists character codes for each glyph (except
133
+ # .notdef)
134
+ #
135
+ # @param io [StringIO] Input stream positioned after format byte
136
+ def parse_format_0(io)
137
+ n_codes = read_uint8(io)
138
+
139
+ # Read one code per glyph (GIDs start at 1, skipping .notdef)
140
+ n_codes.times do |i|
141
+ code = read_uint8(io)
142
+ gid = i + 1 # GID 0 is .notdef, so start at 1
143
+
144
+ @code_to_gid[code] = gid
145
+ @gid_to_code[gid] = code
146
+ end
147
+ end
148
+
149
+ # Parse Format 1: Ranges of codes
150
+ #
151
+ # Format 1 uses ranges: first code, nLeft (number of consecutive codes)
152
+ #
153
+ # @param io [StringIO] Input stream positioned after format byte
154
+ def parse_format_1(io)
155
+ n_ranges = read_uint8(io)
156
+ gid = 1 # Start at GID 1 (skip .notdef at 0)
157
+
158
+ n_ranges.times do
159
+ first_code = read_uint8(io)
160
+ n_left = read_uint8(io)
161
+
162
+ # Map the range of codes
163
+ (n_left + 1).times do |i|
164
+ code = first_code + i
165
+ @code_to_gid[code] = gid
166
+ @gid_to_code[gid] = code
167
+ gid += 1
168
+ end
169
+ end
170
+ end
171
+
172
+ # Parse supplemental encoding
173
+ #
174
+ # Supplemental encoding provides additional code-to-GID mappings
175
+ #
176
+ # @param io [StringIO] Input stream positioned after main encoding data
177
+ def parse_supplement(io)
178
+ n_sups = read_uint8(io)
179
+
180
+ n_sups.times do
181
+ read_uint8(io)
182
+ read_uint16(io)
183
+
184
+ # Find GID for this SID (requires charset lookup)
185
+ # For now, we'll store the code mapping
186
+ # A full implementation would need charset access to resolve SID to
187
+ # GID
188
+ # This is typically used when the charset has glyphs not in the
189
+ # standard encoding
190
+ end
191
+ end
192
+
193
+ # Load a predefined encoding
194
+ #
195
+ # @param encoding_id [Integer] Predefined encoding ID (0 or 1)
196
+ def load_predefined_encoding(encoding_id)
197
+ @format_type = nil # Predefined encodings don't have a format
198
+
199
+ case encoding_id
200
+ when 0
201
+ load_standard_encoding
202
+ when 1
203
+ load_expert_encoding
204
+ end
205
+ end
206
+
207
+ # Load Standard encoding
208
+ #
209
+ # Adobe Standard Encoding is the default encoding for Type 1 fonts
210
+ # It maps common Latin characters to specific codes
211
+ def load_standard_encoding
212
+ # Standard encoding for common characters (codes 0-255)
213
+ # This is a simplified version - a full implementation would include
214
+ # all 256 standard encoding mappings from the CFF specification
215
+ # Appendix B
216
+
217
+ # Common ASCII mappings (basic Latin)
218
+ gid = 1
219
+ (32..126).each do |code|
220
+ @code_to_gid[code] = gid
221
+ @gid_to_code[gid] = code
222
+ gid += 1
223
+ break if gid >= @num_glyphs
224
+ end
225
+ end
226
+
227
+ # Load Expert encoding
228
+ #
229
+ # Adobe Expert Encoding is used for expert fonts with special
230
+ # characters like small caps, old-style figures, ligatures, etc.
231
+ def load_expert_encoding
232
+ # Expert encoding for special characters
233
+ # This is a simplified version - a full implementation would include
234
+ # all expert encoding mappings from the CFF specification Appendix C
235
+
236
+ # Map some common expert characters
237
+ gid = 1
238
+ expert_codes = [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
239
+ 45, 46, 47]
240
+ expert_codes.each do |code|
241
+ @code_to_gid[code] = gid if gid < @num_glyphs
242
+ @gid_to_code[gid] = code if gid < @num_glyphs
243
+ gid += 1
244
+ break if gid >= @num_glyphs
245
+ end
246
+ end
247
+
248
+ # Read an unsigned 8-bit integer
249
+ #
250
+ # @param io [StringIO] Input stream
251
+ # @return [Integer] The value
252
+ def read_uint8(io)
253
+ byte = io.read(1)
254
+ raise CorruptedTableError, "Unexpected end of Encoding data" if
255
+ byte.nil?
256
+
257
+ byte.unpack1("C")
258
+ end
259
+
260
+ # Read an unsigned 16-bit integer (big-endian)
261
+ #
262
+ # @param io [StringIO] Input stream
263
+ # @return [Integer] The value
264
+ def read_uint16(io)
265
+ bytes = io.read(2)
266
+ raise CorruptedTableError, "Unexpected end of Encoding data" if
267
+ bytes.nil? || bytes.bytesize < 2
268
+
269
+ bytes.unpack1("n") # Big-endian unsigned 16-bit
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../binary/base_record"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ class Cff
8
+ # CFF Header structure
9
+ #
10
+ # The CFF header appears at the beginning of the CFF table and contains
11
+ # basic version and structural information about the CFF data.
12
+ #
13
+ # Structure (4 bytes minimum):
14
+ # - uint8: major version (always 1 for CFF, 2 for CFF2)
15
+ # - uint8: minor version (always 0)
16
+ # - uint8: hdr_size (header size in bytes, typically 4)
17
+ # - uint8: off_size (offset size used throughout CFF, 1-4 bytes)
18
+ #
19
+ # Reference: CFF specification section 4 "Header"
20
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
21
+ #
22
+ # @example Reading a CFF header
23
+ # data = File.binread("font.otf", 4, cff_offset)
24
+ # header = Fontisan::Tables::Cff::Header.read(data)
25
+ # puts header.major # => 1
26
+ # puts header.minor # => 0
27
+ # puts header.off_size # => 4
28
+ class Header < Binary::BaseRecord
29
+ # Major version number (1 for CFF, 2 for CFF2)
30
+ uint8 :major
31
+
32
+ # Minor version number (always 0)
33
+ uint8 :minor
34
+
35
+ # Header size in bytes (typically 4, but can be larger for extensions)
36
+ uint8 :hdr_size
37
+
38
+ # Offset size used throughout the CFF table
39
+ # Valid values are 1, 2, 3, or 4 bytes
40
+ #
41
+ # This determines how offsets are encoded in INDEX structures and
42
+ # other parts of the CFF table.
43
+ uint8 :off_size
44
+
45
+ # Check if this is a valid CFF version 1.0 header
46
+ #
47
+ # @return [Boolean] True if major version is 1 and minor is 0
48
+ def cff?
49
+ major == 1 && minor.zero?
50
+ end
51
+
52
+ # Check if this is a CFF2 header (variable CFF fonts)
53
+ #
54
+ # @return [Boolean] True if major version is 2
55
+ def cff2?
56
+ major == 2
57
+ end
58
+
59
+ # Get the version as a string
60
+ #
61
+ # @return [String] Version in "major.minor" format
62
+ def version
63
+ "#{major}.#{minor}"
64
+ end
65
+
66
+ # Validate that the header has correct values
67
+ #
68
+ # @return [Boolean] True if header is valid
69
+ def valid?
70
+ # Major version must be 1 or 2
71
+ return false unless [1, 2].include?(major)
72
+
73
+ # Minor version must be 0
74
+ return false unless minor.zero?
75
+
76
+ # Header size must be at least 4 bytes
77
+ return false unless hdr_size >= 4
78
+
79
+ # Offset size must be between 1 and 4
80
+ return false unless (1..4).cover?(off_size)
81
+
82
+ true
83
+ end
84
+
85
+ # Validate header and raise error if invalid
86
+ #
87
+ # @raise [Fontisan::CorruptedTableError] If header is invalid
88
+ def validate!
89
+ return if valid?
90
+
91
+ message = "Invalid CFF header: " \
92
+ "version=#{version}, " \
93
+ "hdr_size=#{hdr_size}, " \
94
+ "off_size=#{off_size}"
95
+ error = Fontisan::CorruptedTableError.new(message)
96
+ error.set_backtrace(caller)
97
+ Kernel.raise(error)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end