fontisan 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +529 -65
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1301 -275
  6. data/Rakefile +27 -2
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +309 -0
  12. data/lib/fontisan/collection/builder.rb +260 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +241 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +8 -1
  18. data/lib/fontisan/commands/convert_command.rb +291 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +295 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +178 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +69 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +259 -0
  39. data/lib/fontisan/converters/outline_converter.rb +936 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +121 -12
  57. data/lib/fontisan/font_writer.rb +301 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +177 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
  65. data/lib/fontisan/loading_modes.rb +113 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +233 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +296 -10
  88. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  89. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  90. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  91. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  92. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  93. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  94. data/lib/fontisan/outline_extractor.rb +423 -0
  95. data/lib/fontisan/subset/builder.rb +268 -0
  96. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  97. data/lib/fontisan/subset/options.rb +142 -0
  98. data/lib/fontisan/subset/profile.rb +152 -0
  99. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  100. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  101. data/lib/fontisan/svg/font_generator.rb +264 -0
  102. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  103. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  104. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  105. data/lib/fontisan/tables/cff/charset.rb +282 -0
  106. data/lib/fontisan/tables/cff/charstring.rb +905 -0
  107. data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
  108. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  109. data/lib/fontisan/tables/cff/dict.rb +351 -0
  110. data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
  111. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  112. data/lib/fontisan/tables/cff/header.rb +102 -0
  113. data/lib/fontisan/tables/cff/index.rb +237 -0
  114. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  115. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  116. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  117. data/lib/fontisan/tables/cff.rb +487 -0
  118. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  119. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  120. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  121. data/lib/fontisan/tables/cff2.rb +341 -0
  122. data/lib/fontisan/tables/cvar.rb +242 -0
  123. data/lib/fontisan/tables/fvar.rb +2 -2
  124. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  125. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  126. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  127. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  128. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  129. data/lib/fontisan/tables/glyf.rb +235 -0
  130. data/lib/fontisan/tables/gvar.rb +270 -0
  131. data/lib/fontisan/tables/hhea.rb +124 -0
  132. data/lib/fontisan/tables/hmtx.rb +287 -0
  133. data/lib/fontisan/tables/hvar.rb +191 -0
  134. data/lib/fontisan/tables/loca.rb +322 -0
  135. data/lib/fontisan/tables/maxp.rb +192 -0
  136. data/lib/fontisan/tables/mvar.rb +185 -0
  137. data/lib/fontisan/tables/name.rb +99 -30
  138. data/lib/fontisan/tables/variation_common.rb +346 -0
  139. data/lib/fontisan/tables/vvar.rb +234 -0
  140. data/lib/fontisan/true_type_collection.rb +156 -2
  141. data/lib/fontisan/true_type_font.rb +297 -11
  142. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  143. data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
  144. data/lib/fontisan/utils/thread_pool.rb +134 -0
  145. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  146. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  147. data/lib/fontisan/validation/structure_validator.rb +198 -0
  148. data/lib/fontisan/validation/table_validator.rb +158 -0
  149. data/lib/fontisan/validation/validator.rb +152 -0
  150. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  151. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  152. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  153. data/lib/fontisan/variable/instancer.rb +344 -0
  154. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  155. data/lib/fontisan/variable/region_matcher.rb +208 -0
  156. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  157. data/lib/fontisan/variable/table_updater.rb +219 -0
  158. data/lib/fontisan/variation/blend_applier.rb +199 -0
  159. data/lib/fontisan/variation/cache.rb +298 -0
  160. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  161. data/lib/fontisan/variation/converter.rb +268 -0
  162. data/lib/fontisan/variation/data_extractor.rb +86 -0
  163. data/lib/fontisan/variation/delta_applier.rb +266 -0
  164. data/lib/fontisan/variation/delta_parser.rb +228 -0
  165. data/lib/fontisan/variation/inspector.rb +275 -0
  166. data/lib/fontisan/variation/instance_generator.rb +273 -0
  167. data/lib/fontisan/variation/interpolator.rb +231 -0
  168. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  169. data/lib/fontisan/variation/optimizer.rb +418 -0
  170. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  171. data/lib/fontisan/variation/region_matcher.rb +221 -0
  172. data/lib/fontisan/variation/subsetter.rb +463 -0
  173. data/lib/fontisan/variation/table_accessor.rb +105 -0
  174. data/lib/fontisan/variation/validator.rb +345 -0
  175. data/lib/fontisan/variation/variation_context.rb +211 -0
  176. data/lib/fontisan/version.rb +1 -1
  177. data/lib/fontisan/woff2/directory.rb +257 -0
  178. data/lib/fontisan/woff2/header.rb +101 -0
  179. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  180. data/lib/fontisan/woff2_font.rb +712 -0
  181. data/lib/fontisan/woff_font.rb +483 -0
  182. data/lib/fontisan.rb +120 -0
  183. data/scripts/compare_stack_aware.rb +187 -0
  184. data/scripts/measure_optimization.rb +141 -0
  185. metadata +205 -4
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ class Cff
8
+ # CFF INDEX structure builder
9
+ #
10
+ # [`IndexBuilder`](lib/fontisan/tables/cff/index_builder.rb) constructs
11
+ # binary INDEX structures from arrays of data items. INDEX is a fundamental
12
+ # CFF data structure used for storing arrays of variable-length data.
13
+ #
14
+ # The builder calculates optimal offset sizes, constructs the offset array,
15
+ # and produces compact binary output.
16
+ #
17
+ # Structure produced:
18
+ # - count (Card16): Number of items
19
+ # - offSize (OffSize): Size of offset values (1-4 bytes)
20
+ # - offset[count+1] (Offset): Array of offsets to data
21
+ # - data: Concatenated data bytes
22
+ #
23
+ # Offsets are 1-based (first offset is always 1). The last offset points
24
+ # one byte past the end of the data.
25
+ #
26
+ # Reference: CFF specification section 5 "INDEX Data"
27
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
28
+ #
29
+ # @example Building an INDEX
30
+ # items = ["data1".b, "data2".b, "data3".b]
31
+ # index_data = Fontisan::Tables::Cff::IndexBuilder.build(items)
32
+ class IndexBuilder
33
+ # Build INDEX structure from array of binary strings
34
+ #
35
+ # @param items [Array<String>] Array of binary data items
36
+ # @return [String] Binary INDEX data
37
+ # @raise [ArgumentError] If items is not an Array
38
+ def self.build(items)
39
+ validate_items!(items)
40
+
41
+ return build_empty_index if items.empty?
42
+
43
+ # Calculate total data size
44
+ data_size = items.sum(&:bytesize)
45
+
46
+ # Calculate optimal offset size (1-4 bytes)
47
+ # Last offset will be data_size + 1 (1-based)
48
+ off_size = calculate_off_size(data_size + 1)
49
+
50
+ # Build offset array (count + 1 offsets)
51
+ offsets = build_offsets(items, off_size)
52
+
53
+ # Concatenate all data
54
+ data = items.join
55
+
56
+ # Assemble INDEX structure
57
+ output = StringIO.new("".b)
58
+
59
+ # Write count (Card16)
60
+ output.write([items.length].pack("n"))
61
+
62
+ # Write offSize (OffSize)
63
+ output.putc(off_size)
64
+
65
+ # Write offset array
66
+ offsets.each do |offset|
67
+ write_offset(output, offset, off_size)
68
+ end
69
+
70
+ # Write data
71
+ output.write(data)
72
+
73
+ output.string
74
+ end
75
+
76
+ # Build an empty INDEX (count = 0)
77
+ #
78
+ # @return [String] Binary empty INDEX
79
+ def self.build_empty_index
80
+ # Empty INDEX has only count field (0)
81
+ [0].pack("n")
82
+ end
83
+ private_class_method :build_empty_index
84
+
85
+ # Validate items parameter
86
+ #
87
+ # @param items [Object] Items to validate
88
+ # @raise [ArgumentError] If items is invalid
89
+ def self.validate_items!(items)
90
+ raise ArgumentError, "items must be Array" unless items.is_a?(Array)
91
+
92
+ items.each_with_index do |item, i|
93
+ unless item.is_a?(String)
94
+ raise ArgumentError,
95
+ "item #{i} must be String, got: #{item.class}"
96
+ end
97
+ unless item.encoding == ::Encoding::BINARY
98
+ raise ArgumentError,
99
+ "item #{i} must have BINARY encoding, got: #{item.encoding}"
100
+ end
101
+ end
102
+ end
103
+ private_class_method :validate_items!
104
+
105
+ # Calculate optimal offset size for given maximum offset
106
+ #
107
+ # @param max_offset [Integer] Maximum offset value
108
+ # @return [Integer] Offset size (1-4 bytes)
109
+ def self.calculate_off_size(max_offset)
110
+ return 1 if max_offset <= 0xFF
111
+ return 2 if max_offset <= 0xFFFF
112
+ return 3 if max_offset <= 0xFFFFFF
113
+
114
+ 4
115
+ end
116
+ private_class_method :calculate_off_size
117
+
118
+ # Build offset array from items
119
+ #
120
+ # Offsets are 1-based. First offset is always 1.
121
+ # Each offset points to the start of its item in the data array.
122
+ # Last offset points one byte past the end of data.
123
+ #
124
+ # @param items [Array<String>] Array of data items
125
+ # @param off_size [Integer] Offset size (1-4 bytes)
126
+ # @return [Array<Integer>] Array of offsets (count + 1 elements)
127
+ def self.build_offsets(items, _off_size)
128
+ offsets = []
129
+ current_offset = 1 # 1-based
130
+
131
+ # First offset is always 1
132
+ offsets << current_offset
133
+
134
+ # Calculate offset for each item
135
+ items.each do |item|
136
+ current_offset += item.bytesize
137
+ offsets << current_offset
138
+ end
139
+
140
+ offsets
141
+ end
142
+ private_class_method :build_offsets
143
+
144
+ # Write an offset value of specified size
145
+ #
146
+ # @param io [StringIO] Output stream
147
+ # @param offset [Integer] Offset value to write
148
+ # @param size [Integer] Number of bytes (1-4)
149
+ def self.write_offset(io, offset, size)
150
+ case size
151
+ when 1
152
+ io.putc(offset & 0xFF)
153
+ when 2
154
+ io.write([offset].pack("n")) # Big-endian unsigned 16-bit
155
+ when 3
156
+ # 24-bit big-endian
157
+ io.putc((offset >> 16) & 0xFF)
158
+ io.putc((offset >> 8) & 0xFF)
159
+ io.putc(offset & 0xFF)
160
+ when 4
161
+ io.write([offset].pack("N")) # Big-endian unsigned 32-bit
162
+ else
163
+ raise ArgumentError, "Invalid offset size: #{size}"
164
+ end
165
+ end
166
+ private_class_method :write_offset
167
+ end
168
+ end
169
+ end
170
+ end
@@ -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,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dict"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ class Cff
8
+ # CFF Top DICT structure
9
+ #
10
+ # The Top DICT contains font-level metadata and pointers to other CFF
11
+ # structures like CharStrings, Charset, Encoding, and Private DICT.
12
+ #
13
+ # Top DICT Operators (in addition to common DICT operators):
14
+ # - charset: Offset to Charset data
15
+ # - encoding: Offset to Encoding data
16
+ # - charstrings: Offset to CharStrings INDEX
17
+ # - private: Size and offset to Private DICT
18
+ # - font_bbox: Font bounding box [xMin, yMin, xMax, yMax]
19
+ # - unique_id: Unique ID for this font
20
+ # - xuid: Extended unique ID array
21
+ # - ros: CIDFont registry, ordering, supplement
22
+ # - cidcount: Number of CIDs in CIDFont
23
+ # - fdarray: Offset to Font DICT INDEX (CIDFont)
24
+ # - fdselect: Offset to FDSelect data (CIDFont)
25
+ #
26
+ # Reference: CFF specification section 9 "Top DICT"
27
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
28
+ #
29
+ # @example Parsing a Top DICT
30
+ # top_dict_data = cff.top_dict_index[0]
31
+ # top_dict = Fontisan::Tables::Cff::TopDict.new(top_dict_data)
32
+ # puts top_dict[:charstrings] # => offset to CharStrings
33
+ # puts top_dict[:charset] # => offset to Charset
34
+ class TopDict < Dict
35
+ # Top DICT specific operators
36
+ #
37
+ # These extend the common operators defined in the base Dict class
38
+ TOP_DICT_OPERATORS = {
39
+ 5 => :font_bbox,
40
+ 13 => :unique_id,
41
+ 14 => :xuid,
42
+ 15 => :charset,
43
+ 16 => :encoding,
44
+ 17 => :charstrings,
45
+ 18 => :private,
46
+ [12, 30] => :ros,
47
+ [12, 31] => :cid_font_version,
48
+ [12, 32] => :cid_font_revision,
49
+ [12, 33] => :cid_font_type,
50
+ [12, 34] => :cid_count,
51
+ [12, 35] => :uid_base,
52
+ [12, 36] => :fd_array,
53
+ [12, 37] => :fd_select,
54
+ [12, 38] => :font_name,
55
+ }.freeze
56
+
57
+ # Default values for Top DICT operators
58
+ #
59
+ # These are used when an operator is not present in the DICT
60
+ DEFAULTS = {
61
+ is_fixed_pitch: false,
62
+ italic_angle: 0,
63
+ underline_position: -100,
64
+ underline_thickness: 50,
65
+ paint_type: 0,
66
+ charstring_type: 2,
67
+ font_matrix: [0.001, 0, 0, 0.001, 0, 0],
68
+ unique_id: nil,
69
+ font_bbox: [0, 0, 0, 0],
70
+ stroke_width: 0,
71
+ charset: 0, # Offset 0 = ISOAdobe charset
72
+ encoding: 0, # Offset 0 = Standard encoding
73
+ cid_count: 8720,
74
+ }.freeze
75
+
76
+ # Get a value with default fallback
77
+ #
78
+ # @param key [Symbol] Operator name
79
+ # @return [Object] Value or default value
80
+ def fetch(key, default = nil)
81
+ @dict.fetch(key, DEFAULTS.fetch(key, default))
82
+ end
83
+
84
+ # Get the charset offset
85
+ #
86
+ # Charset determines which glyphs are present and their SIDs
87
+ #
88
+ # Special values:
89
+ # - 0: ISOAdobe charset
90
+ # - 1: Expert charset
91
+ # - 2: Expert Subset charset
92
+ # - Otherwise: Offset to custom charset
93
+ #
94
+ # @return [Integer] Charset offset or predefined charset ID
95
+ def charset
96
+ fetch(:charset)
97
+ end
98
+
99
+ # Get the encoding offset
100
+ #
101
+ # Encoding maps character codes to glyph indices
102
+ #
103
+ # Special values:
104
+ # - 0: Standard encoding
105
+ # - 1: Expert encoding
106
+ # - Otherwise: Offset to custom encoding
107
+ #
108
+ # @return [Integer] Encoding offset or predefined encoding ID
109
+ def encoding
110
+ fetch(:encoding)
111
+ end
112
+
113
+ # Get the CharStrings offset
114
+ #
115
+ # CharStrings INDEX contains the glyph programs (outline data)
116
+ #
117
+ # @return [Integer, nil] Offset to CharStrings INDEX
118
+ def charstrings
119
+ @dict[:charstrings]
120
+ end
121
+
122
+ # Get the Private DICT size and offset
123
+ #
124
+ # The private operator stores [size, offset] as a two-element array
125
+ #
126
+ # @return [Array<Integer>, nil] [size, offset] or nil if not present
127
+ def private
128
+ @dict[:private]
129
+ end
130
+
131
+ # Get the Private DICT size
132
+ #
133
+ # @return [Integer, nil] Size in bytes, or nil if no Private DICT
134
+ def private_size
135
+ private&.first
136
+ end
137
+
138
+ # Get the Private DICT offset
139
+ #
140
+ # @return [Integer, nil] Offset in bytes, or nil if no Private DICT
141
+ def private_offset
142
+ private&.last
143
+ end
144
+
145
+ # Get the font bounding box
146
+ #
147
+ # @return [Array<Integer>] [xMin, yMin, xMax, yMax]
148
+ def font_bbox
149
+ fetch(:font_bbox)
150
+ end
151
+
152
+ # Get the font matrix
153
+ #
154
+ # Transform from glyph space to user space
155
+ #
156
+ # @return [Array<Float>] 6-element affine transformation matrix
157
+ def font_matrix
158
+ fetch(:font_matrix)
159
+ end
160
+
161
+ # Check if this is a CIDFont
162
+ #
163
+ # CIDFonts have the ROS (Registry-Ordering-Supplement) operator
164
+ #
165
+ # @return [Boolean] True if CIDFont
166
+ def cid_font?
167
+ has_key?(:ros)
168
+ end
169
+
170
+ # Get the ROS (Registry, Ordering, Supplement) for CIDFonts
171
+ #
172
+ # @return [Array<Integer>, nil] [registry_sid, ordering_sid, supplement]
173
+ def ros
174
+ @dict[:ros]
175
+ end
176
+
177
+ # Get the CID count for CIDFonts
178
+ #
179
+ # @return [Integer] Number of CIDs
180
+ def cid_count
181
+ fetch(:cid_count)
182
+ end
183
+
184
+ # Get the FDArray offset for CIDFonts
185
+ #
186
+ # FDArray is a Font DICT INDEX for CIDFonts
187
+ #
188
+ # @return [Integer, nil] Offset to FDArray
189
+ def fd_array
190
+ @dict[:fd_array]
191
+ end
192
+
193
+ # Get the FDSelect offset for CIDFonts
194
+ #
195
+ # FDSelect maps CIDs to Font DICTs in FDArray
196
+ #
197
+ # @return [Integer, nil] Offset to FDSelect
198
+ def fd_select
199
+ @dict[:fd_select]
200
+ end
201
+
202
+ # Get the CharString type
203
+ #
204
+ # @return [Integer] CharString type (typically 2 for Type 2 CharStrings)
205
+ def charstring_type
206
+ fetch(:charstring_type)
207
+ end
208
+
209
+ # Check if the font has a custom charset
210
+ #
211
+ # @return [Boolean] True if charset is custom (not 0, 1, or 2)
212
+ def custom_charset?
213
+ charset_val = charset
214
+ charset_val && charset_val > 2
215
+ end
216
+
217
+ # Check if the font has a custom encoding
218
+ #
219
+ # @return [Boolean] True if encoding is custom (not 0 or 1)
220
+ def custom_encoding?
221
+ encoding_val = encoding
222
+ encoding_val && encoding_val > 1
223
+ end
224
+
225
+ private
226
+
227
+ # Get Top DICT specific operators
228
+ #
229
+ # @return [Hash] Top DICT operators merged with base operators
230
+ def derived_operators
231
+ TOP_DICT_OPERATORS
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end