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,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dict"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ class Cff
8
+ # CFF Private DICT structure
9
+ #
10
+ # The Private DICT contains glyph-specific hinting and width data.
11
+ # Each font has its own Private DICT (or multiple for CIDFonts).
12
+ #
13
+ # Private DICT Operators:
14
+ # - blue_values: Alignment zones for overshoot suppression
15
+ # - other_blues: Additional alignment zones
16
+ # - family_blues: Family-wide alignment zones
17
+ # - family_other_blues: Family-wide additional alignment zones
18
+ # - blue_scale: Point size for overshoot suppression
19
+ # - blue_shift: Pixels to shift alignment zones
20
+ # - blue_fuzz: Tolerance for alignment zones
21
+ # - std_hw: Standard horizontal stem width
22
+ # - std_vw: Standard vertical stem width
23
+ # - stem_snap_h: Horizontal stem snap widths
24
+ # - stem_snap_v: Vertical stem snap widths
25
+ # - force_bold: Force bold flag
26
+ # - language_group: Language group (0=Latin, 1=CJK)
27
+ # - expansion_factor: Expansion factor for counters
28
+ # - initial_random_seed: Random seed for Type 1 hinting
29
+ # - subrs: Offset to Local Subr INDEX (relative to Private DICT)
30
+ # - default_width_x: Default glyph width
31
+ # - nominal_width_x: Nominal glyph width
32
+ #
33
+ # Reference: CFF specification section 10 "Private DICT"
34
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
35
+ #
36
+ # @example Parsing a Private DICT
37
+ # private_size, private_offset = top_dict.private
38
+ # private_data = cff.raw_data[private_offset, private_size]
39
+ # private_dict = Fontisan::Tables::Cff::PrivateDict.new(private_data)
40
+ # puts private_dict[:blue_values] # => [array of blue values]
41
+ # puts private_dict.default_width_x # => default glyph width
42
+ class PrivateDict < Dict
43
+ # Private DICT specific operators
44
+ #
45
+ # These extend the common operators defined in the base Dict class
46
+ PRIVATE_DICT_OPERATORS = {
47
+ 6 => :blue_values,
48
+ 7 => :other_blues,
49
+ 8 => :family_blues,
50
+ 9 => :family_other_blues,
51
+ [12, 9] => :blue_scale,
52
+ [12, 10] => :blue_shift,
53
+ [12, 11] => :blue_fuzz,
54
+ 10 => :std_hw,
55
+ 11 => :std_vw,
56
+ [12, 12] => :stem_snap_h,
57
+ [12, 13] => :stem_snap_v,
58
+ [12, 14] => :force_bold,
59
+ [12, 17] => :language_group,
60
+ [12, 18] => :expansion_factor,
61
+ [12, 19] => :initial_random_seed,
62
+ 19 => :subrs,
63
+ 20 => :default_width_x,
64
+ 21 => :nominal_width_x,
65
+ }.freeze
66
+
67
+ # Default values for Private DICT operators
68
+ #
69
+ # These are used when an operator is not present in the DICT
70
+ DEFAULTS = {
71
+ blue_scale: 0.039625,
72
+ blue_shift: 7,
73
+ blue_fuzz: 1,
74
+ force_bold: false,
75
+ language_group: 0,
76
+ expansion_factor: 0.06,
77
+ initial_random_seed: 0,
78
+ default_width_x: 0,
79
+ nominal_width_x: 0,
80
+ }.freeze
81
+
82
+ # Get a value with default fallback
83
+ #
84
+ # @param key [Symbol] Operator name
85
+ # @return [Object] Value or default value
86
+ def fetch(key, default = nil)
87
+ @dict.fetch(key, DEFAULTS.fetch(key, default))
88
+ end
89
+
90
+ # Get the blue values (alignment zones)
91
+ #
92
+ # Blue values define vertical zones for overshoot suppression
93
+ #
94
+ # @return [Array<Integer>, nil] Array of blue values (pairs of bottom/top)
95
+ def blue_values
96
+ @dict[:blue_values]
97
+ end
98
+
99
+ # Get the other blue values
100
+ #
101
+ # Additional alignment zones beyond the baseline and cap height
102
+ #
103
+ # @return [Array<Integer>, nil] Array of other blue values
104
+ def other_blues
105
+ @dict[:other_blues]
106
+ end
107
+
108
+ # Get the family blue values
109
+ #
110
+ # Family-wide alignment zones shared across fonts in a family
111
+ #
112
+ # @return [Array<Integer>, nil] Array of family blue values
113
+ def family_blues
114
+ @dict[:family_blues]
115
+ end
116
+
117
+ # Get the family other blue values
118
+ #
119
+ # @return [Array<Integer>, nil] Array of family other blue values
120
+ def family_other_blues
121
+ @dict[:family_other_blues]
122
+ end
123
+
124
+ # Get the blue scale
125
+ #
126
+ # Point size at which overshoot suppression is maximum
127
+ #
128
+ # @return [Float] Blue scale value
129
+ def blue_scale
130
+ fetch(:blue_scale)
131
+ end
132
+
133
+ # Get the blue shift
134
+ #
135
+ # Number of device pixels to shift alignment zones
136
+ #
137
+ # @return [Integer] Blue shift in pixels
138
+ def blue_shift
139
+ fetch(:blue_shift)
140
+ end
141
+
142
+ # Get the blue fuzz
143
+ #
144
+ # Tolerance for alignment zone matching
145
+ #
146
+ # @return [Integer] Blue fuzz in font units
147
+ def blue_fuzz
148
+ fetch(:blue_fuzz)
149
+ end
150
+
151
+ # Get the standard horizontal width
152
+ #
153
+ # Dominant horizontal stem width
154
+ #
155
+ # @return [Integer, nil] Standard horizontal width
156
+ def std_hw
157
+ value = @dict[:std_hw]
158
+ # std_hw is stored as an array with one element
159
+ value.is_a?(Array) ? value.first : value
160
+ end
161
+
162
+ # Get the standard vertical width
163
+ #
164
+ # Dominant vertical stem width
165
+ #
166
+ # @return [Integer, nil] Standard vertical width
167
+ def std_vw
168
+ value = @dict[:std_vw]
169
+ # std_vw is stored as an array with one element
170
+ value.is_a?(Array) ? value.first : value
171
+ end
172
+
173
+ # Get the horizontal stem snap widths
174
+ #
175
+ # Array of horizontal stem widths for stem snapping
176
+ #
177
+ # @return [Array<Integer>, nil] Horizontal stem snap widths
178
+ def stem_snap_h
179
+ @dict[:stem_snap_h]
180
+ end
181
+
182
+ # Get the vertical stem snap widths
183
+ #
184
+ # Array of vertical stem widths for stem snapping
185
+ #
186
+ # @return [Array<Integer>, nil] Vertical stem snap widths
187
+ def stem_snap_v
188
+ @dict[:stem_snap_v]
189
+ end
190
+
191
+ # Check if force bold is enabled
192
+ #
193
+ # @return [Boolean] True if force bold is enabled
194
+ def force_bold?
195
+ fetch(:force_bold)
196
+ end
197
+
198
+ # Get the language group
199
+ #
200
+ # 0 = Latin/Greek/Cyrillic, 1 = CJK
201
+ #
202
+ # @return [Integer] Language group (0 or 1)
203
+ def language_group
204
+ fetch(:language_group)
205
+ end
206
+
207
+ # Get the expansion factor
208
+ #
209
+ # Controls horizontal counter expansion
210
+ #
211
+ # @return [Float] Expansion factor
212
+ def expansion_factor
213
+ fetch(:expansion_factor)
214
+ end
215
+
216
+ # Get the initial random seed
217
+ #
218
+ # Seed for pseudo-random number generation in Type 1 hinting
219
+ #
220
+ # @return [Integer] Initial random seed
221
+ def initial_random_seed
222
+ fetch(:initial_random_seed)
223
+ end
224
+
225
+ # Get the Local Subr INDEX offset
226
+ #
227
+ # Offset is relative to the beginning of the Private DICT
228
+ #
229
+ # @return [Integer, nil] Offset to Local Subr INDEX
230
+ def subrs
231
+ @dict[:subrs]
232
+ end
233
+
234
+ # Get the default glyph width
235
+ #
236
+ # Used when width is not explicitly specified in CharString
237
+ #
238
+ # @return [Integer] Default width in font units
239
+ def default_width_x
240
+ fetch(:default_width_x)
241
+ end
242
+
243
+ # Get the nominal glyph width
244
+ #
245
+ # Base value for width calculations in CharStrings
246
+ #
247
+ # @return [Integer] Nominal width in font units
248
+ def nominal_width_x
249
+ fetch(:nominal_width_x)
250
+ end
251
+
252
+ # Check if this Private DICT has local subroutines
253
+ #
254
+ # @return [Boolean] True if subrs offset is present
255
+ def has_local_subrs?
256
+ !subrs.nil?
257
+ end
258
+
259
+ # Check if this Private DICT has blue values defined
260
+ #
261
+ # @return [Boolean] True if blue values are present
262
+ def has_blue_values?
263
+ !blue_values.nil? && !blue_values.empty?
264
+ end
265
+
266
+ # Check if this is for CJK language group
267
+ #
268
+ # @return [Boolean] True if language group is 1 (CJK)
269
+ def cjk?
270
+ language_group == 1
271
+ end
272
+
273
+ private
274
+
275
+ # Get Private DICT specific operators
276
+ #
277
+ # @return [Hash] Private DICT operators merged with base operators
278
+ def derived_operators
279
+ PRIVATE_DICT_OPERATORS
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dict_builder"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ class Cff
8
+ # Builds CFF Private DICT with hint parameters
9
+ #
10
+ # Private DICT contains font-level hint information used for rendering quality.
11
+ # This writer validates hint parameters against CFF spec limits and serializes
12
+ # them into binary DICT format.
13
+ #
14
+ # Supported hint parameters:
15
+ # - blue_values: Alignment zones (max 14 values, pairs)
16
+ # - other_blues: Additional zones (max 10 values, pairs)
17
+ # - family_blues: Family alignment zones (max 14 values, pairs)
18
+ # - family_other_blues: Family zones (max 10 values, pairs)
19
+ # - std_hw: Standard horizontal stem width
20
+ # - std_vw: Standard vertical stem width
21
+ # - stem_snap_h: Horizontal stem snap widths (max 12)
22
+ # - stem_snap_v: Vertical stem snap widths (max 12)
23
+ # - blue_scale, blue_shift, blue_fuzz: Overshoot parameters
24
+ # - force_bold: Bold flag
25
+ # - language_group: 0=Latin, 1=CJK
26
+ class PrivateDictWriter
27
+ # CFF specification limits for hint parameters
28
+ HINT_LIMITS = {
29
+ blue_values: { max: 14, pairs: true },
30
+ other_blues: { max: 10, pairs: true },
31
+ family_blues: { max: 14, pairs: true },
32
+ family_other_blues: { max: 10, pairs: true },
33
+ stem_snap_h: { max: 12 },
34
+ stem_snap_v: { max: 12 },
35
+ }.freeze
36
+
37
+ # Initialize writer with optional source Private DICT
38
+ #
39
+ # @param source_dict [PrivateDict, nil] Source to copy non-hint params from
40
+ def initialize(source_dict = nil)
41
+ @params = {}
42
+ parse_source(source_dict) if source_dict
43
+ end
44
+
45
+ # Update hint parameters
46
+ #
47
+ # @param hint_params [Hash] Hint parameters to add/update
48
+ # @raise [ArgumentError] If parameters are invalid
49
+ def update_hints(hint_params)
50
+ validate!(hint_params)
51
+ @params.merge!(hint_params.transform_keys(&:to_sym))
52
+ end
53
+
54
+ # Serialize to binary DICT format
55
+ #
56
+ # @return [String] Binary DICT data
57
+ def serialize
58
+ DictBuilder.build(@params)
59
+ end
60
+
61
+ # Get serialized size in bytes
62
+ #
63
+ # @return [Integer] Size in bytes
64
+ def size
65
+ serialize.bytesize
66
+ end
67
+
68
+ private
69
+
70
+ # Parse non-hint parameters from source Private DICT
71
+ #
72
+ # @param source_dict [PrivateDict] Source dictionary
73
+ def parse_source(source_dict)
74
+ return unless source_dict.respond_to?(:to_h)
75
+
76
+ # Extract only non-hint params (subrs, widths)
77
+ @params = source_dict.to_h.select do |k, _|
78
+ %i[subrs default_width_x nominal_width_x].include?(k)
79
+ end
80
+ end
81
+
82
+ # Validate hint parameters against CFF spec
83
+ #
84
+ # @param params [Hash] Hint parameters
85
+ # @raise [ArgumentError] If validation fails
86
+ def validate!(params)
87
+ params.each do |key, value|
88
+ k = key.to_sym
89
+ validate_hint_param(k, value)
90
+ end
91
+ end
92
+
93
+ # Validate individual hint parameter
94
+ #
95
+ # @param key [Symbol] Parameter name
96
+ # @param value [Object] Parameter value
97
+ # @raise [ArgumentError] If validation fails
98
+ def validate_hint_param(key, value)
99
+ # Check array limits
100
+ if HINT_LIMITS[key]
101
+ raise ArgumentError, "#{key} invalid" unless value.is_a?(Array)
102
+ raise ArgumentError, "#{key} too long" if value.length > HINT_LIMITS[key][:max]
103
+ if HINT_LIMITS[key][:pairs] && value.length.odd?
104
+ raise ArgumentError, "#{key} must be pairs"
105
+ end
106
+ end
107
+
108
+ # Check value-specific constraints
109
+ case key
110
+ when :std_hw, :std_vw
111
+ raise ArgumentError, "#{key} negative" if value.negative?
112
+ when :blue_scale
113
+ raise ArgumentError, "#{key} not positive" if value <= 0
114
+ when :blue_shift, :blue_fuzz
115
+ raise ArgumentError, "#{key} invalid" unless value.is_a?(Numeric)
116
+ when :force_bold
117
+ raise ArgumentError, "#{key} must be 0 or 1" unless [0, 1].include?(value)
118
+ when :language_group
119
+ raise ArgumentError, "#{key} must be 0 or 1" unless [0, 1].include?(value)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "private_dict_writer"
4
+ require_relative "offset_recalculator"
5
+ require_relative "index_builder"
6
+ require_relative "dict_builder"
7
+ require_relative "charstring_rebuilder"
8
+ require_relative "hint_operation_injector"
9
+ require "stringio"
10
+
11
+ module Fontisan
12
+ module Tables
13
+ class Cff
14
+ # Rebuilds CFF table with modifications
15
+ #
16
+ # This builder extracts sections from a source CFF table, applies
17
+ # modifications (e.g., hint parameters to Private DICT), recalculates
18
+ # offsets, and assembles a new CFF table.
19
+ #
20
+ # Process:
21
+ # 1. Extract all CFF sections (header, indexes, dicts)
22
+ # 2. Apply modifications to Private DICT
23
+ # 3. Recalculate offsets (charstrings, private)
24
+ # 4. Rebuild Top DICT INDEX with new offsets
25
+ # 5. Reassemble all sections into new CFF table
26
+ #
27
+ # @example Rebuild with hints
28
+ # new_cff = TableBuilder.rebuild(source_cff, {
29
+ # private_dict_hints: { blue_values: [-15, 0], std_hw: 70 }
30
+ # })
31
+ class TableBuilder
32
+ # Rebuild CFF table with modifications
33
+ #
34
+ # @param source_cff [Cff] Source CFF table
35
+ # @param modifications [Hash] Modifications to apply
36
+ # @return [String] Binary CFF table data
37
+ def self.rebuild(source_cff, modifications = {})
38
+ new(source_cff).tap do |builder|
39
+ builder.apply_modifications(modifications)
40
+ end.serialize
41
+ end
42
+
43
+ # Initialize with source CFF
44
+ #
45
+ # @param source_cff [Cff] Source CFF table
46
+ def initialize(source_cff)
47
+ @source = source_cff
48
+ @sections = extract_sections
49
+ end
50
+
51
+ # Apply modifications to CFF structure
52
+ #
53
+ # @param mods [Hash] Modifications hash
54
+ def apply_modifications(mods)
55
+ update_private_dict(mods[:private_dict_hints]) if mods[:private_dict_hints]
56
+ update_charstrings(mods[:per_glyph_hints]) if mods[:per_glyph_hints]
57
+ end
58
+
59
+ # Serialize to binary CFF table
60
+ #
61
+ # @return [String] Binary CFF data
62
+ def serialize
63
+ # Calculate initial offsets
64
+ offsets = OffsetRecalculator.calculate_offsets(@sections)
65
+ top_dict = extract_top_dict_data
66
+ updated = OffsetRecalculator.update_top_dict(top_dict, offsets)
67
+ rebuild_top_dict_index(updated)
68
+
69
+ # Recalculate after Top DICT rebuild (size may change)
70
+ offsets = OffsetRecalculator.calculate_offsets(@sections)
71
+ updated = OffsetRecalculator.update_top_dict(top_dict, offsets)
72
+ rebuild_top_dict_index(updated)
73
+
74
+ assemble
75
+ end
76
+
77
+ private
78
+
79
+ # Extract all CFF sections from source
80
+ #
81
+ # @return [Hash] Hash of section_name => binary_data
82
+ def extract_sections
83
+ {
84
+ header: extract_header,
85
+ name_index: extract_index(@source.name_index),
86
+ top_dict_index: extract_index(@source.top_dict_index),
87
+ string_index: extract_index(@source.string_index),
88
+ global_subr_index: extract_index(@source.global_subr_index),
89
+ charstrings_index: extract_index(@source.charstrings_index(0)),
90
+ private_dict: extract_private_dict,
91
+ }
92
+ end
93
+
94
+ # Extract header bytes
95
+ #
96
+ # @return [String] Binary header data
97
+ def extract_header
98
+ @source.raw_data[0, @source.header.hdr_size]
99
+ end
100
+
101
+ # Extract INDEX as binary data
102
+ #
103
+ # @param index [Index] INDEX object
104
+ # @return [String] Binary INDEX data
105
+ def extract_index(index)
106
+ return [0].pack("n") if index.nil? || index.count.zero?
107
+
108
+ start = index.instance_variable_get(:@start_offset)
109
+ io = StringIO.new(@source.raw_data)
110
+ io.seek(start)
111
+
112
+ count = io.read(2).unpack1("n")
113
+ return [0].pack("n") if count.zero?
114
+
115
+ off_size = io.read(1).unpack1("C")
116
+ offset_array_size = (count + 1) * off_size
117
+
118
+ # Read last offset to determine data size
119
+ io.seek(start + 3 + count * off_size)
120
+ last_offset = read_offset(io, off_size)
121
+ data_size = last_offset - 1
122
+
123
+ # Read entire INDEX
124
+ io.seek(start)
125
+ io.read(3 + offset_array_size + data_size)
126
+ end
127
+
128
+ # Extract Private DICT bytes
129
+ #
130
+ # @return [String] Binary Private DICT data
131
+ def extract_private_dict
132
+ priv_info = @source.top_dict(0).private
133
+ return "".b unless priv_info
134
+
135
+ size, offset = priv_info
136
+ @source.raw_data[offset, size]
137
+ end
138
+
139
+ # Update Private DICT with hints
140
+ #
141
+ # @param hints [Hash] Hint parameters
142
+ def update_private_dict(hints)
143
+ source_priv = @source.private_dict(0)
144
+ writer = PrivateDictWriter.new(source_priv)
145
+ writer.update_hints(hints)
146
+ @sections[:private_dict] = writer.serialize
147
+ end
148
+
149
+ # Update CharStrings with per-glyph hints
150
+ #
151
+ # @param per_glyph_hints [Hash] Hash of glyph_id => Array<Hint>
152
+ def update_charstrings(per_glyph_hints)
153
+ return if per_glyph_hints.nil? || per_glyph_hints.empty?
154
+
155
+ # Create CharStringRebuilder
156
+ charstrings_index = @source.charstrings_index(0)
157
+ rebuilder = CharStringRebuilder.new(charstrings_index)
158
+
159
+ # Inject hints for each glyph
160
+ per_glyph_hints.each do |glyph_id, hints|
161
+ injector = HintOperationInjector.new
162
+
163
+ rebuilder.modify_charstring(glyph_id) do |operations|
164
+ # Inject hint operations
165
+ injector.inject(hints, operations)
166
+ end
167
+ end
168
+
169
+ # Rebuild CharStrings INDEX
170
+ @sections[:charstrings_index] = rebuilder.rebuild
171
+ end
172
+
173
+ # Extract Top DICT data as hash
174
+ #
175
+ # @return [Hash] Top DICT parameters
176
+ def extract_top_dict_data
177
+ @source.top_dict(0).to_h
178
+ end
179
+
180
+ # Rebuild Top DICT INDEX with updated data
181
+ #
182
+ # @param data [Hash] Top DICT parameters
183
+ def rebuild_top_dict_index(data)
184
+ dict_bytes = DictBuilder.build(data)
185
+ @sections[:top_dict_index] = IndexBuilder.build([dict_bytes])
186
+ end
187
+
188
+ # Assemble all sections into CFF table
189
+ #
190
+ # @return [String] Binary CFF table
191
+ def assemble
192
+ output = StringIO.new("".b)
193
+ output.write(@sections[:header])
194
+ output.write(@sections[:name_index])
195
+ output.write(@sections[:top_dict_index])
196
+ output.write(@sections[:string_index])
197
+ output.write(@sections[:global_subr_index])
198
+ output.write(@sections[:charstrings_index])
199
+ output.write(@sections[:private_dict])
200
+ output.string
201
+ end
202
+
203
+ # Read offset of specified size
204
+ #
205
+ # @param io [IO] IO object
206
+ # @param size [Integer] Offset size (1-4 bytes)
207
+ # @return [Integer] Offset value
208
+ def read_offset(io, size)
209
+ case size
210
+ when 1 then io.read(1).unpack1("C")
211
+ when 2 then io.read(2).unpack1("n")
212
+ when 3
213
+ bytes = io.read(3).unpack("C*")
214
+ (bytes[0] << 16) | (bytes[1] << 8) | bytes[2]
215
+ when 4 then io.read(4).unpack1("N")
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end