fontisan 0.4.6 → 0.4.7

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/BUG-stitcher-drops-isolated-cps.md +58 -0
  3. data/BUG-stitcher-drops-plane1-codepoints.md +310 -0
  4. data/BUG-stitcher-gid-cap-65535.md +110 -0
  5. data/CHANGELOG.md +106 -0
  6. data/README.adoc +121 -68
  7. data/benchmark/compile_benchmark.rb +70 -0
  8. data/docs/CFF2_SUPPORT.adoc +184 -0
  9. data/docs/STITCHER_GUIDE.adoc +151 -0
  10. data/docs/SVG_TO_GLYF.adoc +118 -0
  11. data/docs/UFO_COMPILATION.adoc +119 -0
  12. data/lib/fontisan/collection/writer.rb +5 -6
  13. data/lib/fontisan/error.rb +31 -0
  14. data/lib/fontisan/stitcher/deduplicator.rb +47 -0
  15. data/lib/fontisan/stitcher/glyph_limit.rb +53 -0
  16. data/lib/fontisan/stitcher/glyph_signature.rb +51 -0
  17. data/lib/fontisan/stitcher.rb +188 -167
  18. data/lib/fontisan/svg_to_glyf/assembler.rb +132 -0
  19. data/lib/fontisan/svg_to_glyf/document.rb +83 -0
  20. data/lib/fontisan/svg_to_glyf/geometry/affine_transform.rb +112 -0
  21. data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +45 -0
  22. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +91 -0
  23. data/lib/fontisan/svg_to_glyf/geometry.rb +13 -0
  24. data/lib/fontisan/svg_to_glyf/path/command.rb +18 -0
  25. data/lib/fontisan/svg_to_glyf/path/contour_builder.rb +140 -0
  26. data/lib/fontisan/svg_to_glyf/path/parser.rb +98 -0
  27. data/lib/fontisan/svg_to_glyf/path/state.rb +79 -0
  28. data/lib/fontisan/svg_to_glyf/path.rb +14 -0
  29. data/lib/fontisan/svg_to_glyf.rb +62 -0
  30. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +216 -0
  31. data/lib/fontisan/tables/cff.rb +1 -0
  32. data/lib/fontisan/tables/cff2/dict_encoder.rb +94 -0
  33. data/lib/fontisan/tables/cff2/fd_select.rb +69 -0
  34. data/lib/fontisan/tables/cff2/header.rb +34 -0
  35. data/lib/fontisan/tables/cff2/index_builder.rb +79 -0
  36. data/lib/fontisan/tables/cff2.rb +4 -0
  37. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +103 -0
  38. data/lib/fontisan/ufo/compile/cff2.rb +181 -0
  39. data/lib/fontisan/ufo/compile/cff2_subroutines.rb +39 -0
  40. data/lib/fontisan/ufo/compile/colr.rb +80 -0
  41. data/lib/fontisan/ufo/compile/cpal.rb +61 -0
  42. data/lib/fontisan/ufo/compile/math.rb +143 -0
  43. data/lib/fontisan/ufo/compile/meta.rb +51 -0
  44. data/lib/fontisan/ufo/compile/otf2_compiler.rb +46 -0
  45. data/lib/fontisan/ufo/compile/sbix.rb +99 -0
  46. data/lib/fontisan/ufo/compile/svg_table.rb +60 -0
  47. data/lib/fontisan/ufo/compile/variable_otf.rb +75 -0
  48. data/lib/fontisan/ufo/compile.rb +11 -0
  49. data/lib/fontisan/version.rb +1 -1
  50. data/lib/fontisan.rb +3 -0
  51. metadata +41 -2
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType CFF2 table from UFO glyphs.
7
+ #
8
+ # CFF2 is simpler than CFF1: no Name INDEX, String INDEX, Encoding,
9
+ # or Charset. The Top DICT references CharStrings and a Font DICT
10
+ # INDEX (which wraps at least one Font DICT pointing to a Private
11
+ # DICT).
12
+ #
13
+ # Layout (static font, single Font DICT, empty Private DICT):
14
+ #
15
+ # Header (5 bytes)
16
+ # Top DICT (variable — offsets to CharStrings + Font DICT INDEX)
17
+ # Global Subr INDEX (4 bytes — empty)
18
+ # CharStrings INDEX (variable)
19
+ # Font DICT INDEX (variable — wraps one Font DICT)
20
+ # Font DICT: DICT with Private operator [0, 0] (empty Private)
21
+ #
22
+ # The Top DICT offsets depend on the Top DICT's own size — a
23
+ # circular dependency resolved with fixed-point iteration (the
24
+ # same pattern as the CFF1 builder).
25
+ #
26
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/cff2
27
+ module Cff2
28
+ # CFF2 Top DICT operator encodings.
29
+ CHARSTRINGS_OPERATOR = 17 # 0x11
30
+ VARIATION_STORE_OPERATOR = 24 # 0x18
31
+ FONT_DICT_INDEX_OPERATOR = [12, 36].freeze # 0x0C24
32
+ PRIVATE_OPERATOR = 18 # 0x12
33
+
34
+ # @param font [Fontisan::Ufo::Font]
35
+ # @param glyphs [Array<Fontisan::Ufo::Glyph>] in gid order
36
+ # @param variation_store [String, nil] ItemVariationStore bytes
37
+ # for variable CFF2 fonts. When present, embedded between the
38
+ # GlobalSubr INDEX and CharStrings INDEX, and referenced from
39
+ # the Top DICT via operator 24.
40
+ # @return [String] CFF2 table bytes
41
+ def self.build(_font, glyphs:, variation_store: nil)
42
+ charstrings = glyphs.map { |g| charstring_for(g) }
43
+ global_subr_index = empty_global_subr_index
44
+ font_dict = build_font_dict(private_size: 0, private_offset: 0)
45
+ font_dict_index = Tables::Cff2::IndexBuilder.build([font_dict])
46
+ vs_bytes = variation_store&.b
47
+
48
+ # Fixed-point iteration: encode Top DICT, compute offsets, repeat.
49
+ top_dict = encode_top_dict(
50
+ charstrings_offset: 0, font_dict_index_offset: 0,
51
+ variation_store_offset: vs_bytes ? 0 : nil
52
+ )
53
+ layout = compute_layout(top_dict:, charstrings:, global_subr_index:,
54
+ font_dict_index:, variation_store: vs_bytes)
55
+
56
+ 10.times do
57
+ top_dict = encode_top_dict(
58
+ charstrings_offset: layout[:charstrings_offset],
59
+ font_dict_index_offset: layout[:font_dict_index_offset],
60
+ variation_store_offset: layout[:variation_store_offset],
61
+ )
62
+ next_layout = compute_layout(top_dict:, charstrings:, global_subr_index:,
63
+ font_dict_index:, variation_store: vs_bytes)
64
+ break if same_offsets?(layout, next_layout)
65
+
66
+ layout = next_layout
67
+ end
68
+
69
+ assemble(layout:)
70
+ end
71
+
72
+ # ---------- charstring per-glyph ----------
73
+
74
+ def self.charstring_for(glyph)
75
+ return empty_charstring(glyph.width.to_i) if glyph.contours.empty?
76
+
77
+ builder = Tables::Cff::CharStringBuilder.new
78
+ builder.build(glyph.to_outline, width: glyph.width.to_i)
79
+ rescue StandardError
80
+ empty_charstring(glyph.width.to_i)
81
+ end
82
+
83
+ def self.empty_charstring(width)
84
+ builder = Tables::Cff::CharStringBuilder.new
85
+ builder.build_empty(width: width.zero? ? nil : width)
86
+ end
87
+
88
+ # ---------- DICT encoding ----------
89
+
90
+ # Encode the Top DICT with offsets to CharStrings, Font DICT INDEX,
91
+ # and optionally VariationStore.
92
+ def self.encode_top_dict(charstrings_offset:, font_dict_index_offset:,
93
+ variation_store_offset: nil)
94
+ io = +""
95
+ io << Tables::Cff2::DictEncoder.encode_entry(
96
+ [charstrings_offset], CHARSTRINGS_OPERATOR
97
+ )
98
+ io << Tables::Cff2::DictEncoder.encode_entry(
99
+ [font_dict_index_offset], FONT_DICT_INDEX_OPERATOR
100
+ )
101
+ if variation_store_offset
102
+ io << Tables::Cff2::DictEncoder.encode_entry(
103
+ [variation_store_offset], VARIATION_STORE_OPERATOR
104
+ )
105
+ end
106
+ io
107
+ end
108
+
109
+ # Encode a Font DICT: just the Private operator [size, offset].
110
+ def self.build_font_dict(private_size:, private_offset:)
111
+ Tables::Cff2::DictEncoder.encode_entry(
112
+ [private_size, private_offset], PRIVATE_OPERATOR
113
+ )
114
+ end
115
+
116
+ # ---------- layout ----------
117
+
118
+ # Compute byte offsets for each section. The Top DICT's size
119
+ # determines where the Global Subr INDEX starts, which cascades
120
+ # to all subsequent offsets. When a VariationStore is present,
121
+ # it sits between the GlobalSubr INDEX and the CharStrings INDEX.
122
+ def self.compute_layout(top_dict:, charstrings:, global_subr_index:,
123
+ font_dict_index:, variation_store: nil)
124
+ charstrings_index = Tables::Cff2::IndexBuilder.build(charstrings)
125
+
126
+ header_size = Tables::Cff2::Header::HEADER_SIZE
127
+ global_subr_offset = header_size + top_dict.bytesize
128
+ post_global_subr = global_subr_offset + global_subr_index.bytesize
129
+
130
+ if variation_store
131
+ variation_store_offset = post_global_subr
132
+ charstrings_offset = variation_store_offset + variation_store.bytesize
133
+ else
134
+ variation_store_offset = nil
135
+ charstrings_offset = post_global_subr
136
+ end
137
+ font_dict_index_offset = charstrings_offset + charstrings_index.bytesize
138
+
139
+ {
140
+ top_dict: top_dict,
141
+ charstrings_index: charstrings_index,
142
+ global_subr_index: global_subr_index,
143
+ font_dict_index: font_dict_index,
144
+ variation_store: variation_store,
145
+ variation_store_offset: variation_store_offset,
146
+ charstrings_offset: charstrings_offset,
147
+ font_dict_index_offset: font_dict_index_offset,
148
+ }
149
+ end
150
+
151
+ def self.same_offsets?(a, b)
152
+ a[:charstrings_offset] == b[:charstrings_offset] &&
153
+ a[:font_dict_index_offset] == b[:font_dict_index_offset] &&
154
+ a[:variation_store_offset] == b[:variation_store_offset]
155
+ end
156
+
157
+ def self.empty_global_subr_index
158
+ Tables::Cff2::IndexBuilder.build([])
159
+ end
160
+
161
+ # ---------- assembly ----------
162
+
163
+ def self.assemble(layout:)
164
+ io = +""
165
+ io << Tables::Cff2::Header.build(top_dict_size: layout[:top_dict].bytesize)
166
+ io << layout[:top_dict]
167
+ io << layout[:global_subr_index]
168
+ io << layout[:variation_store] if layout[:variation_store]
169
+ io << layout[:charstrings_index]
170
+ io << layout[:font_dict_index]
171
+ io
172
+ end
173
+
174
+ private_class_method :charstring_for, :empty_charstring,
175
+ :encode_top_dict, :build_font_dict,
176
+ :compute_layout, :same_offsets?,
177
+ :empty_global_subr_index, :assemble
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # CFF2 subroutine utilities — bias calculation and INDEX building.
7
+ #
8
+ # Subroutines are shared charstring sequences referenced by index
9
+ # from the caller. CFF2 supports two kinds:
10
+ # - GlobalSubr: shared across all CharStrings
11
+ # - LocalSubr: per Font DICT, referenced via operator 19 (0x13)
12
+ #
13
+ # The bias for callsubr/callgsubr index lookup is:
14
+ # 0-1240 → bias 107
15
+ # 1241-33800 → bias 1131
16
+ # 33801+ → bias 32768
17
+ #
18
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#charstring-operators
19
+ module Cff2Subrs
20
+ # Calculate the bias for subroutine index lookup.
21
+ # @param count [Integer] number of subroutines in the INDEX
22
+ # @return [Integer] bias
23
+ def self.bias(count)
24
+ return 107 if count <= 1240
25
+ return 1131 if count <= 33800
26
+
27
+ 32768
28
+ end
29
+
30
+ # Build a Subr INDEX (same structure for Global and Local).
31
+ # @param subrs [Array<String>] charstring bytes for each subr
32
+ # @return [String] INDEX binary
33
+ def self.build_index(subrs)
34
+ Tables::Cff2::IndexBuilder.build(subrs.map(&:b))
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `COLR` (Color) table (version 0).
7
+ #
8
+ # COLR v0 maps glyph IDs to layers. Each layer is a reference
9
+ # to another glyph painted in a specific color. This produces
10
+ # multi-color glyphs (e.g., emoji) using simple flat layers.
11
+ #
12
+ # Header (14 bytes):
13
+ # uint16 version (= 0)
14
+ # uint16 numBaseGlyphRecords
15
+ # Offset32 baseGlyphRecordsOffset
16
+ # Offset32 layerRecordsOffset
17
+ # uint16 numLayerRecords
18
+ #
19
+ # BaseGlyphRecord (6 bytes):
20
+ # uint16 glyphID
21
+ # uint16 firstLayerIndex
22
+ # uint16 numLayers
23
+ #
24
+ # LayerRecord (3 bytes):
25
+ # uint16 layerGlyphID
26
+ # uint8 paletteIndex
27
+ #
28
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/colr
29
+ module Colr
30
+ VERSION = 0
31
+ HEADER_SIZE = 14
32
+ BASE_GLYPH_RECORD_SIZE = 6
33
+ LAYER_RECORD_SIZE = 3
34
+
35
+ # @param base_glyphs [Array<Hash>] each with :gid (Integer) and
36
+ # :layers (Array<Hash> with :layer_gid and :palette_index)
37
+ # @return [String, nil] COLR table bytes, or nil if no base glyphs
38
+ def self.build(base_glyphs:)
39
+ return nil if base_glyphs.nil? || base_glyphs.empty?
40
+
41
+ base_records = +""
42
+ layer_records = +""
43
+ first_layer_index = 0
44
+
45
+ base_glyphs.each do |bg|
46
+ layers = bg[:layers] || []
47
+ num_layers = layers.size
48
+
49
+ base_records << [
50
+ bg[:gid] || 0,
51
+ first_layer_index,
52
+ num_layers,
53
+ ].pack("nnn")
54
+
55
+ layers.each do |layer|
56
+ layer_records << [
57
+ layer[:layer_gid] || 0,
58
+ layer[:palette_index] || 0,
59
+ ].pack("nC")
60
+ end
61
+
62
+ first_layer_index += num_layers
63
+ end
64
+
65
+ num_base = base_glyphs.size
66
+ base_offset = HEADER_SIZE
67
+ layer_offset = base_offset + (num_base * BASE_GLYPH_RECORD_SIZE)
68
+ num_layer_records = layer_records.bytesize / LAYER_RECORD_SIZE
69
+
70
+ io = +""
71
+ io << [VERSION, num_base, base_offset, layer_offset,
72
+ num_layer_records].pack("nnNNn")
73
+ io << base_records
74
+ io << layer_records
75
+ io
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `CPAL` (Color Palette) table.
7
+ #
8
+ # CPAL stores one or more palettes of BGRA color records,
9
+ # referenced by COLR layers and other color-glyph mechanisms.
10
+ #
11
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/cpal
12
+ module Cpal
13
+ VERSION = 0
14
+ HEADER_SIZE = 12
15
+ COLOR_RECORD_SIZE = 4 # BGRA
16
+
17
+ # Color value object: BGRA uint8 tuple.
18
+ Color = Struct.new(:blue, :green, :red, :alpha, keyword_init: true) do
19
+ def to_bytes
20
+ [blue || 0, green || 0, red || 0, alpha || 255].pack("C4")
21
+ end
22
+ end
23
+
24
+ # @param palettes [Array<Array<Color>>] one or more palettes,
25
+ # each an array of Color values. All palettes must have the
26
+ # same number of entries.
27
+ # @return [String, nil] CPAL table bytes, or nil if no palettes
28
+ def self.build(palettes:)
29
+ return nil if palettes.nil? || palettes.empty?
30
+
31
+ num_entries = palettes.first.size
32
+ num_palettes = palettes.size
33
+ num_records = num_entries * num_palettes
34
+
35
+ indices = Array.new(num_palettes) { |i| i * num_entries }
36
+ records = palettes.flatten
37
+
38
+ offset = HEADER_SIZE + (num_palettes * 2) # header + indices
39
+
40
+ io = +""
41
+ io << [VERSION, num_entries, num_palettes, num_records, offset].pack("nnnnN")
42
+ indices.each { |idx| io << [idx].pack("n") }
43
+ records.each { |c| io << color_bytes(c) }
44
+ io
45
+ end
46
+
47
+ def self.color_bytes(color)
48
+ return color.to_bytes if color.is_a?(Color)
49
+
50
+ b = color[:blue] || color["blue"] || 0
51
+ g = color[:green] || color["green"] || 0
52
+ r = color[:red] || color["red"] || 0
53
+ a = color[:alpha] || color["alpha"] || 255
54
+ [b, g, r, a].pack("C4")
55
+ end
56
+
57
+ private_class_method :color_bytes
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `MATH` table for mathematical formula layout.
7
+ #
8
+ # The MATH table provides constants and glyph info for rendering
9
+ # equations in math-aware environments (Word, LibreOffice, TeX).
10
+ #
11
+ # Layout:
12
+ # Header (10 bytes):
13
+ # uint16 version (= 1)
14
+ # Offset32 mathConstantsOffset
15
+ # Offset32 mathGlyphInfoOffset
16
+ # Offset32 mathVariantsOffset
17
+ #
18
+ # MathConstants: 57 layout constants (int16 + bool each)
19
+ # MathGlyphInfo: italic correction, top accent, extension
20
+ # MathVariants: glyph assembly for stretched delimiters
21
+ #
22
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/math
23
+ module MathTable
24
+ VERSION = 1
25
+ HEADER_SIZE = 10
26
+
27
+ # @param constants [Hash, nil] math layout constants
28
+ # @param glyph_info [Hash, nil] per-glyph math data
29
+ # @param variants [Hash, nil] stretched delimiter data
30
+ # @return [String, nil] MATH table bytes, or nil if no data
31
+ def self.build(constants: nil, glyph_info: nil, variants: nil)
32
+ return nil unless constants || glyph_info || variants
33
+
34
+ constants_bytes = constants ? build_constants(constants) : nil
35
+ glyph_info_bytes = glyph_info ? build_glyph_info(glyph_info) : nil
36
+ variants_bytes = variants ? build_variants(variants) : nil
37
+
38
+ constants_off = constants_bytes ? HEADER_SIZE : 0
39
+ gi_off = glyph_info_bytes ? constants_off + (constants_bytes&.bytesize || 0) : 0
40
+ if gi_off.zero? && glyph_info_bytes
41
+ gi_off = HEADER_SIZE + (constants_bytes&.bytesize || 0)
42
+ end
43
+ variants_off = variants_bytes ? gi_off + (glyph_info_bytes&.bytesize || 0) : 0
44
+ if variants_off.zero? && variants_bytes
45
+ variants_off = HEADER_SIZE + (constants_bytes&.bytesize || 0) + (glyph_info_bytes&.bytesize || 0)
46
+ end
47
+
48
+ io = +""
49
+ io << [0x00010000, constants_off, gi_off, variants_off].pack("Nnnn")
50
+ io << constants_bytes if constants_bytes
51
+ io << glyph_info_bytes if glyph_info_bytes
52
+ io << variants_bytes if variants_bytes
53
+ io
54
+ end
55
+
56
+ # Build the MathConstants subtable (57 constants).
57
+ # Each constant is int16 (or int32 for larger ranges).
58
+ def self.build_constants(constants)
59
+ io = +""
60
+ # The 57 constants are a fixed sequence. We pack what's
61
+ # provided and use 0 (default) for missing ones.
62
+ constant_names.each do |name|
63
+ value = constants[name] || constants[name.to_s] || 0
64
+ io << [value.to_i].pack("s>")
65
+ end
66
+ io
67
+ end
68
+
69
+ def self.constant_names
70
+ %i[
71
+ scriptPercentScaleDown
72
+ scriptScriptPercentScaleDown
73
+ delimitedSubFormulaMinHeight
74
+ displayOperatorMinHeight
75
+ mathLeading
76
+ axisHeight
77
+ accentBaseHeight
78
+ flattenedAccentBaseHeight
79
+ subscriptShiftDown
80
+ subscriptTopMax
81
+ subscriptBaselineDropMin
82
+ superscriptShiftUp
83
+ superscriptShiftUpCramped
84
+ superscriptBottomMin
85
+ superscriptBaselineDropMax
86
+ subSuperscriptGapMin
87
+ superscriptBottomMaxWithSubscript
88
+ spaceAfterScript
89
+ upperLimitGapMin
90
+ upperLimitBaselineRiseMin
91
+ lowerLimitGapMin
92
+ lowerLimitBaselineDropMin
93
+ stackTopShiftUp
94
+ stackTopDisplayStyleShiftUp
95
+ stackBottomShiftDown
96
+ stackBottomDisplayStyleShiftDown
97
+ stackGapMin
98
+ stackDisplayStyleGapMin
99
+ stretchStackTopShiftUp
100
+ stretchStackBottomShiftDown
101
+ fractionNumeratorShiftUp
102
+ fractionNumeratorDisplayStyleShiftUp
103
+ fractionDenominatorShiftDown
104
+ fractionDenominatorDisplayStyleShiftDown
105
+ fractionNumeratorGapMin
106
+ fractionNumDisplayStyleGapMin
107
+ fractionRuleThickness
108
+ fractionDenominatorGapMin
109
+ fractionDenomDisplayStyleGapMin
110
+ skewedFractionHorizontalGap
111
+ skewedFractionVerticalGap
112
+ overbarVerticalGap
113
+ overbarRuleThickness
114
+ overbarExtraAscender
115
+ underbarVerticalGap
116
+ underbarRuleThickness
117
+ underbarExtraDescender
118
+ radicalVerticalGap
119
+ radicalDisplayStyleVerticalGap
120
+ radicalRuleThickness
121
+ radicalExtraAscender
122
+ radicalKernBeforeDegree
123
+ radicalKernAfterDegree
124
+ radicalDegreeBottomRaisePercent
125
+ ].freeze
126
+ end
127
+
128
+ def self.build_glyph_info(_info)
129
+ # Minimal: just the header with offsets (all empty for now)
130
+ [0, 0, 0, 0].pack("NNNN")
131
+ end
132
+
133
+ def self.build_variants(_variants)
134
+ # Minimal: just the header with offset (empty for now)
135
+ [0, 0, 0, 0].pack("NNNN")
136
+ end
137
+
138
+ private_class_method :build_constants, :constant_names,
139
+ :build_glyph_info, :build_variants
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # Builds the OpenType `meta` table.
7
+ #
8
+ # The meta table stores font metadata as tagged data — longer-form
9
+ # strings than the `name` table supports. Common tags:
10
+ # "dlng" — design languages (BCP-47 tags)
11
+ # "slng" — supported languages
12
+ #
13
+ # @see https://learn.microsoft.com/en-us/typography/opentype/spec/meta
14
+ module Meta
15
+ VERSION = 1
16
+ HEADER_SIZE = 16
17
+ DATA_MAP_SIZE = 12 # tag(4) + offset(4) + length(4)
18
+
19
+ # @param data [Hash<String,String>] tag → UTF-8 string value
20
+ # @return [String, nil] meta table bytes, or nil if no data
21
+ def self.build(data:)
22
+ return nil if data.nil? || data.empty?
23
+
24
+ count = data.size
25
+ data_offset = HEADER_SIZE + (count * DATA_MAP_SIZE)
26
+
27
+ io = +""
28
+ io << [VERSION, 0, count, data_offset].pack("NNNN")
29
+
30
+ # Two iterations over the same data: the meta table has two
31
+ # distinct output sections (data map entries then data values)
32
+ # that cannot be interleaved. The map-entry section records
33
+ # offsets that depend on the total size of the value section,
34
+ # so the value section must be produced last.
35
+ #
36
+ # rubocop:disable Style/CombinableLoops
37
+ offset = data_offset
38
+ data.each do |tag, value|
39
+ io << tag.ljust(4, " ")[0, 4]
40
+ io << [offset, value.bytesize].pack("NN")
41
+ offset += value.bytesize
42
+ end
43
+ data.each_value { |value| io << value.b }
44
+ # rubocop:enable Style/CombinableLoops
45
+
46
+ io
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ module Compile
6
+ # UFO → OTF (CFF2). Uses CFF2 outlines instead of CFF1.
7
+ #
8
+ # CFF2 uses the same OTTO sfnt signature as CFF1. The difference
9
+ # is the table tag: `CFF2` (vs `CFF ` for CFF1). CFF2 enables
10
+ # variable font support and improved subroutinization.
11
+ #
12
+ # Note: CFF2 does NOT bypass the 65,535 glyph cap. The maxp
13
+ # table's numGlyphs is uint16 in all OpenType versions, and the
14
+ # CFF2 CharStrings INDEX count must match it. For > 65,535
15
+ # glyphs, use TTC splitting.
16
+ class Otf2Compiler < BaseCompiler
17
+ SFNT_VERSION = SFNT_VERSION_OPEN_TYPE
18
+
19
+ def build_outline_tables
20
+ {
21
+ "CFF2" => Cff2.build(font, glyphs: font.glyphs.values),
22
+ }
23
+ end
24
+
25
+ def compile(output_path:)
26
+ glyphs = font.glyphs.values
27
+
28
+ tables = {
29
+ "head" => Head.build(font, glyphs: glyphs, loca_format: Head::LOCA_FORMAT_LONG),
30
+ "hhea" => Hhea.build(font, glyphs: glyphs),
31
+ "maxp" => Maxp.build(font, glyphs: glyphs, version: Maxp::VERSION_OPEN_TYPE),
32
+ "OS/2" => Os2.build(font, glyphs: glyphs),
33
+ "name" => Name.build(font),
34
+ "post" => Post.build(font),
35
+ "hmtx" => Hmtx.build(font, glyphs: glyphs),
36
+ "cmap" => Cmap.build(font, glyphs: glyphs),
37
+ "CFF2" => Cff2.build(font, glyphs: glyphs),
38
+ }
39
+
40
+ write(tables, output_path)
41
+ output_path
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end