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,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,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Fontisan
6
+ module Woff2
7
+ # Reconstructs hmtx table from WOFF2 transformed format
8
+ #
9
+ # WOFF2 hmtx transformation optimizes horizontal metrics by:
10
+ # - Using variable-length encoding for advance widths
11
+ # - Optionally deriving LSB from glyf bounding boxes
12
+ # - Omitting redundant trailing advance widths
13
+ #
14
+ # See: https://www.w3.org/TR/WOFF2/#hmtx_table_format
15
+ #
16
+ # @example Reconstructing hmtx table
17
+ # hmtx_data = HmtxTransformer.reconstruct(
18
+ # transformed_data,
19
+ # num_glyphs,
20
+ # number_of_h_metrics
21
+ # )
22
+ class HmtxTransformer
23
+ # Flags for hmtx transformation
24
+ HMTX_FLAG_EXPLICIT_ADVANCE_WIDTHS = 0x01
25
+ HMTX_FLAG_EXPLICIT_LSB_VALUES = 0x02
26
+ HMTX_FLAG_SYMMETRIC = 0x04
27
+
28
+ # Reconstruct hmtx table from transformed data
29
+ #
30
+ # @param transformed_data [String] The transformed hmtx table data
31
+ # @param num_glyphs [Integer] Number of glyphs
32
+ # @param num_h_metrics [Integer] From hhea.numberOfHMetrics
33
+ # @param glyf_lsbs [Array<Integer>, nil] LSB values from glyf bboxes (optional)
34
+ # @return [String] Standard hmtx table data
35
+ # @raise [InvalidFontError] If data is corrupted or invalid
36
+ def self.reconstruct(transformed_data, num_glyphs, num_h_metrics, glyf_lsbs = nil)
37
+ io = StringIO.new(transformed_data)
38
+
39
+ # Read transformation flags
40
+ flags = read_uint8(io)
41
+
42
+ # Read advance widths
43
+ advance_widths = []
44
+
45
+ if (flags & HMTX_FLAG_EXPLICIT_ADVANCE_WIDTHS).zero?
46
+ # Proportional encoding - read deltas
47
+ # First advance width is explicit
48
+ first_advance = read_255_uint16(io)
49
+ advance_widths << first_advance
50
+
51
+ # Remaining are deltas from previous
52
+ (num_h_metrics - 1).times do
53
+ delta = read_int16(io)
54
+ advance_widths << (advance_widths.last + delta)
55
+ end
56
+ else
57
+ # Explicit advance widths in transformed format
58
+ num_h_metrics.times do
59
+ advance_widths << read_255_uint16(io)
60
+ end
61
+ end
62
+
63
+ # Read LSB values
64
+ lsbs = []
65
+
66
+ if (flags & HMTX_FLAG_EXPLICIT_LSB_VALUES) != 0
67
+ # Explicit LSB values
68
+ num_glyphs.times do
69
+ lsbs << read_int16(io)
70
+ end
71
+ elsif glyf_lsbs
72
+ # Use LSB values from glyf bounding boxes
73
+ lsbs = glyf_lsbs
74
+ else
75
+ # Need to read LSB values for long metrics
76
+ num_h_metrics.times do
77
+ lsbs << read_int16(io)
78
+ end
79
+
80
+ # Remaining LSBs for glyphs that share the last advance width
81
+ (num_glyphs - num_h_metrics).times do
82
+ lsbs << read_int16(io)
83
+ end
84
+ end
85
+
86
+ # Build standard hmtx table
87
+ build_hmtx_table(advance_widths, lsbs, num_h_metrics, num_glyphs)
88
+ end
89
+
90
+ # Read variable-length 255UInt16 integer
91
+ #
92
+ # Format from WOFF2 spec:
93
+ # - value < 253: one byte
94
+ # - value == 253: 253 + next uint16
95
+ # - value == 254: 253 * 2 + next uint16
96
+ # - value == 255: 253 * 3 + next uint16
97
+ #
98
+ # @param io [StringIO] Input stream
99
+ # @return [Integer] Decoded value
100
+ def self.read_255_uint16(io)
101
+ code = read_uint8(io)
102
+
103
+ case code
104
+ when 255
105
+ 759 + read_uint16(io) # 253 * 3 + value
106
+ when 254
107
+ 506 + read_uint16(io) # 253 * 2 + value
108
+ when 253
109
+ 253 + read_uint16(io)
110
+ else
111
+ code
112
+ end
113
+ end
114
+
115
+ # Build standard hmtx table format
116
+ #
117
+ # Standard hmtx format:
118
+ # - longHorMetric[numberOfHMetrics] (advanceWidth, lsb pairs)
119
+ # - int16[numGlyphs - numberOfHMetrics] (additional LSBs)
120
+ #
121
+ # @param advance_widths [Array<Integer>] Advance widths
122
+ # @param lsbs [Array<Integer>] Left side bearings
123
+ # @param num_h_metrics [Integer] Number of entries with full hMetrics
124
+ # @param num_glyphs [Integer] Total number of glyphs
125
+ # @return [String] Standard hmtx table data
126
+ def self.build_hmtx_table(advance_widths, lsbs, num_h_metrics, num_glyphs)
127
+ data = +""
128
+
129
+ # Write longHorMetric array (advanceWidth + lsb pairs)
130
+ num_h_metrics.times do |i|
131
+ advance_width = advance_widths[i] || advance_widths.last
132
+ lsb = lsbs[i] || 0
133
+
134
+ data << [advance_width].pack("n") # uint16 advanceWidth
135
+ data << [lsb].pack("n") # int16 lsb
136
+ end
137
+
138
+ # Write remaining LSB values
139
+ # These glyphs all share the last advance width from the array
140
+ (num_h_metrics...num_glyphs).each do |i|
141
+ lsb = lsbs[i] || 0
142
+ data << [lsb].pack("n") # int16 lsb
143
+ end
144
+
145
+ data
146
+ end
147
+
148
+ # Helper methods for reading binary data
149
+
150
+ def self.read_uint8(io)
151
+ io.read(1)&.unpack1("C") || raise(EOFError, "Unexpected end of stream")
152
+ end
153
+
154
+ def self.read_uint16(io)
155
+ io.read(2)&.unpack1("n") || raise(EOFError, "Unexpected end of stream")
156
+ end
157
+
158
+ def self.read_int16(io)
159
+ value = read_uint16(io)
160
+ value > 0x7FFF ? value - 0x10000 : value
161
+ end
162
+ end
163
+ end
164
+ 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