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,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "charstring_parser"
4
+ require_relative "charstring_builder"
5
+ require_relative "index_builder"
6
+
7
+ module Fontisan
8
+ module Tables
9
+ class Cff
10
+ # Rebuilds CharStrings INDEX with modified CharStrings
11
+ #
12
+ # CharStringRebuilder provides high-level interface for modifying
13
+ # CharStrings in a CFF font. It extracts all CharStrings from the source
14
+ # INDEX, allows modifications through a callback, and rebuilds the INDEX
15
+ # with updated CharString data.
16
+ #
17
+ # Use Cases:
18
+ # - Per-glyph hint injection
19
+ # - CharString optimization
20
+ # - Subroutine insertion
21
+ # - Any operation requiring CharString modification
22
+ #
23
+ # @example Inject hints into specific glyphs
24
+ # rebuilder = CharStringRebuilder.new(charstrings_index)
25
+ # rebuilder.modify_charstring(42) do |operations|
26
+ # # Insert hint operations at beginning
27
+ # hint_ops = [
28
+ # { type: :operator, name: :hstem, operands: [10, 20] }
29
+ # ]
30
+ # hint_ops + operations
31
+ # end
32
+ # new_index_data = rebuilder.rebuild
33
+ class CharStringRebuilder
34
+ # @return [CharstringsIndex] Source CharStrings INDEX
35
+ attr_reader :source_index
36
+
37
+ # @return [Hash] Modified CharString data by glyph index
38
+ attr_reader :modifications
39
+
40
+ # Initialize rebuilder with source CharStrings INDEX
41
+ #
42
+ # @param source_index [CharstringsIndex] Source CharStrings INDEX
43
+ # @param stem_count [Integer] Number of stem hints (for parsing hintmask)
44
+ def initialize(source_index, stem_count: 0)
45
+ @source_index = source_index
46
+ @stem_count = stem_count
47
+ @modifications = {}
48
+ end
49
+
50
+ # Modify a CharString by glyph index
51
+ #
52
+ # The block receives the parsed operations for the glyph and should
53
+ # return modified operations.
54
+ #
55
+ # @param glyph_index [Integer] Glyph index (0 = .notdef)
56
+ # @yield [operations] Block to modify operations
57
+ # @yieldparam operations [Array<Hash>] Parsed operations
58
+ # @yieldreturn [Array<Hash>] Modified operations
59
+ def modify_charstring(glyph_index, &block)
60
+ # Get original CharString data
61
+ original_data = @source_index[glyph_index]
62
+ return unless original_data
63
+
64
+ # Parse to operations
65
+ parser = CharStringParser.new(original_data, stem_count: @stem_count)
66
+ operations = parser.parse
67
+
68
+ # Apply modification
69
+ modified_operations = block.call(operations)
70
+
71
+ # Build new CharString
72
+ new_data = CharStringBuilder.build_from_operations(modified_operations)
73
+
74
+ # Store modification
75
+ @modifications[glyph_index] = new_data
76
+ end
77
+
78
+ # Rebuild CharStrings INDEX with modifications
79
+ #
80
+ # Creates new INDEX with modified CharStrings, keeping unmodified
81
+ # CharStrings unchanged.
82
+ #
83
+ # @return [String] Binary CharStrings INDEX data
84
+ def rebuild
85
+ # Collect all CharString data (modified and unmodified)
86
+ charstrings = []
87
+
88
+ (0...@source_index.count).each do |i|
89
+ if @modifications.key?(i)
90
+ # Use modified CharString
91
+ charstrings << @modifications[i]
92
+ else
93
+ # Use original CharString
94
+ charstrings << @source_index[i]
95
+ end
96
+ end
97
+
98
+ # Build INDEX
99
+ IndexBuilder.build(charstrings)
100
+ end
101
+
102
+ # Batch modify multiple CharStrings
103
+ #
104
+ # More efficient than calling modify_charstring multiple times.
105
+ #
106
+ # @param glyph_indices [Array<Integer>] Glyph indices to modify
107
+ # @yield [glyph_index, operations] Block to modify each glyph
108
+ # @yieldparam glyph_index [Integer] Current glyph index
109
+ # @yieldparam operations [Array<Hash>] Parsed operations
110
+ # @yieldreturn [Array<Hash>] Modified operations
111
+ def batch_modify(glyph_indices, &block)
112
+ glyph_indices.each do |glyph_index|
113
+ modify_charstring(glyph_index) do |operations|
114
+ block.call(glyph_index, operations)
115
+ end
116
+ end
117
+ end
118
+
119
+ # Modify all CharStrings
120
+ #
121
+ # Applies the same modification to every glyph.
122
+ #
123
+ # @yield [glyph_index, operations] Block to modify each glyph
124
+ # @yieldparam glyph_index [Integer] Current glyph index
125
+ # @yieldparam operations [Array<Hash>] Parsed operations
126
+ # @yieldreturn [Array<Hash>] Modified operations
127
+ def modify_all(&block)
128
+ (0...@source_index.count).each do |i|
129
+ modify_charstring(i) do |operations|
130
+ block.call(i, operations)
131
+ end
132
+ end
133
+ end
134
+
135
+ # Get CharString data (modified or original)
136
+ #
137
+ # @param glyph_index [Integer] Glyph index
138
+ # @return [String] CharString binary data
139
+ def charstring_data(glyph_index)
140
+ @modifications[glyph_index] || @source_index[glyph_index]
141
+ end
142
+
143
+ # Check if glyph has been modified
144
+ #
145
+ # @param glyph_index [Integer] Glyph index
146
+ # @return [Boolean] True if modified
147
+ def modified?(glyph_index)
148
+ @modifications.key?(glyph_index)
149
+ end
150
+
151
+ # Get count of modified glyphs
152
+ #
153
+ # @return [Integer] Number of modified glyphs
154
+ def modification_count
155
+ @modifications.size
156
+ end
157
+
158
+ # Clear all modifications
159
+ def clear_modifications
160
+ @modifications.clear
161
+ end
162
+
163
+ # Update stem count (needed for hintmask parsing)
164
+ #
165
+ # @param count [Integer] Number of stem hints
166
+ def stem_count=(count)
167
+ @stem_count = count
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "index"
4
+ require_relative "charstring"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ class Cff
9
+ # CharStrings INDEX wrapper
10
+ #
11
+ # This class wraps the CharStrings INDEX to provide convenient access
12
+ # to individual CharString objects. The CharStrings INDEX contains the
13
+ # glyph outline programs (Type 2 CharStrings) for each glyph in the font.
14
+ #
15
+ # CharStrings Format:
16
+ # - INDEX structure containing binary CharString data
17
+ # - Each entry is a Type 2 CharString program
18
+ # - Number of entries typically matches the number of glyphs
19
+ # - Index 0 is typically .notdef glyph
20
+ #
21
+ # Usage:
22
+ # 1. Create from raw CharStrings INDEX data
23
+ # 2. Provide Private DICT and subroutine INDEXes for interpretation
24
+ # 3. Access individual CharStrings by glyph index
25
+ #
26
+ # Reference: CFF specification section 16 "Local/Global Subrs INDEXes"
27
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
28
+ #
29
+ # @example Using CharStringsIndex
30
+ # # Get CharStrings INDEX from CFF table
31
+ # charstrings_offset = top_dict.charstrings
32
+ # io = StringIO.new(cff.raw_data)
33
+ # io.seek(charstrings_offset)
34
+ # charstrings_index = CharstringsIndex.new(io, start_offset:
35
+ # charstrings_offset)
36
+ #
37
+ # # Get a specific CharString
38
+ # charstring = charstrings_index.charstring_at(
39
+ # glyph_index,
40
+ # private_dict,
41
+ # global_subrs,
42
+ # local_subrs
43
+ # )
44
+ #
45
+ # # Access CharString properties
46
+ # puts charstring.width
47
+ # puts charstring.bounding_box
48
+ # charstring.to_commands.each { |cmd| puts cmd.inspect }
49
+ class CharstringsIndex < Index
50
+ # Get a CharString object at the specified glyph index
51
+ #
52
+ # This method retrieves the binary CharString data at the given index
53
+ # and interprets it as a Type 2 CharString program.
54
+ #
55
+ # @param index [Integer] Glyph index (0-based, 0 is typically .notdef)
56
+ # @param private_dict [PrivateDict] Private DICT for width defaults
57
+ # @param global_subrs [Index] Global subroutines INDEX
58
+ # @param local_subrs [Index, nil] Local subroutines INDEX (optional)
59
+ # @return [CharString, nil] Interpreted CharString object, or nil if
60
+ # index is out of bounds
61
+ #
62
+ # @example Getting a CharString
63
+ # charstring = charstrings_index.charstring_at(
64
+ # 42,
65
+ # private_dict,
66
+ # global_subrs,
67
+ # local_subrs
68
+ # )
69
+ # puts "Width: #{charstring.width}"
70
+ # puts "Bounding box: #{charstring.bounding_box.inspect}"
71
+ def charstring_at(index, private_dict, global_subrs, local_subrs = nil)
72
+ data = self[index]
73
+ return nil unless data
74
+
75
+ CharString.new(data, private_dict, global_subrs, local_subrs)
76
+ end
77
+
78
+ # Get all CharStrings as an array of CharString objects
79
+ #
80
+ # This method interprets all CharStrings in the INDEX. Use with
81
+ # caution for fonts with many glyphs as this can be memory-intensive.
82
+ #
83
+ # @param private_dict [PrivateDict] Private DICT for width defaults
84
+ # @param global_subrs [Index] Global subroutines INDEX
85
+ # @param local_subrs [Index, nil] Local subroutines INDEX (optional)
86
+ # @return [Array<CharString>] Array of interpreted CharString objects
87
+ #
88
+ # @example Getting all CharStrings
89
+ # charstrings = charstrings_index.all_charstrings(
90
+ # private_dict,
91
+ # global_subrs,
92
+ # local_subrs
93
+ # )
94
+ # charstrings.each_with_index do |cs, i|
95
+ # puts "Glyph #{i}: width=#{cs.width}, bbox=#{cs.bounding_box}"
96
+ # end
97
+ def all_charstrings(private_dict, global_subrs, local_subrs = nil)
98
+ Array.new(count) do |i|
99
+ charstring_at(i, private_dict, global_subrs, local_subrs)
100
+ end
101
+ end
102
+
103
+ # Iterate over each CharString in the INDEX
104
+ #
105
+ # This method yields each CharString as it is interpreted, which is
106
+ # more memory-efficient than loading all at once.
107
+ #
108
+ # @param private_dict [PrivateDict] Private DICT for width defaults
109
+ # @param global_subrs [Index] Global subroutines INDEX
110
+ # @param local_subrs [Index, nil] Local subroutines INDEX (optional)
111
+ # @yield [CharString, Integer] Interpreted CharString and its index
112
+ # @return [Enumerator] If no block given
113
+ #
114
+ # @example Iterating over CharStrings
115
+ # charstrings_index.each_charstring(private_dict, global_subrs,
116
+ # local_subrs) do |cs, index|
117
+ # puts "Glyph #{index}: #{cs.bounding_box}"
118
+ # end
119
+ def each_charstring(private_dict, global_subrs, local_subrs = nil)
120
+ unless block_given?
121
+ return enum_for(:each_charstring, private_dict, global_subrs,
122
+ local_subrs)
123
+ end
124
+
125
+ count.times do |i|
126
+ charstring = charstring_at(i, private_dict, global_subrs,
127
+ local_subrs)
128
+ yield charstring, i if charstring
129
+ end
130
+ end
131
+
132
+ # Get the number of glyphs (CharStrings) in this INDEX
133
+ #
134
+ # This is typically the same as the number of glyphs in the font.
135
+ #
136
+ # @return [Integer] Number of glyphs
137
+ def glyph_count
138
+ count
139
+ end
140
+
141
+ # Check if a glyph index is valid
142
+ #
143
+ # @param index [Integer] Glyph index to check
144
+ # @return [Boolean] True if index is valid
145
+ def valid_glyph_index?(index)
146
+ index >= 0 && index < count
147
+ end
148
+
149
+ # Get the size of a CharString in bytes
150
+ #
151
+ # This returns the size of the binary CharString data without
152
+ # interpreting it.
153
+ #
154
+ # @param index [Integer] Glyph index
155
+ # @return [Integer, nil] Size in bytes, or nil if index is invalid
156
+ def charstring_size(index)
157
+ item_size(index)
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,351 @@
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 DICT (Dictionary) structure parser
10
+ #
11
+ # DICTs in CFF use a compact operand-operator format similar to PostScript.
12
+ # Operands are pushed onto a stack, then an operator consumes them.
13
+ #
14
+ # Operand Encoding:
15
+ # - 32-247: Small integers (values -107 to +107)
16
+ # - 28: 3-byte signed integer follows
17
+ # - 29: 5-byte signed integer follows
18
+ # - 30: Real number (nibble-encoded)
19
+ # - 247-254: 2-byte signed integers
20
+ # - 255: Reserved
21
+ # - 0-21, 22-27: Operators (single or two-byte)
22
+ #
23
+ # Reference: CFF specification section 4 "DICT Data"
24
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
25
+ #
26
+ # @example Parsing a DICT
27
+ # data = top_dict_index[0]
28
+ # dict = Fontisan::Tables::Cff::Dict.new(data)
29
+ # puts dict[:charset] # => offset to charset
30
+ # puts dict[:version] # => version SID
31
+ class Dict
32
+ # Common DICT operators shared across Top DICT and Private DICT
33
+ #
34
+ # Key: operator byte(s), Value: operator name symbol
35
+ OPERATORS = {
36
+ 0 => :version,
37
+ 1 => :notice,
38
+ 2 => :full_name,
39
+ 3 => :family_name,
40
+ 4 => :weight,
41
+ [12, 0] => :copyright,
42
+ [12, 1] => :is_fixed_pitch,
43
+ [12, 2] => :italic_angle,
44
+ [12, 3] => :underline_position,
45
+ [12, 4] => :underline_thickness,
46
+ [12, 5] => :paint_type,
47
+ [12, 6] => :charstring_type,
48
+ [12, 7] => :font_matrix,
49
+ [12, 8] => :stroke_width,
50
+ [12, 20] => :synthetic_base,
51
+ [12, 21] => :postscript,
52
+ [12, 22] => :base_font_name,
53
+ [12, 23] => :base_font_blend,
54
+ }.freeze
55
+
56
+ # @return [Hash] Parsed dictionary as key-value pairs
57
+ attr_reader :dict
58
+
59
+ # @return [String] Raw binary data of the DICT
60
+ attr_reader :data
61
+
62
+ # Initialize and parse a DICT from binary data
63
+ #
64
+ # @param data [String, IO, StringIO] Binary DICT data
65
+ def initialize(data)
66
+ @data = data.is_a?(String) ? data : data.read
67
+ @dict = {}
68
+ @io = StringIO.new(@data)
69
+ parse!
70
+ end
71
+
72
+ # Get a value from the dictionary by operator name
73
+ #
74
+ # @param key [Symbol] Operator name (e.g., :charset, :encoding)
75
+ # @return [Object, nil] Value for the operator, or nil if not present
76
+ def [](key)
77
+ @dict[key]
78
+ end
79
+
80
+ # Set a value in the dictionary
81
+ #
82
+ # @param key [Symbol] Operator name
83
+ # @param value [Object] Value to set
84
+ def []=(key, value)
85
+ @dict[key] = value
86
+ end
87
+
88
+ # Check if the dictionary contains a specific operator
89
+ #
90
+ # @param key [Symbol] Operator name
91
+ # @return [Boolean] True if operator is present
92
+ def has_key?(key)
93
+ @dict.key?(key)
94
+ end
95
+
96
+ # Get all operator names in this DICT
97
+ #
98
+ # @return [Array<Symbol>] Array of operator names
99
+ def keys
100
+ @dict.keys
101
+ end
102
+
103
+ # Get all values in this DICT
104
+ #
105
+ # @return [Array<Object>] Array of values
106
+ def values
107
+ @dict.values
108
+ end
109
+
110
+ # Convert DICT to Hash
111
+ #
112
+ # @return [Hash] Dictionary as hash
113
+ def to_h
114
+ @dict.dup
115
+ end
116
+
117
+ # Number of entries in the DICT
118
+ #
119
+ # @return [Integer] Entry count
120
+ def size
121
+ @dict.size
122
+ end
123
+
124
+ # Check if DICT is empty
125
+ #
126
+ # @return [Boolean] True if no entries
127
+ def empty?
128
+ @dict.empty?
129
+ end
130
+
131
+ private
132
+
133
+ # Parse the DICT structure
134
+ #
135
+ # DICTs use a stack-based format:
136
+ # 1. Read operands and push onto operand stack
137
+ # 2. When operator is encountered, pop operands and process
138
+ # 3. Store result in dictionary
139
+ def parse!
140
+ operand_stack = []
141
+
142
+ until @io.eof?
143
+ byte = read_byte
144
+
145
+ if operator?(byte)
146
+ # Process operator with current operand stack
147
+ operator = read_operator(byte)
148
+ process_operator(operator, operand_stack)
149
+ operand_stack.clear
150
+ else
151
+ # Read operand and push onto stack
152
+ @io.pos -= 1 # Unread the byte
153
+ operand = read_operand
154
+ operand_stack << operand
155
+ end
156
+ end
157
+ end
158
+
159
+ # Check if a byte is an operator
160
+ #
161
+ # @param byte [Integer] Byte value
162
+ # @return [Boolean] True if operator byte
163
+ def operator?(byte)
164
+ # Operators are 0-21 or escape (12) followed by another byte
165
+ byte <= 21 || byte == 12
166
+ end
167
+
168
+ # Read an operator (single or two-byte)
169
+ #
170
+ # @param first_byte [Integer] First operator byte
171
+ # @return [Integer, Array<Integer>] Operator identifier
172
+ def read_operator(first_byte)
173
+ if first_byte == 12
174
+ # Two-byte operator (escape operator)
175
+ second_byte = read_byte
176
+ [first_byte, second_byte]
177
+ else
178
+ # Single-byte operator
179
+ first_byte
180
+ end
181
+ end
182
+
183
+ # Process an operator with its operands
184
+ #
185
+ # @param operator [Integer, Array<Integer>] Operator identifier
186
+ # @param operands [Array] Operand stack
187
+ def process_operator(operator, operands)
188
+ operator_name = operator_name_for(operator)
189
+ return unless operator_name
190
+
191
+ # Store the operand(s) in the dictionary
192
+ # Most operators take a single operand, some take arrays
193
+ value = operands.size == 1 ? operands.first : operands.dup
194
+ @dict[operator_name] = value
195
+ end
196
+
197
+ # Get the operator name for an operator byte(s)
198
+ #
199
+ # @param operator [Integer, Array<Integer>] Operator identifier
200
+ # @return [Symbol, nil] Operator name or nil if unknown
201
+ def operator_name_for(operator)
202
+ # Check in the OPERATORS table (common operators)
203
+ self.class::OPERATORS[operator] || derived_operators[operator]
204
+ end
205
+
206
+ # Get derived class-specific operators
207
+ #
208
+ # Subclasses override this to add their specific operators
209
+ #
210
+ # @return [Hash] Additional operators for this DICT type
211
+ def derived_operators
212
+ {}
213
+ end
214
+
215
+ # Read a single operand from the DICT data
216
+ #
217
+ # Operands can be:
218
+ # - Small integers (1 byte: 32-246 or 247-254 with next byte)
219
+ # - Medium integers (3 bytes: 28 + 2 bytes)
220
+ # - Large integers (5 bytes: 29 + 4 bytes)
221
+ # - Real numbers (30 + nibble-encoded decimal)
222
+ #
223
+ # @return [Integer, Float] The operand value
224
+ def read_operand
225
+ byte = read_byte
226
+
227
+ case byte
228
+ when 28
229
+ # 3-byte signed integer
230
+ read_int16
231
+ when 29
232
+ # 5-byte signed integer
233
+ read_int32
234
+ when 30
235
+ # Real number (nibble-encoded)
236
+ read_real
237
+ when 32..246
238
+ # Small integer: -107 to +107
239
+ byte - 139
240
+ when 247..250
241
+ # Positive 2-byte integer
242
+ second_byte = read_byte
243
+ (byte - 247) * 256 + second_byte + 108
244
+ when 251..254
245
+ # Negative 2-byte integer
246
+ second_byte = read_byte
247
+ -(byte - 251) * 256 - second_byte - 108
248
+ else
249
+ raise CorruptedTableError,
250
+ "Invalid DICT operand byte: #{byte}"
251
+ end
252
+ end
253
+
254
+ # Read a 16-bit signed integer (big-endian)
255
+ #
256
+ # @return [Integer] Signed 16-bit value
257
+ def read_int16
258
+ bytes = @io.read(2)
259
+ if bytes.nil? || bytes.bytesize < 2
260
+ raise CorruptedTableError,
261
+ "Unexpected end of DICT"
262
+ end
263
+
264
+ value = bytes.unpack1("n") # Unsigned 16-bit big-endian
265
+ # Convert to signed
266
+ value > 0x7FFF ? value - 0x10000 : value
267
+ end
268
+
269
+ # Read a 32-bit signed integer (big-endian)
270
+ #
271
+ # @return [Integer] Signed 32-bit value
272
+ def read_int32
273
+ bytes = @io.read(4)
274
+ if bytes.nil? || bytes.bytesize < 4
275
+ raise CorruptedTableError,
276
+ "Unexpected end of DICT"
277
+ end
278
+
279
+ value = bytes.unpack1("N") # Unsigned 32-bit big-endian
280
+ # Convert to signed
281
+ value > 0x7FFFFFFF ? value - 0x100000000 : value
282
+ end
283
+
284
+ # Read a real number (nibble-encoded)
285
+ #
286
+ # Real numbers in CFF are encoded as a sequence of nibbles (4-bit values)
287
+ # where each nibble represents a digit or special character.
288
+ #
289
+ # Nibble values:
290
+ # - 0-9: Decimal digits
291
+ # - a (10): Decimal point
292
+ # - b (11): Positive exponent (E)
293
+ # - c (12): Negative exponent (E-)
294
+ # - d (13): Reserved
295
+ # - e (14): Minus sign
296
+ # - f (15): End of number
297
+ #
298
+ # @return [Float] The decoded real number
299
+ def read_real
300
+ nibbles = []
301
+
302
+ loop do
303
+ byte = read_byte
304
+ high_nibble = (byte >> 4) & 0x0F
305
+ low_nibble = byte & 0x0F
306
+
307
+ break if high_nibble == 0xF
308
+
309
+ nibbles << high_nibble
310
+
311
+ break if low_nibble == 0xF
312
+
313
+ nibbles << low_nibble
314
+ end
315
+
316
+ # Convert nibbles to string representation
317
+ str = +""
318
+ nibbles.each do |nibble|
319
+ case nibble
320
+ when 0..9
321
+ str << nibble.to_s
322
+ when 0xa # Decimal point
323
+ str << "."
324
+ when 0xb # Positive exponent (E)
325
+ str << "e"
326
+ when 0xc # Negative exponent (E-)
327
+ str << "e-"
328
+ when 0xe # Minus sign
329
+ str << "-"
330
+ when 0xd, 0xf # Reserved or end marker
331
+ # Skip
332
+ end
333
+ end
334
+
335
+ # Convert to float
336
+ str.to_f
337
+ end
338
+
339
+ # Read a single byte from the IO
340
+ #
341
+ # @return [Integer] Byte value (0-255)
342
+ def read_byte
343
+ byte = @io.getbyte
344
+ raise CorruptedTableError, "Unexpected end of DICT" if byte.nil?
345
+
346
+ byte
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end