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.
- checksums.yaml +4 -4
- data/BUG-stitcher-drops-isolated-cps.md +58 -0
- data/BUG-stitcher-drops-plane1-codepoints.md +310 -0
- data/BUG-stitcher-gid-cap-65535.md +110 -0
- data/CHANGELOG.md +106 -0
- data/README.adoc +121 -68
- data/benchmark/compile_benchmark.rb +70 -0
- data/docs/CFF2_SUPPORT.adoc +184 -0
- data/docs/STITCHER_GUIDE.adoc +151 -0
- data/docs/SVG_TO_GLYF.adoc +118 -0
- data/docs/UFO_COMPILATION.adoc +119 -0
- data/lib/fontisan/collection/writer.rb +5 -6
- data/lib/fontisan/error.rb +31 -0
- data/lib/fontisan/stitcher/deduplicator.rb +47 -0
- data/lib/fontisan/stitcher/glyph_limit.rb +53 -0
- data/lib/fontisan/stitcher/glyph_signature.rb +51 -0
- data/lib/fontisan/stitcher.rb +188 -167
- data/lib/fontisan/svg_to_glyf/assembler.rb +132 -0
- data/lib/fontisan/svg_to_glyf/document.rb +83 -0
- data/lib/fontisan/svg_to_glyf/geometry/affine_transform.rb +112 -0
- data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +45 -0
- data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +91 -0
- data/lib/fontisan/svg_to_glyf/geometry.rb +13 -0
- data/lib/fontisan/svg_to_glyf/path/command.rb +18 -0
- data/lib/fontisan/svg_to_glyf/path/contour_builder.rb +140 -0
- data/lib/fontisan/svg_to_glyf/path/parser.rb +98 -0
- data/lib/fontisan/svg_to_glyf/path/state.rb +79 -0
- data/lib/fontisan/svg_to_glyf/path.rb +14 -0
- data/lib/fontisan/svg_to_glyf.rb +62 -0
- data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +216 -0
- data/lib/fontisan/tables/cff.rb +1 -0
- data/lib/fontisan/tables/cff2/dict_encoder.rb +94 -0
- data/lib/fontisan/tables/cff2/fd_select.rb +69 -0
- data/lib/fontisan/tables/cff2/header.rb +34 -0
- data/lib/fontisan/tables/cff2/index_builder.rb +79 -0
- data/lib/fontisan/tables/cff2.rb +4 -0
- data/lib/fontisan/ufo/compile/cbdt_cblc.rb +103 -0
- data/lib/fontisan/ufo/compile/cff2.rb +181 -0
- data/lib/fontisan/ufo/compile/cff2_subroutines.rb +39 -0
- data/lib/fontisan/ufo/compile/colr.rb +80 -0
- data/lib/fontisan/ufo/compile/cpal.rb +61 -0
- data/lib/fontisan/ufo/compile/math.rb +143 -0
- data/lib/fontisan/ufo/compile/meta.rb +51 -0
- data/lib/fontisan/ufo/compile/otf2_compiler.rb +46 -0
- data/lib/fontisan/ufo/compile/sbix.rb +99 -0
- data/lib/fontisan/ufo/compile/svg_table.rb +60 -0
- data/lib/fontisan/ufo/compile/variable_otf.rb +75 -0
- data/lib/fontisan/ufo/compile.rb +11 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +3 -0
- 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
|