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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Woff2
5
+ # WOFF2 Table Directory Entry
6
+ #
7
+ # [`Woff2::Directory`](lib/fontisan/woff2/directory.rb) represents
8
+ # a single table entry in the WOFF2 table directory. Unlike WOFF,
9
+ # WOFF2 uses variable-length encoding for sizes and supports table
10
+ # transformations for better compression.
11
+ #
12
+ # Each entry contains:
13
+ # - flags (1 byte): Contains tag index and transformation version
14
+ # - tag (0 or 4 bytes): Table tag (omitted if using known tag index)
15
+ # - origLength (UIntBase128): Original uncompressed table length
16
+ # - transformLength (UIntBase128, optional): Transformed data length
17
+ #
18
+ # Flags byte structure:
19
+ # - Bits 0-5: Table tag index (0-62 = known tags, 63 = custom tag)
20
+ # - Bits 6-7: Transformation version
21
+ #
22
+ # Reference: https://www.w3.org/TR/WOFF2/#table_dir_format
23
+ #
24
+ # @example Create entry for known table
25
+ # entry = Directory::Entry.new
26
+ # entry.tag = "glyf"
27
+ # entry.orig_length = 12000
28
+ # entry.flags = entry.calculate_flags
29
+ #
30
+ # @example Create entry for custom table
31
+ # entry = Directory::Entry.new
32
+ # entry.tag = "CUST"
33
+ # entry.orig_length = 5000
34
+ # entry.flags = 0x3F # Custom tag indicator
35
+ module Directory
36
+ # Known table tags with assigned indices (0-62)
37
+ # Index 63 (0x3F) indicates a custom tag follows
38
+ KNOWN_TAGS = [
39
+ "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
40
+ "cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
41
+ "EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea",
42
+ "vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
43
+ "CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
44
+ "bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
45
+ "gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop",
46
+ "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill"
47
+ ].freeze
48
+
49
+ # Transformation versions
50
+ TRANSFORM_NONE = 0
51
+ TRANSFORM_GLYF_LOCA = 0 # Applied to both glyf and loca
52
+ TRANSFORM_HMTX = 0 # Applied to hmtx
53
+
54
+ # Custom tag indicator
55
+ CUSTOM_TAG_INDEX = 0x3F
56
+
57
+ # WOFF2 Table Directory Entry
58
+ #
59
+ # Represents a single table in the WOFF2 font with all metadata
60
+ # needed for decompression and reconstruction.
61
+ class Entry
62
+ attr_accessor :tag, :flags, :orig_length, :transform_length, :offset # Calculated during encoding
63
+
64
+ def initialize
65
+ @tag = nil
66
+ @flags = 0
67
+ @orig_length = 0
68
+ @transform_length = nil
69
+ @offset = 0
70
+ end
71
+
72
+ # Calculate flags byte for this entry
73
+ #
74
+ # @return [Integer] Flags byte (0-255)
75
+ def calculate_flags
76
+ tag_index = KNOWN_TAGS.index(tag) || CUSTOM_TAG_INDEX
77
+ transform_version = determine_transform_version
78
+
79
+ # Combine tag index (bits 0-5) and transform version (bits 6-7)
80
+ (transform_version << 6) | tag_index
81
+ end
82
+
83
+ # Check if table uses a known tag
84
+ #
85
+ # @return [Boolean] True if known tag
86
+ def known_tag?
87
+ KNOWN_TAGS.include?(tag)
88
+ end
89
+
90
+ # Check if table is transformed
91
+ #
92
+ # @return [Boolean] True if transformed
93
+ def transformed?
94
+ transform_version != TRANSFORM_NONE && transform_length
95
+ end
96
+
97
+ # Get transformation version from flags
98
+ #
99
+ # @return [Integer] Transform version (0-3)
100
+ def transform_version
101
+ (flags >> 6) & 0x03
102
+ end
103
+
104
+ # Get tag index from flags
105
+ #
106
+ # @return [Integer] Tag index (0-63)
107
+ def tag_index
108
+ flags & 0x3F
109
+ end
110
+
111
+ # Determine if this table should be transformed
112
+ #
113
+ # For Phase 2 Milestone 2.1, we support transformation flags
114
+ # but don't implement the actual transformations yet.
115
+ #
116
+ # @return [Integer] Transform version
117
+ def determine_transform_version
118
+ # For this milestone, we don't apply transformations
119
+ # but we recognize which tables could be transformed
120
+ TRANSFORM_NONE
121
+ end
122
+
123
+ # Check if table can be transformed (glyf, loca, hmtx)
124
+ #
125
+ # @return [Boolean] True if transformable
126
+ def transformable?
127
+ %w[glyf loca hmtx].include?(tag)
128
+ end
129
+
130
+ # Calculate size of this entry when serialized
131
+ #
132
+ # @return [Integer] Size in bytes
133
+ def serialized_size
134
+ size = 1 # flags byte
135
+ size += 4 unless known_tag? # custom tag
136
+ size += uint_base128_size(orig_length)
137
+ size += uint_base128_size(transform_length) if transformed?
138
+ size
139
+ end
140
+
141
+ private
142
+
143
+ # Estimate size of UIntBase128 encoded value
144
+ #
145
+ # @param value [Integer] Value to encode
146
+ # @return [Integer] Size in bytes (1-5)
147
+ def uint_base128_size(value)
148
+ return 1 if value.nil? || value < 128
149
+
150
+ bytes = 0
151
+ v = value
152
+ while v.positive?
153
+ bytes += 1
154
+ v >>= 7
155
+ end
156
+ [bytes, 5].min # Max 5 bytes
157
+ end
158
+ end
159
+
160
+ # Encode an integer as UIntBase128
161
+ #
162
+ # Variable-length encoding where:
163
+ # - If value < 128, use 1 byte
164
+ # - Otherwise, use high bit to indicate continuation
165
+ #
166
+ # @param value [Integer] Value to encode
167
+ # @return [String] Binary encoded data
168
+ def self.encode_uint_base128(value)
169
+ return [value].pack("C") if value < 128
170
+
171
+ bytes = []
172
+ v = value
173
+
174
+ # Build bytes from least to most significant
175
+ loop do
176
+ bytes.unshift(v & 0x7F)
177
+ v >>= 7
178
+ break if v.zero?
179
+ end
180
+
181
+ # Set high bit on all but last byte
182
+ (0...bytes.length - 1).each do |i|
183
+ bytes[i] |= 0x80
184
+ end
185
+
186
+ bytes.pack("C*")
187
+ end
188
+
189
+ # Decode UIntBase128 from IO
190
+ #
191
+ # @param io [IO] Input stream
192
+ # @return [Integer] Decoded value
193
+ # @raise [Error] If encoding is invalid
194
+ def self.decode_uint_base128(io)
195
+ result = 0
196
+ 5.times do
197
+ byte = io.read(1)&.unpack1("C")
198
+ return nil unless byte
199
+
200
+ # Check if high bit is set (continuation)
201
+ if (byte & 0x80).zero?
202
+ return (result << 7) | byte
203
+ else
204
+ result = (result << 7) | (byte & 0x7F)
205
+ end
206
+ end
207
+
208
+ # If we're here, encoding is invalid (> 5 bytes)
209
+ raise Fontisan::Error, "Invalid UIntBase128 encoding"
210
+ end
211
+
212
+ # Encode 255UInt16 value
213
+ #
214
+ # Used in transformed glyf table:
215
+ # - 0-252: value itself (1 byte)
216
+ # - 253: next byte + 253 (2 bytes)
217
+ # - 254: next 2 bytes as big-endian (3 bytes)
218
+ # - 255: next 2 bytes + 506 (3 bytes)
219
+ #
220
+ # @param value [Integer] Value to encode (0-65535)
221
+ # @return [String] Binary encoded data
222
+ def self.encode_255_uint16(value)
223
+ if value < 253
224
+ [value].pack("C")
225
+ elsif value < 506
226
+ [253, value - 253].pack("C*")
227
+ elsif value < 65536
228
+ [254].pack("C") + [value].pack("n")
229
+ else
230
+ [255].pack("C") + [value - 506].pack("n")
231
+ end
232
+ end
233
+
234
+ # Decode 255UInt16 from IO
235
+ #
236
+ # @param io [IO] Input stream
237
+ # @return [Integer] Decoded value
238
+ def self.decode_255_uint16(io)
239
+ first = io.read(1)&.unpack1("C")
240
+ return nil unless first
241
+
242
+ case first
243
+ when 0..252
244
+ first
245
+ when 253
246
+ second = io.read(1)&.unpack1("C")
247
+ 253 + second
248
+ when 254
249
+ io.read(2)&.unpack1("n")
250
+ when 255
251
+ value = io.read(2)&.unpack1("n")
252
+ value + 506
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module Fontisan
6
+ module Woff2
7
+ # WOFF2 Header structure
8
+ #
9
+ # [`Woff2::Header`](lib/fontisan/woff2/header.rb) represents the main
10
+ # header of a WOFF2 file according to W3C WOFF2 specification.
11
+ #
12
+ # The header is more compact than WOFF, using 48 bytes.
13
+ #
14
+ # Structure (all big-endian):
15
+ # - uint32: signature (0x774F4632 'wOF2')
16
+ # - uint32: flavor (0x00010000 for TTF, 0x4F54544F for CFF)
17
+ # - uint32: file_length (total WOFF2 file size)
18
+ # - uint16: numTables (number of font tables)
19
+ # - uint16: reserved (must be 0)
20
+ # - uint32: totalSfntSize (uncompressed font size)
21
+ # - uint32: totalCompressedSize (size of compressed data block)
22
+ # - uint16: majorVersion (major version of WOFF file)
23
+ # - uint16: minorVersion (minor version of WOFF file)
24
+ # - uint32: metaOffset (offset to metadata, 0 if none)
25
+ # - uint32: metaLength (compressed metadata length)
26
+ # - uint32: metaOrigLength (uncompressed metadata length)
27
+ # - uint32: privOffset (offset to private data, 0 if none)
28
+ # - uint32: privLength (length of private data)
29
+ #
30
+ # Reference: https://www.w3.org/TR/WOFF2/#woff20Header
31
+ #
32
+ # @example Create a header
33
+ # header = Woff2::Header.new
34
+ # header.signature = 0x774F4632
35
+ # header.flavor = 0x00010000
36
+ # header.num_tables = 10
37
+ class Woff2Header < BinData::Record
38
+ endian :big
39
+
40
+ uint32 :signature # 'wOF2' magic number
41
+ uint32 :flavor # Font format (TTF or CFF)
42
+ uint32 :file_length # Total WOFF2 file size
43
+ uint16 :num_tables # Number of font tables
44
+ uint16 :reserved # Reserved, must be 0
45
+ uint32 :total_sfnt_size # Uncompressed font size
46
+ uint32 :total_compressed_size # Compressed data block size
47
+ uint16 :major_version # Major version number
48
+ uint16 :minor_version # Minor version number
49
+ uint32 :meta_offset # Metadata block offset (0 if none)
50
+ uint32 :meta_length # Compressed metadata length
51
+ uint32 :meta_orig_length # Uncompressed metadata length
52
+ uint32 :priv_offset # Private data offset (0 if none)
53
+ uint32 :priv_length # Private data length
54
+
55
+ # WOFF2 signature constant
56
+ SIGNATURE = 0x774F4632 # 'wOF2'
57
+
58
+ # Check if signature is valid
59
+ #
60
+ # @return [Boolean] True if signature is valid
61
+ def valid_signature?
62
+ signature == SIGNATURE
63
+ end
64
+
65
+ # Check if font is TrueType flavored
66
+ #
67
+ # @return [Boolean] True if TrueType
68
+ def truetype?
69
+ [0x00010000, 0x74727565].include?(flavor) # 'true'
70
+ end
71
+
72
+ # Check if font is CFF flavored
73
+ #
74
+ # @return [Boolean] True if CFF/OpenType
75
+ def cff?
76
+ flavor == 0x4F54544F # 'OTTO'
77
+ end
78
+
79
+ # Check if metadata is present
80
+ #
81
+ # @return [Boolean] True if metadata exists
82
+ def has_metadata?
83
+ meta_offset.positive? && meta_length.positive?
84
+ end
85
+
86
+ # Check if private data is present
87
+ #
88
+ # @return [Boolean] True if private data exists
89
+ def has_private_data?
90
+ priv_offset.positive? && priv_length.positive?
91
+ end
92
+
93
+ # Get header size in bytes
94
+ #
95
+ # @return [Integer] Header size (always 48 bytes)
96
+ def self.header_size
97
+ 48
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Woff2
5
+ # Table transformer for WOFF2 encoding
6
+ #
7
+ # [`Woff2::TableTransformer`](lib/fontisan/woff2/table_transformer.rb)
8
+ # handles table transformations that improve compression in WOFF2.
9
+ # The WOFF2 spec defines transformations for glyf/loca and hmtx tables.
10
+ #
11
+ # For Phase 2 Milestone 2.1:
12
+ # - Architecture is in place for transformations
13
+ # - Actual transformation implementations are marked as TODO
14
+ # - Tables are copied as-is without transformation
15
+ # - This allows valid WOFF2 generation while leaving room for optimization
16
+ #
17
+ # Future milestones will implement:
18
+ # - glyf/loca transformation (combined stream, delta encoding)
19
+ # - hmtx transformation (compact representation)
20
+ #
21
+ # Reference: https://www.w3.org/TR/WOFF2/#table_tranforms
22
+ #
23
+ # @example Transform tables for WOFF2
24
+ # transformer = TableTransformer.new(font)
25
+ # glyf_data = transformer.transform_table("glyf")
26
+ class TableTransformer
27
+ # @return [Object] Font object with table access
28
+ attr_reader :font
29
+
30
+ # Initialize transformer with font
31
+ #
32
+ # @param font [TrueTypeFont, OpenTypeFont] Source font
33
+ def initialize(font)
34
+ @font = font
35
+ end
36
+
37
+ # Transform a table for WOFF2 encoding
38
+ #
39
+ # For Milestone 2.1, this returns the original table data
40
+ # without transformation. The architecture supports future
41
+ # implementation of actual transformations.
42
+ #
43
+ # @param tag [String] Table tag
44
+ # @return [String, nil] Transformed (or original) table data
45
+ def transform_table(tag)
46
+ case tag
47
+ when "glyf"
48
+ transform_glyf
49
+ when "loca"
50
+ transform_loca
51
+ when "hmtx"
52
+ transform_hmtx
53
+ else
54
+ # No transformation, return original data
55
+ get_table_data(tag)
56
+ end
57
+ end
58
+
59
+ # Check if a table can be transformed
60
+ #
61
+ # @param tag [String] Table tag
62
+ # @return [Boolean] True if table supports transformation
63
+ def transformable?(tag)
64
+ %w[glyf loca hmtx].include?(tag)
65
+ end
66
+
67
+ # Determine transformation version for a table
68
+ #
69
+ # For Milestone 2.1, always returns TRANSFORM_NONE since
70
+ # we don't implement transformations yet.
71
+ #
72
+ # @param tag [String] Table tag
73
+ # @return [Integer] Transformation version (0 = none)
74
+ def transformation_version(_tag)
75
+ # For this milestone, no transformations are applied
76
+ Directory::TRANSFORM_NONE
77
+ end
78
+
79
+ private
80
+
81
+ # Transform glyf table
82
+ #
83
+ # The WOFF2 glyf transformation combines glyf and loca into a
84
+ # single stream with delta-encoded coordinates and flags.
85
+ #
86
+ # TODO: Implement full glyf transformation for better compression.
87
+ # For now, returns original table data.
88
+ #
89
+ # @return [String, nil] Transformed glyf data
90
+ def transform_glyf
91
+ # TODO: Implement glyf transformation
92
+ # This would involve:
93
+ # 1. Parse all glyphs from glyf table
94
+ # 2. Combine with loca offsets
95
+ # 3. Create transformed stream with:
96
+ # - nContour values
97
+ # - nPoints values
98
+ # - Flag bytes (with run-length encoding)
99
+ # - x-coordinates (delta-encoded)
100
+ # - y-coordinates (delta-encoded)
101
+ # - Instruction bytes
102
+ # 4. Use 255UInt16 encoding for variable-length integers
103
+ #
104
+ # Reference: https://www.w3.org/TR/WOFF2/#glyf_table_format
105
+
106
+ get_table_data("glyf")
107
+ end
108
+
109
+ # Transform loca table
110
+ #
111
+ # In WOFF2, loca is combined with glyf during transformation.
112
+ # When glyf is transformed, loca table is omitted from output.
113
+ #
114
+ # TODO: Implement loca transformation (combined with glyf).
115
+ # For now, returns original table data.
116
+ #
117
+ # @return [String, nil] Transformed loca data
118
+ def transform_loca
119
+ # TODO: Implement loca transformation
120
+ # When glyf transformation is implemented, loca will be:
121
+ # 1. Combined into the transformed glyf stream
122
+ # 2. Reconstructed during decompression
123
+ # 3. Not present as separate table in WOFF2
124
+
125
+ get_table_data("loca")
126
+ end
127
+
128
+ # Transform hmtx table
129
+ #
130
+ # The WOFF2 hmtx transformation stores advance widths more efficiently
131
+ # by exploiting redundancy (many glyphs have same advance width).
132
+ #
133
+ # TODO: Implement hmtx transformation for better compression.
134
+ # For now, returns original table data.
135
+ #
136
+ # @return [String, nil] Transformed hmtx data
137
+ def transform_hmtx
138
+ # TODO: Implement hmtx transformation
139
+ # This would involve:
140
+ # 1. Parse hmtx table
141
+ # 2. Extract common advance widths
142
+ # 3. Identify proportional vs monospace sections
143
+ # 4. Use flags to indicate structure
144
+ # 5. Store only unique advance widths
145
+ # 6. Store LSB array separately
146
+ #
147
+ # Reference: https://www.w3.org/TR/WOFF2/#hmtx_table_format
148
+
149
+ get_table_data("hmtx")
150
+ end
151
+
152
+ # Get raw table data from font
153
+ #
154
+ # @param tag [String] Table tag
155
+ # @return [String, nil] Table data or nil if not found
156
+ def get_table_data(tag)
157
+ return nil unless font.respond_to?(:table_data)
158
+
159
+ font.table_data(tag)
160
+ end
161
+ end
162
+ end
163
+ end