fontisan 0.2.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +270 -131
  3. data/README.adoc +158 -4
  4. data/Rakefile +44 -47
  5. data/lib/fontisan/cli.rb +84 -33
  6. data/lib/fontisan/collection/builder.rb +81 -0
  7. data/lib/fontisan/collection/table_deduplicator.rb +76 -0
  8. data/lib/fontisan/commands/base_command.rb +16 -0
  9. data/lib/fontisan/commands/convert_command.rb +97 -170
  10. data/lib/fontisan/commands/instance_command.rb +71 -80
  11. data/lib/fontisan/commands/validate_command.rb +25 -0
  12. data/lib/fontisan/config/validation_rules.yml +1 -1
  13. data/lib/fontisan/constants.rb +10 -0
  14. data/lib/fontisan/converters/format_converter.rb +150 -1
  15. data/lib/fontisan/converters/outline_converter.rb +80 -18
  16. data/lib/fontisan/converters/woff_writer.rb +1 -1
  17. data/lib/fontisan/font_loader.rb +3 -5
  18. data/lib/fontisan/font_writer.rb +7 -6
  19. data/lib/fontisan/hints/hint_converter.rb +133 -0
  20. data/lib/fontisan/hints/postscript_hint_applier.rb +221 -140
  21. data/lib/fontisan/hints/postscript_hint_extractor.rb +100 -0
  22. data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
  23. data/lib/fontisan/hints/truetype_hint_extractor.rb +127 -0
  24. data/lib/fontisan/loading_modes.rb +2 -0
  25. data/lib/fontisan/models/font_export.rb +2 -2
  26. data/lib/fontisan/models/hint.rb +173 -1
  27. data/lib/fontisan/models/validation_report.rb +1 -1
  28. data/lib/fontisan/open_type_font.rb +25 -9
  29. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  30. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  31. data/lib/fontisan/pipeline/output_writer.rb +154 -0
  32. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  33. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  34. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  35. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  36. data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
  37. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  38. data/lib/fontisan/tables/cff/charstring.rb +33 -4
  39. data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
  40. data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
  41. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  42. data/lib/fontisan/tables/cff/dict_builder.rb +15 -0
  43. data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
  44. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  45. data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
  46. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  47. data/lib/fontisan/tables/cff.rb +2 -0
  48. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
  49. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  50. data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
  51. data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
  52. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  53. data/lib/fontisan/tables/cff2.rb +9 -4
  54. data/lib/fontisan/tables/cvar.rb +2 -41
  55. data/lib/fontisan/tables/gvar.rb +2 -41
  56. data/lib/fontisan/true_type_font.rb +24 -9
  57. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  58. data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
  59. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  60. data/lib/fontisan/validation/table_validator.rb +1 -1
  61. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  62. data/lib/fontisan/variation/converter.rb +120 -13
  63. data/lib/fontisan/variation/instance_writer.rb +341 -0
  64. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  65. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  66. data/lib/fontisan/variation/variation_preserver.rb +288 -0
  67. data/lib/fontisan/version.rb +1 -1
  68. data/lib/fontisan/version.rb.orig +9 -0
  69. data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
  70. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  71. data/lib/fontisan/woff2_font.rb +475 -470
  72. data/lib/fontisan/woff_font.rb +16 -11
  73. data/lib/fontisan.rb +12 -0
  74. metadata +31 -2
@@ -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