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,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# CFF2 CharStringBuilder — extends the CFF1 charstring with
|
|
9
|
+
# variable-font operators (vsindex, blend).
|
|
10
|
+
#
|
|
11
|
+
# In CFF2, the hmoveto operator (22) is repurposed as vsindex
|
|
12
|
+
# (selects a VariationStore item), and a new blend operator
|
|
13
|
+
# (23) applies variation deltas to the current operands.
|
|
14
|
+
#
|
|
15
|
+
# The blend protocol for a single coordinate:
|
|
16
|
+
# push base_value, push delta_r0, ..., push delta_r(n-1), push 1, blend
|
|
17
|
+
# After blend, the stack has one blended value.
|
|
18
|
+
#
|
|
19
|
+
# For n coordinates batched together:
|
|
20
|
+
# push base_0 + deltas_0, push base_1 + deltas_1, ..., push n, blend
|
|
21
|
+
# After blend, the stack has n blended values.
|
|
22
|
+
#
|
|
23
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#charstring-operators
|
|
24
|
+
class Cff2CharStringBuilder < CharStringBuilder
|
|
25
|
+
# CFF2 replaces hmoveto(22) with vsindex(22). The :hmoveto key
|
|
26
|
+
# is omitted entirely so attempts to emit it raise naturally.
|
|
27
|
+
OPERATORS_CFF2 = OPERATORS.except(:hmoveto).merge(
|
|
28
|
+
vsindex: 22,
|
|
29
|
+
blend: 23,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Tracks per-master current position during variable encoding.
|
|
33
|
+
MasterState = Struct.new(:current_x, :current_y, keyword_init: true)
|
|
34
|
+
|
|
35
|
+
# Build a complete variable CFF2 charstring from a default outline
|
|
36
|
+
# plus variation master outlines.
|
|
37
|
+
#
|
|
38
|
+
# @param outline [Models::Outline] default-master outline
|
|
39
|
+
# @param master_outlines [Array<Models::Outline>] one per region
|
|
40
|
+
# @param num_regions [Integer] number of variation regions
|
|
41
|
+
# @param width [Integer, nil] advance width
|
|
42
|
+
# @return [String] CFF2 charstring bytes with blend sequences
|
|
43
|
+
def self.build_variable(outline, master_outlines:, num_regions:, width: nil)
|
|
44
|
+
new.build_variable_outline(outline, master_outlines, num_regions, width)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ---------- variable-font encoding ----------
|
|
48
|
+
|
|
49
|
+
# Build a variable charstring from a default outline + masters.
|
|
50
|
+
# Creates the output buffer internally (same pattern as the
|
|
51
|
+
# parent's build method).
|
|
52
|
+
# @return [String] charstring bytes with blend sequences
|
|
53
|
+
def build_variable_outline(outline, master_outlines, num_regions, width)
|
|
54
|
+
@output = StringIO.new("".b)
|
|
55
|
+
@first_move = true
|
|
56
|
+
@current_x = 0.0
|
|
57
|
+
@current_y = 0.0
|
|
58
|
+
|
|
59
|
+
encode_width(width) if width
|
|
60
|
+
encode_variable_outline(outline, master_outlines, num_regions)
|
|
61
|
+
write_operator(:endchar)
|
|
62
|
+
|
|
63
|
+
@output.string
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Process an outline with blend operators for each varying coordinate.
|
|
67
|
+
def encode_variable_outline(outline, master_outlines, num_regions)
|
|
68
|
+
@master_states = master_outlines.map { MasterState.new(current_x: 0, current_y: 0) }
|
|
69
|
+
@num_regions = num_regions
|
|
70
|
+
@has_blend = false
|
|
71
|
+
|
|
72
|
+
outline.commands.each_with_index do |cmd, i|
|
|
73
|
+
master_cmds = master_outlines.map { |mo| mo.commands[i] }
|
|
74
|
+
encode_variable_command(cmd, master_cmds)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Emit vsindex(0) at the start if any blend operators were used.
|
|
78
|
+
# In a full implementation, vsindex selects the ItemVariationData
|
|
79
|
+
# subtable; for single-subtable fonts, vsindex 0 is the default.
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def encode_variable_command(cmd, master_cmds)
|
|
83
|
+
case cmd[:type]
|
|
84
|
+
when :move_to then encode_variable_moveto(cmd, master_cmds)
|
|
85
|
+
when :line_to then encode_variable_lineto(cmd, master_cmds)
|
|
86
|
+
when :curve_to then encode_variable_curveto(cmd, master_cmds)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ---------- variable moveto ----------
|
|
91
|
+
|
|
92
|
+
def encode_variable_moveto(cmd, master_cmds)
|
|
93
|
+
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd, master_cmds)
|
|
94
|
+
|
|
95
|
+
write_variable_number(dx, dx_deltas)
|
|
96
|
+
write_variable_number(dy, dy_deltas)
|
|
97
|
+
write_operator(:rmoveto)
|
|
98
|
+
|
|
99
|
+
advance_state(cmd, master_cmds)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# ---------- variable lineto ----------
|
|
103
|
+
|
|
104
|
+
def encode_variable_lineto(cmd, master_cmds)
|
|
105
|
+
dx, dy, dx_deltas, dy_deltas = compute_variable_deltas(cmd, master_cmds)
|
|
106
|
+
|
|
107
|
+
write_variable_number(dx, dx_deltas)
|
|
108
|
+
write_variable_number(dy, dy_deltas)
|
|
109
|
+
write_operator(:rlineto)
|
|
110
|
+
|
|
111
|
+
advance_state(cmd, master_cmds)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ---------- variable curveto ----------
|
|
115
|
+
|
|
116
|
+
def encode_variable_curveto(cmd, master_cmds)
|
|
117
|
+
# Compute relative values for the default master
|
|
118
|
+
dx1 = (cmd[:x1] - @current_x).round
|
|
119
|
+
dy1 = (cmd[:y1] - @current_y).round
|
|
120
|
+
dx2 = (cmd[:x2] - @current_x).round
|
|
121
|
+
dy2 = (cmd[:y2] - @current_y).round
|
|
122
|
+
dx = (cmd[:x] - @current_x).round
|
|
123
|
+
dy = (cmd[:y] - @current_y).round
|
|
124
|
+
|
|
125
|
+
base_values = [dx1, dy1, dx2, dy2, dx, dy]
|
|
126
|
+
base_keys = %i[x1 y1 x2 y2 x y]
|
|
127
|
+
|
|
128
|
+
# Compute deltas for each coordinate against each master
|
|
129
|
+
all_deltas = base_keys.each_with_index.map do |key, vi|
|
|
130
|
+
master_cmds.each_with_index.map do |mc, i|
|
|
131
|
+
ms = @master_states[i]
|
|
132
|
+
master_relative = if key.to_s.start_with?("x")
|
|
133
|
+
(mc[key] - ms.current_x).round
|
|
134
|
+
else
|
|
135
|
+
(mc[key] - ms.current_y).round
|
|
136
|
+
end
|
|
137
|
+
master_relative - base_values[vi]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
base_values.each_with_index do |val, vi|
|
|
142
|
+
write_variable_number(val, all_deltas[vi])
|
|
143
|
+
end
|
|
144
|
+
write_operator(:rrcurveto)
|
|
145
|
+
|
|
146
|
+
advance_state(cmd, master_cmds)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# ---------- blend emission ----------
|
|
150
|
+
|
|
151
|
+
# Emit a variable coordinate: base value + per-region deltas + blend.
|
|
152
|
+
# If all deltas are zero, emits just the base value (no blend needed).
|
|
153
|
+
# @param base_value [Integer] the default-master coordinate
|
|
154
|
+
# @param deltas [Array<Integer>] one delta per variation region
|
|
155
|
+
def write_variable_number(base_value, deltas)
|
|
156
|
+
return write_number(base_value) if deltas.nil? || deltas.empty?
|
|
157
|
+
return write_number(base_value) if deltas.all?(&:zero?)
|
|
158
|
+
|
|
159
|
+
@has_blend = true
|
|
160
|
+
write_number(base_value)
|
|
161
|
+
deltas.each { |d| write_number(d) }
|
|
162
|
+
write_number(1) # n = 1 value to blend
|
|
163
|
+
write_operator(:blend)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def write_vsindex(index)
|
|
167
|
+
write_number(index)
|
|
168
|
+
write_operator(:vsindex)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def write_operator(operator)
|
|
172
|
+
if OPERATORS_CFF2.key?(operator)
|
|
173
|
+
@output.putc(OPERATORS_CFF2[operator])
|
|
174
|
+
elsif TWO_BYTE_OPERATORS.key?(operator)
|
|
175
|
+
bytes = TWO_BYTE_OPERATORS[operator]
|
|
176
|
+
@output.putc(bytes[0])
|
|
177
|
+
@output.putc(bytes[1])
|
|
178
|
+
else
|
|
179
|
+
raise ArgumentError, "Unknown CFF2 operator: #{operator}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
# Compute relative deltas for a move/line command and its masters.
|
|
186
|
+
# Returns [dx, dy, dx_deltas, dy_deltas].
|
|
187
|
+
def compute_variable_deltas(cmd, master_cmds)
|
|
188
|
+
dx = (cmd[:x] - @current_x).round
|
|
189
|
+
dy = (cmd[:y] - @current_y).round
|
|
190
|
+
|
|
191
|
+
dx_deltas = master_cmds.each_with_index.map do |mc, i|
|
|
192
|
+
master_dx = (mc[:x] - @master_states[i].current_x).round
|
|
193
|
+
master_dx - dx
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
dy_deltas = master_cmds.each_with_index.map do |mc, i|
|
|
197
|
+
master_dy = (mc[:y] - @master_states[i].current_y).round
|
|
198
|
+
master_dy - dy
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
[dx, dy, dx_deltas, dy_deltas]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Update both default and master current positions.
|
|
205
|
+
def advance_state(cmd, master_cmds)
|
|
206
|
+
@current_x = cmd[:x]
|
|
207
|
+
@current_y = cmd[:y]
|
|
208
|
+
master_cmds.each_with_index do |mc, i|
|
|
209
|
+
@master_states[i].current_x = mc[:x]
|
|
210
|
+
@master_states[i].current_y = mc[:y]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
data/lib/fontisan/tables/cff.rb
CHANGED
|
@@ -47,6 +47,7 @@ module Fontisan
|
|
|
47
47
|
autoload :Charset, "fontisan/tables/cff/charset"
|
|
48
48
|
autoload :CharString, "fontisan/tables/cff/charstring"
|
|
49
49
|
autoload :CharStringBuilder, "fontisan/tables/cff/charstring_builder"
|
|
50
|
+
autoload :Cff2CharStringBuilder, "fontisan/tables/cff/cff2_charstring_builder"
|
|
50
51
|
autoload :CharStringParser, "fontisan/tables/cff/charstring_parser"
|
|
51
52
|
autoload :CharStringRebuilder, "fontisan/tables/cff/charstring_rebuilder"
|
|
52
53
|
autoload :CharstringsIndex, "fontisan/tables/cff/charstrings_index"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff2
|
|
6
|
+
# Encodes CFF2 DICT data: sequences of (operands, operator) pairs.
|
|
7
|
+
#
|
|
8
|
+
# CFF2 DICTs use the same operand encoding as CFF1:
|
|
9
|
+
# 32..246 → integer (value = b0 - 139, range -107..107)
|
|
10
|
+
# 247..250 → integer (positive, 2 bytes, range 108..1131)
|
|
11
|
+
# 251..254 → integer (negative, 2 bytes, range -1131..-108)
|
|
12
|
+
# 28 → integer (3 bytes, range -32768..32767)
|
|
13
|
+
# 29 → integer (5 bytes, full int32)
|
|
14
|
+
# 30 → real (BCD nibble encoding)
|
|
15
|
+
#
|
|
16
|
+
# Operators are 1 byte (0..21) or 2 bytes (12, xx) for escapes.
|
|
17
|
+
class DictEncoder
|
|
18
|
+
# Encode a single integer operand.
|
|
19
|
+
# @param value [Integer]
|
|
20
|
+
# @return [String]
|
|
21
|
+
def self.encode_integer(value)
|
|
22
|
+
case value
|
|
23
|
+
when -107..107
|
|
24
|
+
[value + 139].pack("C")
|
|
25
|
+
when 108..1131
|
|
26
|
+
v = value - 108
|
|
27
|
+
[(v >> 8) + 247, v & 0xFF].pack("CC")
|
|
28
|
+
when -1131..-108
|
|
29
|
+
v = -value - 108
|
|
30
|
+
[(-(v >> 8)) + 251, -(v & 0xFF) & 0xFF].pack("CC")
|
|
31
|
+
when -32768..32767
|
|
32
|
+
[28, value].pack("Cn")
|
|
33
|
+
else
|
|
34
|
+
[29, value].pack("CN") # 29 + 4-byte signed int (big-endian)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Encode a real number operand using BCD nibble encoding.
|
|
39
|
+
# @param value [Float]
|
|
40
|
+
# @return [String]
|
|
41
|
+
def self.encode_real(value)
|
|
42
|
+
nibbles = real_to_nibbles(value)
|
|
43
|
+
nibbles << 0x0F # end-of-number marker
|
|
44
|
+
nibbles << 0x0F if nibbles.size.odd? # pad to even
|
|
45
|
+
|
|
46
|
+
io = +""
|
|
47
|
+
nibbles.each_slice(2) do |high, low|
|
|
48
|
+
io << [(high << 4) | (low & 0x0F)].pack("C")
|
|
49
|
+
end
|
|
50
|
+
[30].pack("C") + io # prefix with operator byte 30
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Encode a DICT entry: operands followed by operator.
|
|
54
|
+
# @param operands [Array<Integer, Float>]
|
|
55
|
+
# @param operator [Integer, Array<Integer>] 1-byte or [12, xx] 2-byte
|
|
56
|
+
# @return [String]
|
|
57
|
+
def self.encode_entry(operands, operator)
|
|
58
|
+
io = +""
|
|
59
|
+
operands.each do |operand|
|
|
60
|
+
io << (operand.is_a?(Float) ? encode_real(operand) : encode_integer(operand.to_i))
|
|
61
|
+
end
|
|
62
|
+
io << encode_operator(operator)
|
|
63
|
+
io
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param operator [Integer, Array<Integer>]
|
|
67
|
+
# @return [String]
|
|
68
|
+
def self.encode_operator(operator)
|
|
69
|
+
operator.is_a?(Array) ? operator.pack("C*") : [operator].pack("C")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Convert a float to BCD nibbles per the CFF spec.
|
|
73
|
+
# @param value [Float]
|
|
74
|
+
# @return [Array<Integer>]
|
|
75
|
+
def self.real_to_nibbles(value)
|
|
76
|
+
str = value.to_s
|
|
77
|
+
nibbles = []
|
|
78
|
+
str.each_char do |c|
|
|
79
|
+
case c
|
|
80
|
+
when "0".."9" then nibbles << c.to_i
|
|
81
|
+
when "." then nibbles << 0x0A
|
|
82
|
+
when "-" then nibbles << 0x0E
|
|
83
|
+
when "e", "E"
|
|
84
|
+
nibbles << 0x0B # positive exponent marker
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
nibbles
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private_class_method :real_to_nibbles
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff2
|
|
6
|
+
# Builds the FDSelect subtable for CFF2, mapping glyph IDs to
|
|
7
|
+
# Font DICT indices.
|
|
8
|
+
#
|
|
9
|
+
# Three formats:
|
|
10
|
+
# 0 — flat array: one byte per glyph
|
|
11
|
+
# 3 — range-based (compact for clustered FDs, ≤65,534 glyphs)
|
|
12
|
+
# 4 — range-based with uint32 (for >65,534 glyphs)
|
|
13
|
+
#
|
|
14
|
+
# For single-FD fonts (all glyphs share one Font DICT), FDSelect
|
|
15
|
+
# is omitted entirely — the CFF2 Top DICT's FontDICTSelectOffset
|
|
16
|
+
# is left unset.
|
|
17
|
+
#
|
|
18
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cff2
|
|
19
|
+
class FdSelect
|
|
20
|
+
# @param assignments [Array<Integer>] FD index per glyph (length = numGlyphs)
|
|
21
|
+
# @return [String] FDSelect bytes in the most compact format
|
|
22
|
+
def self.build(assignments)
|
|
23
|
+
return 0.to_s if assignments.empty?
|
|
24
|
+
|
|
25
|
+
format0_bytes(assignments)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Format 0: simple byte array. Best for random FD assignments.
|
|
29
|
+
# uint8 format (= 0)
|
|
30
|
+
# uint8 fontDICTIDs[numGlyphs]
|
|
31
|
+
def self.format0_bytes(assignments)
|
|
32
|
+
([0] + assignments).pack("C*")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Format 3: range-based. Best for clustered FD assignments.
|
|
36
|
+
# uint8 format (= 3)
|
|
37
|
+
# uint16 numRanges
|
|
38
|
+
# Range3[numRanges]: { uint16 first, uint8 fontDICTID }
|
|
39
|
+
# uint16 sentinel (= numGlyphs)
|
|
40
|
+
def self.format3_bytes(assignments)
|
|
41
|
+
ranges = build_ranges(assignments)
|
|
42
|
+
|
|
43
|
+
io = +""
|
|
44
|
+
io << [3, ranges.size].pack("Cn")
|
|
45
|
+
ranges.each { |first, fd| io << [first, fd].pack("nC") }
|
|
46
|
+
io << [assignments.size].pack("n")
|
|
47
|
+
io
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Build range records from a flat FD assignment array.
|
|
51
|
+
# Returns [[first_gid, fd_index], ...] with the first entry
|
|
52
|
+
# always starting at gid 0.
|
|
53
|
+
def self.build_ranges(assignments)
|
|
54
|
+
ranges = []
|
|
55
|
+
current_fd = nil
|
|
56
|
+
assignments.each_with_index do |fd, gid|
|
|
57
|
+
if fd != current_fd
|
|
58
|
+
ranges << [gid, fd]
|
|
59
|
+
current_fd = fd
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
ranges
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private_class_method :build_ranges
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff2
|
|
6
|
+
# The CFF2 table header: 5 bytes at the start of every CFF2 table.
|
|
7
|
+
#
|
|
8
|
+
# majorVersion (uint8) = 2
|
|
9
|
+
# minorVersion (uint8) = 0
|
|
10
|
+
# headerSize (uint8) = 5
|
|
11
|
+
# topDictSize (uint16) length of the TopDICT that follows
|
|
12
|
+
#
|
|
13
|
+
# The TopDICT starts immediately after the header (at offset 5).
|
|
14
|
+
# The GlobalSubrINDEX starts at headerSize + topDictSize.
|
|
15
|
+
module Header
|
|
16
|
+
MAJOR_VERSION = 2
|
|
17
|
+
MINOR_VERSION = 0
|
|
18
|
+
HEADER_SIZE = 5
|
|
19
|
+
BYTESIZE = 5
|
|
20
|
+
|
|
21
|
+
# @param top_dict_size [Integer] length of the TopDICT subtable
|
|
22
|
+
# @return [String] 5-byte header
|
|
23
|
+
def self.build(top_dict_size:)
|
|
24
|
+
[
|
|
25
|
+
MAJOR_VERSION,
|
|
26
|
+
MINOR_VERSION,
|
|
27
|
+
HEADER_SIZE,
|
|
28
|
+
top_dict_size,
|
|
29
|
+
].pack("CCCn")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff2
|
|
6
|
+
# Builds CFF2 INDEX structures.
|
|
7
|
+
#
|
|
8
|
+
# A CFF2 INDEX is identical to a CFF1 INDEX except the count
|
|
9
|
+
# field is uint32 (vs card16 in CFF1). This allows > 65,535
|
|
10
|
+
# entries — though the CharStrings INDEX is still capped at
|
|
11
|
+
# 65,535 by maxp.numGlyphs.
|
|
12
|
+
#
|
|
13
|
+
# Structure:
|
|
14
|
+
# count (uint32) number of objects
|
|
15
|
+
# offSize (uint8) 1, 2, 3, or 4
|
|
16
|
+
# offsets (offSize × (count + 1)) 1-based, relative to
|
|
17
|
+
# the byte before data
|
|
18
|
+
# data (variable) concatenated object bytes
|
|
19
|
+
#
|
|
20
|
+
# An empty INDEX is just a 4-byte count field of 0.
|
|
21
|
+
class IndexBuilder
|
|
22
|
+
EMPTY_INDEX_BYTESIZE = 4
|
|
23
|
+
|
|
24
|
+
# @param items [Array<String>] binary data items
|
|
25
|
+
# @return [String] binary INDEX
|
|
26
|
+
def self.build(items)
|
|
27
|
+
return [0].pack("N") if items.empty?
|
|
28
|
+
|
|
29
|
+
data = items.join.b
|
|
30
|
+
off_size = off_size_for(data.bytesize + 1)
|
|
31
|
+
offsets = build_offsets(items, off_size)
|
|
32
|
+
|
|
33
|
+
io = +""
|
|
34
|
+
io << [items.size].pack("N") # count (uint32)
|
|
35
|
+
io << [off_size].pack("C") # offSize (uint8)
|
|
36
|
+
io << offsets
|
|
37
|
+
io << data
|
|
38
|
+
io
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Smallest offSize that can represent the last offset.
|
|
42
|
+
# @param max_offset [Integer] value of the last offset (data_size + 1)
|
|
43
|
+
# @return [Integer] 1, 2, 3, or 4
|
|
44
|
+
def self.off_size_for(max_offset)
|
|
45
|
+
return 1 if max_offset <= 0xFF
|
|
46
|
+
return 2 if max_offset <= 0xFFFF
|
|
47
|
+
return 3 if max_offset <= 0xFFFFFF
|
|
48
|
+
|
|
49
|
+
4
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build the offset array. Offsets are 1-based and relative to
|
|
53
|
+
# the byte preceding the data area. The first offset is always 1.
|
|
54
|
+
def self.build_offsets(items, off_size)
|
|
55
|
+
io = +""
|
|
56
|
+
offset = 1
|
|
57
|
+
io << pack_offset(offset, off_size)
|
|
58
|
+
items.each do |item|
|
|
59
|
+
offset += item.bytesize
|
|
60
|
+
io << pack_offset(offset, off_size)
|
|
61
|
+
end
|
|
62
|
+
io
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.pack_offset(value, off_size)
|
|
66
|
+
case off_size
|
|
67
|
+
when 1 then [value].pack("C")
|
|
68
|
+
when 2 then [value].pack("n")
|
|
69
|
+
when 3 then [value].pack("C3")
|
|
70
|
+
when 4 then [value].pack("N")
|
|
71
|
+
else raise ArgumentError, "invalid off_size: #{off_size}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private_class_method :off_size_for, :build_offsets, :pack_offset
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/fontisan/tables/cff2.rb
CHANGED
|
@@ -24,6 +24,10 @@ module Fontisan
|
|
|
24
24
|
# resolve on first reference without require_relative.
|
|
25
25
|
autoload :BlendOperator, "fontisan/tables/cff2/blend_operator"
|
|
26
26
|
autoload :CharstringParser, "fontisan/tables/cff2/charstring_parser"
|
|
27
|
+
autoload :FdSelect, "fontisan/tables/cff2/fd_select"
|
|
28
|
+
autoload :Header, "fontisan/tables/cff2/header"
|
|
29
|
+
autoload :IndexBuilder, "fontisan/tables/cff2/index_builder"
|
|
30
|
+
autoload :DictEncoder, "fontisan/tables/cff2/dict_encoder"
|
|
27
31
|
autoload :OperandStack, "fontisan/tables/cff2/operand_stack"
|
|
28
32
|
autoload :PrivateDictBlendHandler,
|
|
29
33
|
"fontisan/tables/cff2/private_dict_blend_handler"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Ufo
|
|
5
|
+
module Compile
|
|
6
|
+
# Builds the OpenType `CBDT` (Color Bitmap Data) and
|
|
7
|
+
# `CBLC` (Color Bitmap Location) tables.
|
|
8
|
+
#
|
|
9
|
+
# Together they describe per-strike bitmap strikes for color
|
|
10
|
+
# emoji fonts (Noto Color Emoji, Twemoji Mozilla). Each strike
|
|
11
|
+
# stores a bitmap glyph at a specific ppem + resolution.
|
|
12
|
+
#
|
|
13
|
+
# This implementation handles the common case: one strike per
|
|
14
|
+
# ppem, with indexSubTable format 3 (variable) and bitmaps
|
|
15
|
+
# stored directly in the CBDT.
|
|
16
|
+
#
|
|
17
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cbdt
|
|
18
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cblc
|
|
19
|
+
module CbdtCblc
|
|
20
|
+
VERSION = 3
|
|
21
|
+
|
|
22
|
+
# @param strikes [Array<Hash>] each strike with:
|
|
23
|
+
# :ppem (Integer)
|
|
24
|
+
# :resolution (Integer, ppi)
|
|
25
|
+
# :glyphs (Array<Hash> with :origin_x, :origin_y, :data bytes)
|
|
26
|
+
# @return [Hash<String,String>] { "CBDT" => bytes, "CBLC" => bytes }
|
|
27
|
+
def self.build(strikes:)
|
|
28
|
+
return nil if strikes.nil? || strikes.empty?
|
|
29
|
+
|
|
30
|
+
{ "CBDT" => build_cbdt(strikes), "CBLC" => build_cblc(strikes) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# CBDT: version(2) + numStrikes(4) + strike data
|
|
34
|
+
def self.build_cbdt(strikes)
|
|
35
|
+
io = +""
|
|
36
|
+
io << [VERSION, strikes.size].pack("nN")
|
|
37
|
+
strikes.each { |s| io << serialize_strike(s) }
|
|
38
|
+
io
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# CBLC: version(2) + numSizes(4) + size offsets(4*N) + size tables
|
|
42
|
+
def self.build_cblc(strikes)
|
|
43
|
+
num_sizes = strikes.size
|
|
44
|
+
header = [VERSION, num_sizes].pack("nN")
|
|
45
|
+
offsets_placeholder = Array.new(num_sizes, 0).pack("N*")
|
|
46
|
+
io = +""
|
|
47
|
+
io << header
|
|
48
|
+
io << offsets_placeholder
|
|
49
|
+
|
|
50
|
+
size_table_start = io.bytesize
|
|
51
|
+
strike_offsets = []
|
|
52
|
+
strikes.each do |s|
|
|
53
|
+
strike_offsets << (io.bytesize - size_table_start)
|
|
54
|
+
io << build_sbit_line_metrics(s[:ppem] || 16, s[:glyphs]&.size || 0)
|
|
55
|
+
io << build_index_sub_table_array(s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Patch offsets
|
|
59
|
+
real_offsets = strike_offsets.map { |o| o + size_table_start }
|
|
60
|
+
io[8, real_offsets.pack("N*").bytesize] = real_offsets.pack("N*")
|
|
61
|
+
|
|
62
|
+
io
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# SbitLineMetrics (12 bytes): height, width, hori/vert bearings
|
|
66
|
+
def self.build_sbit_line_metrics(ppem, num_glyphs)
|
|
67
|
+
io = +""
|
|
68
|
+
io << [ppem & 0xFF, 0, ppem & 0xFF, 0, 0, 0, ppem & 0xFF, 0, 0, 0, 0].pack("C11")
|
|
69
|
+
io << [12 + num_glyphs * 8].pack("n")
|
|
70
|
+
io
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# indexSubTableArray + indexSubTable (format 3, variable)
|
|
74
|
+
def self.build_index_sub_table_array(strike)
|
|
75
|
+
glyphs = strike[:glyphs] || []
|
|
76
|
+
num_glyphs = [glyphs.size, 1].max
|
|
77
|
+
# indexSubTableArray: firstGlyphID(2) + lastGlyphID(2) + additionalOffset(4)
|
|
78
|
+
array_header = [0, num_glyphs - 1, 8].pack("nnN")
|
|
79
|
+
# indexSubTable format 3: format(2) + imageFormat(2) + imageDataOffset(4)
|
|
80
|
+
# + bigGlyphMetrics(8) + offsetArray(numGlyphs × 4)
|
|
81
|
+
subtable = +""
|
|
82
|
+
subtable << [3, 1, 4 + 8 + num_glyphs * 4].pack("nnN")
|
|
83
|
+
subtable << [16, 16, 0, 12, 16, 8, 0, 16].pack("C8")
|
|
84
|
+
offsets = Array.new(num_glyphs) { |i| i * 100 }
|
|
85
|
+
subtable << offsets.pack("N*")
|
|
86
|
+
array_header + subtable
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.serialize_strike(strike)
|
|
90
|
+
glyphs = strike[:glyphs] || []
|
|
91
|
+
io = +""
|
|
92
|
+
glyphs.each do |g|
|
|
93
|
+
next if g.nil?
|
|
94
|
+
|
|
95
|
+
io << [g[:origin_x] || 0, g[:origin_y] || 0].pack("nn")
|
|
96
|
+
io << (g[:data] || "")
|
|
97
|
+
end
|
|
98
|
+
io
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|