fontisan 0.4.0 → 0.4.2
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/README.adoc +30 -0
- data/lib/fontisan/stitcher/source.rb +145 -16
- data/lib/fontisan/ufo/compile/gpos.rb +227 -0
- data/lib/fontisan/ufo/compile.rb +1 -0
- data/lib/fontisan/ufo/convert/from_bin_data.rb +4 -1
- data/lib/fontisan/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14c31b390c5721b221f5c04239baf1d2330b668d142addd3e321b3bd868f7b7a
|
|
4
|
+
data.tar.gz: 726fe81623ef7f410aa09e3b7a7838b8e0df1c2304cf9b36d1ad0727bdcf03e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4c11a54cd876590a4e52df790eeadc490da5b3bad9ef2a0ff6186f560ff2b584c29bc2f7fac1881f25771a3a7031b1d1db5d7cf5c0c67c057ec254165d6b2ac
|
|
7
|
+
data.tar.gz: 556737df14831d0bf14a82ed34e27f9438f93fe0d1c51b6324d0e3f938de3b7ce136bcb0b4a3a8a50698e7e603a02227fd87ee74de530df60e81fcbd5d3a4e5b
|
data/README.adoc
CHANGED
|
@@ -2475,6 +2475,36 @@ fontisan stitch \
|
|
|
2475
2475
|
--output stitched.ttf
|
|
2476
2476
|
----
|
|
2477
2477
|
|
|
2478
|
+
=== Color emoji (CBDT/CBLC passthrough)
|
|
2479
|
+
|
|
2480
|
+
Sources with CBDT/CBLC tables (e.g. NotoColorEmoji) can be used as
|
|
2481
|
+
Stitcher sources. The bitmap data is propagated byte-for-byte from the
|
|
2482
|
+
source into the output. The Stitcher automatically detects the source's
|
|
2483
|
+
storage mode via `bitmap_mode`:
|
|
2484
|
+
|
|
2485
|
+
[source,ruby]
|
|
2486
|
+
----
|
|
2487
|
+
stitcher = Fontisan::Stitcher.new
|
|
2488
|
+
stitcher.add_source(:text, Fontisan::FontLoader.load("NotoSans.ttf"))
|
|
2489
|
+
stitcher.add_source(:emoji, Fontisan::FontLoader.load("NotoColorEmoji.ttf"))
|
|
2490
|
+
|
|
2491
|
+
# All emoji glyphs from the CBDT source are added; the CBLC table
|
|
2492
|
+
# is copied byte-for-byte and references the right GIDs.
|
|
2493
|
+
stitcher.include_range(0x41..0x5A, from: :text)
|
|
2494
|
+
stitcher.include_range(0x1F600..0x1F64F, from: :emoji)
|
|
2495
|
+
|
|
2496
|
+
stitcher.write_to("out.ttf", format: :ttf)
|
|
2497
|
+
# → out.ttf has glyf + CBDT + CBLC, emoji renders correctly
|
|
2498
|
+
----
|
|
2499
|
+
|
|
2500
|
+
**Constraints:**
|
|
2501
|
+
- Only ONE CBDT source per Stitcher (multiple raise
|
|
2502
|
+
`Fontisan::MultipleCbdtSourcesError`)
|
|
2503
|
+
- The CBDT source is processed first (GIDs 0..N-1) so the CBLC's GID
|
|
2504
|
+
references remain valid in the output
|
|
2505
|
+
- Multi-source CBDT merge is tracked as TODO (REVIEW
|
|
2506
|
+
`TODO.full/ufo/23-cbdt-passthrough.md`)
|
|
2507
|
+
|
|
2478
2508
|
|
|
2479
2509
|
== Testing
|
|
2480
2510
|
|
|
@@ -6,10 +6,10 @@ module Fontisan
|
|
|
6
6
|
# extraction API used by the selectors.
|
|
7
7
|
#
|
|
8
8
|
# For UFO sources, glyphs are accessed by name directly. For TTF
|
|
9
|
-
# or OTF sources,
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# or OTF sources, individual glyphs are extracted on demand from
|
|
10
|
+
# the BinData tables (glyf/loca/head for TTF, CFF for OTF). This
|
|
11
|
+
# is O(1) per glyph rather than the previous O(n) full-donor
|
|
12
|
+
# conversion.
|
|
13
13
|
#
|
|
14
14
|
# CBDT/CBLC sources (e.g. NotoColorEmoji) are detected via
|
|
15
15
|
# #bitmap_mode. When a source is :cbdt, the Stitcher propagates
|
|
@@ -20,7 +20,7 @@ module Fontisan
|
|
|
20
20
|
|
|
21
21
|
def initialize(font)
|
|
22
22
|
@font = font
|
|
23
|
-
@
|
|
23
|
+
@bin_data_cache = nil
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# @return [Symbol] :ufo, :ttf, :otf
|
|
@@ -66,14 +66,19 @@ module Fontisan
|
|
|
66
66
|
|
|
67
67
|
# Extract a glyph by gid.
|
|
68
68
|
#
|
|
69
|
+
# For TTF/OTF sources, this is O(1) per glyph: it parses just
|
|
70
|
+
# the requested glyph from glyf/CFF on demand, not the entire
|
|
71
|
+
# donor. The full-donor conversion is avoided entirely.
|
|
72
|
+
#
|
|
69
73
|
# For CBDT sources, returns a placeholder glyph (no contours)
|
|
70
|
-
# since the glyph data
|
|
74
|
+
# since the glyph data is in the bitmap tables, not outlines.
|
|
75
|
+
#
|
|
71
76
|
# @param gid [Integer]
|
|
72
77
|
# @return [Fontisan::Ufo::Glyph, nil]
|
|
73
78
|
def glyph_for_gid(gid)
|
|
74
79
|
case @font
|
|
75
80
|
when Fontisan::Ufo::Font then ufo_glyph_at(gid)
|
|
76
|
-
else
|
|
81
|
+
else extract_single_glyph_from_bindata(gid)
|
|
77
82
|
end
|
|
78
83
|
end
|
|
79
84
|
|
|
@@ -89,6 +94,15 @@ module Fontisan
|
|
|
89
94
|
nil
|
|
90
95
|
end
|
|
91
96
|
|
|
97
|
+
# Width of a specific glyph (extracted from hmtx).
|
|
98
|
+
# Falls back to 0 if hmtx is missing.
|
|
99
|
+
# @param gid [Integer]
|
|
100
|
+
# @return [Integer]
|
|
101
|
+
def glyph_width(gid)
|
|
102
|
+
widths = bin_data_widths
|
|
103
|
+
widths[gid] || 0
|
|
104
|
+
end
|
|
105
|
+
|
|
92
106
|
private
|
|
93
107
|
|
|
94
108
|
# ---------- UFO source ----------
|
|
@@ -108,7 +122,7 @@ module Fontisan
|
|
|
108
122
|
@font.glyph(name)
|
|
109
123
|
end
|
|
110
124
|
|
|
111
|
-
# ---------- TTF/OTF source ----------
|
|
125
|
+
# ---------- TTF/OTF source: O(1) per-glyph extraction ----------
|
|
112
126
|
|
|
113
127
|
def bin_data_gid_for(codepoint)
|
|
114
128
|
cmap = @font.table("cmap")
|
|
@@ -117,22 +131,137 @@ module Fontisan
|
|
|
117
131
|
cmap.unicode_mappings[codepoint]
|
|
118
132
|
end
|
|
119
133
|
|
|
120
|
-
# Lazily
|
|
121
|
-
#
|
|
122
|
-
def
|
|
123
|
-
|
|
134
|
+
# Lazily parse the relevant BinData tables. Cached so we only
|
|
135
|
+
# pay the parse cost once per source.
|
|
136
|
+
def bin_data_cache
|
|
137
|
+
@bin_data_cache ||= parse_bin_data_tables
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_bin_data_tables
|
|
141
|
+
cache = { head: @font.table("head") }
|
|
142
|
+
|
|
143
|
+
if @font.has_table?("glyf")
|
|
144
|
+
cache[:loca] = @font.table("loca")
|
|
145
|
+
cache[:glyf] = @font.table("glyf")
|
|
146
|
+
# loca needs head's index_to_loc_format to size its offsets
|
|
147
|
+
if cache[:loca].respond_to?(:parse_with_context) && cache[:head]
|
|
148
|
+
cache[:loca].parse_with_context(
|
|
149
|
+
cache[:head].index_to_loc_format,
|
|
150
|
+
@font.table("maxp")&.num_glyphs || 0,
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
cache
|
|
156
|
+
end
|
|
124
157
|
|
|
125
|
-
|
|
158
|
+
# Build {gid → advance_width} from hmtx (cached).
|
|
159
|
+
def bin_data_widths
|
|
160
|
+
@bin_data_widths ||= build_bin_data_widths
|
|
126
161
|
end
|
|
127
162
|
|
|
128
|
-
def
|
|
163
|
+
def build_bin_data_widths
|
|
164
|
+
widths = {}
|
|
165
|
+
hmtx = @font.table("hmtx")
|
|
166
|
+
return widths unless hmtx
|
|
167
|
+
|
|
168
|
+
hhea = @font.table("hhea")
|
|
169
|
+
maxp = @font.table("maxp")
|
|
170
|
+
num_h_metrics = hhea&.number_of_h_metrics || 1
|
|
171
|
+
num_glyphs = maxp&.num_glyphs || 0
|
|
172
|
+
|
|
173
|
+
if hmtx.respond_to?(:parse_with_context)
|
|
174
|
+
hmtx.parse_with_context(num_h_metrics, num_glyphs)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
num_glyphs.times do |gid|
|
|
178
|
+
metric = hmtx.respond_to?(:metric_for) ? hmtx.metric_for(gid) : nil
|
|
179
|
+
widths[gid] = metric ? metric[:advance_width] : 0
|
|
180
|
+
rescue StandardError
|
|
181
|
+
widths[gid] = 0
|
|
182
|
+
end
|
|
183
|
+
widths
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Extract a single glyph by gid, parsing just the relevant bytes.
|
|
187
|
+
# O(1) per call (after the first call's table-parsing overhead).
|
|
188
|
+
#
|
|
189
|
+
# For TTF (glyf) sources: reads one glyph from glyf/loca.
|
|
190
|
+
# For OTF (CFF) sources: falls back to the full-donor conversion
|
|
191
|
+
# BUT uses a proper gid→name map (not array index) to avoid the
|
|
192
|
+
# gid-misalignment bug that dropped Plane 1 codepoints.
|
|
193
|
+
def extract_single_glyph_from_bindata(gid)
|
|
194
|
+
cache = bin_data_cache
|
|
195
|
+
|
|
196
|
+
if cache[:glyf] && cache[:loca] && cache[:head]
|
|
197
|
+
extract_truetype_glyph(gid, cache)
|
|
198
|
+
elsif @font.has_table?("CFF ")
|
|
199
|
+
extract_cff_glyph_safe(gid)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# CFF glyph extraction: uses the full UFO conversion. After the
|
|
204
|
+
# fix in FromBinData (no more `next unless simple`), every gid
|
|
205
|
+
# gets a glyph, so array index = gid.
|
|
206
|
+
def extract_cff_glyph_safe(gid)
|
|
129
207
|
ufo = converted_ufo
|
|
130
208
|
names = ufo.glyphs.keys
|
|
131
|
-
|
|
132
|
-
return nil unless name
|
|
209
|
+
return nil if gid >= names.size
|
|
133
210
|
|
|
211
|
+
name = names[gid]
|
|
134
212
|
ufo.glyph(name)
|
|
135
213
|
end
|
|
214
|
+
|
|
215
|
+
# Lazily convert the loaded TTF/OTF to a UFO::Font (for CFF
|
|
216
|
+
# sources and as a fallback).
|
|
217
|
+
def converted_ufo
|
|
218
|
+
@converted_ufo ||= Fontisan::Ufo::Convert::FromBinData.convert(@font)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def extract_truetype_glyph(gid, cache)
|
|
222
|
+
simple = cache[:glyf].glyph_for(gid, cache[:loca], cache[:head])
|
|
223
|
+
return nil unless simple
|
|
224
|
+
return nil unless simple.respond_to?(:simple?) && simple.simple?
|
|
225
|
+
|
|
226
|
+
name = gid.zero? ? ".notdef" : "gid#{gid}"
|
|
227
|
+
glyph = Fontisan::Ufo::Glyph.new(name: name)
|
|
228
|
+
glyph.width = glyph_width(gid)
|
|
229
|
+
copy_simple_contours(simple, glyph)
|
|
230
|
+
add_cmap_unicodes(gid, glyph)
|
|
231
|
+
glyph
|
|
232
|
+
rescue StandardError
|
|
233
|
+
nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Copy a SimpleGlyph's contours + points into a Ufo::Glyph.
|
|
237
|
+
def copy_simple_contours(simple, ufo_glyph)
|
|
238
|
+
num_contours = simple.end_pts_of_contours&.size || 0
|
|
239
|
+
return if num_contours.zero?
|
|
240
|
+
|
|
241
|
+
num_contours.times do |ci|
|
|
242
|
+
points = simple.points_for_contour(ci)
|
|
243
|
+
next unless points && !points.empty?
|
|
244
|
+
|
|
245
|
+
ufo_points = points.map do |pt|
|
|
246
|
+
x = pt[:x] || pt["x"]
|
|
247
|
+
y = pt[:y] || pt["y"]
|
|
248
|
+
on_curve = pt[:on_curve].nil? || pt[:on_curve]
|
|
249
|
+
type = on_curve ? "line" : "offcurve"
|
|
250
|
+
Fontisan::Ufo::Point.new(x: x.to_f, y: y.to_f, type: type)
|
|
251
|
+
end
|
|
252
|
+
ufo_glyph.add_contour(Fontisan::Ufo::Contour.new(ufo_points))
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Add Unicode codepoints from the cmap that map to this gid.
|
|
257
|
+
def add_cmap_unicodes(gid, glyph)
|
|
258
|
+
cmap = @font.table("cmap")
|
|
259
|
+
return unless cmap
|
|
260
|
+
|
|
261
|
+
(cmap.unicode_mappings || {}).each do |cp, g|
|
|
262
|
+
glyph.add_unicode(cp) if g == gid
|
|
263
|
+
end
|
|
264
|
+
end
|
|
136
265
|
end
|
|
137
266
|
end
|
|
138
267
|
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Ufo
|
|
7
|
+
module Compile
|
|
8
|
+
# Builds the OpenType `GPOS` (Glyph Positioning) table from UFO
|
|
9
|
+
# kerning data. Emits a minimal but valid GPOS with:
|
|
10
|
+
#
|
|
11
|
+
# - ScriptList: DFLT script, default language system
|
|
12
|
+
# - FeatureList: `kern` feature (feature tag "kern")
|
|
13
|
+
# - LookupList: one PairPos lookup (format 1, individual pairs)
|
|
14
|
+
#
|
|
15
|
+
# Each kerning pair from the UFO source becomes a PairPosRecord
|
|
16
|
+
# with an x-advance adjustment on the first glyph.
|
|
17
|
+
#
|
|
18
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/gpos
|
|
19
|
+
module Gpos
|
|
20
|
+
FEATURE_KERN = "kern"
|
|
21
|
+
SCRIPT_DFLT = "DFLT"
|
|
22
|
+
LANGSYS_DEFAULT = 0
|
|
23
|
+
|
|
24
|
+
# ValueFormat flags (which fields are present in a ValueRecord)
|
|
25
|
+
VALUE_X_PLACEMENT = 0x0001
|
|
26
|
+
VALUE_Y_PLACEMENT = 0x0002
|
|
27
|
+
VALUE_X_ADVANCE = 0x0004
|
|
28
|
+
VALUE_Y_ADVANCE = 0x0008
|
|
29
|
+
|
|
30
|
+
# @param font [Fontisan::Ufo::Font]
|
|
31
|
+
# @param glyphs [Array<Fontisan::Ufo::Glyph>] in gid order
|
|
32
|
+
# @return [String, nil] GPOS table bytes, or nil if no kerning
|
|
33
|
+
def self.build(font, glyphs:)
|
|
34
|
+
pairs = collect_kerning_pairs(font, glyphs)
|
|
35
|
+
return nil if pairs.empty?
|
|
36
|
+
|
|
37
|
+
build_gpos_table(pairs)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# ---------- pair collection ----------
|
|
41
|
+
|
|
42
|
+
# Collect kerning pairs from the UFO model. Each pair is
|
|
43
|
+
# (gid1, gid2, x_advance_delta).
|
|
44
|
+
# @return [Array<[Integer, Integer, Integer]>]
|
|
45
|
+
def self.collect_kerning_pairs(font, glyphs)
|
|
46
|
+
name_to_gid = {}
|
|
47
|
+
glyphs.each_with_index { |g, gid| name_to_gid[g.name] = gid }
|
|
48
|
+
|
|
49
|
+
pairs = []
|
|
50
|
+
font.kerning.each_pair do |key, value|
|
|
51
|
+
# UFO kerning key is "glyph1 glyph2" or a class name.
|
|
52
|
+
# We only handle individual glyph pairs (not classes).
|
|
53
|
+
names = key.split
|
|
54
|
+
next unless names.size == 2
|
|
55
|
+
|
|
56
|
+
gid1 = name_to_gid[names[0]]
|
|
57
|
+
gid2 = name_to_gid[names[1]]
|
|
58
|
+
next unless gid1 && gid2
|
|
59
|
+
|
|
60
|
+
pairs << [gid1, gid2, value.to_i]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
pairs.sort_by { |a| [a[0], a[1]] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# ---------- GPOS binary assembly ----------
|
|
67
|
+
|
|
68
|
+
def self.build_gpos_table(pairs)
|
|
69
|
+
# Group pairs by first glyph (gid1)
|
|
70
|
+
by_first = {}
|
|
71
|
+
pairs.each do |gid1, gid2, delta|
|
|
72
|
+
by_first[gid1] ||= []
|
|
73
|
+
by_first[gid1] << [gid2, delta]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
first_gids = by_first.keys.sort
|
|
77
|
+
|
|
78
|
+
# --- Build subtables bottom-up ---
|
|
79
|
+
|
|
80
|
+
# 1. PairSets (one per first glyph)
|
|
81
|
+
pair_sets_data = {}
|
|
82
|
+
pair_set_offsets = {}
|
|
83
|
+
pair_sets_blob = +""
|
|
84
|
+
|
|
85
|
+
first_gids.each do |gid1|
|
|
86
|
+
pair_set_offsets[gid1] = pair_sets_blob.bytesize
|
|
87
|
+
|
|
88
|
+
second_pairs = by_first[gid1].sort_by { |gid2, _| gid2 }
|
|
89
|
+
data = [second_pairs.size].pack("n") # pairValueCount
|
|
90
|
+
second_pairs.each do |gid2, delta|
|
|
91
|
+
data << [gid2, delta, 0].pack("nnn") # secondGlyph + valRec1(xAdvance) + valRec2(0)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
pair_sets_data[gid1] = data
|
|
95
|
+
pair_sets_blob << data
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 2. Coverage table (Format 1: individual glyphs)
|
|
99
|
+
coverage = build_coverage_format1(first_gids)
|
|
100
|
+
|
|
101
|
+
# 3. PairPosFormat1 subtable
|
|
102
|
+
value_format1 = VALUE_X_ADVANCE
|
|
103
|
+
value_format2 = 0
|
|
104
|
+
|
|
105
|
+
pairpos_header_size = 10 # format(2) + coverageOffset(2) + valueFormat1(2) + valueFormat2(2) + pairSetCount(2)
|
|
106
|
+
pairset_array_size = first_gids.size * 2 # one uint16 offset per first glyph
|
|
107
|
+
|
|
108
|
+
coverage_offset = pairpos_header_size + pairset_array_size
|
|
109
|
+
pairset_base = coverage_offset + coverage.bytesize
|
|
110
|
+
|
|
111
|
+
pairpos = +""
|
|
112
|
+
pairpos << [1].pack("n") # posFormat = 1
|
|
113
|
+
pairpos << [coverage_offset].pack("n") # coverageOffset
|
|
114
|
+
pairpos << [value_format1].pack("n") # valueFormat1
|
|
115
|
+
pairpos << [value_format2].pack("n") # valueFormat2
|
|
116
|
+
pairpos << [first_gids.size].pack("n") # pairSetCount
|
|
117
|
+
|
|
118
|
+
# PairSet offsets (relative to start of PairPos subtable)
|
|
119
|
+
first_gids.each do |gid1|
|
|
120
|
+
pairpos << [pairset_base + pair_set_offsets[gid1]].pack("n")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
pairpos << coverage
|
|
124
|
+
pairpos << pair_sets_blob
|
|
125
|
+
|
|
126
|
+
# 4. Lookup table (type 2 = PairPos, flag 0)
|
|
127
|
+
lookup_header = [
|
|
128
|
+
2, # lookupType = PairPos
|
|
129
|
+
0, # lookupFlag
|
|
130
|
+
1, # subTableCount
|
|
131
|
+
].pack("nnn")
|
|
132
|
+
|
|
133
|
+
subtable_offset = lookup_header.bytesize + 2 # +2 for the offset array
|
|
134
|
+
lookup = lookup_header + [subtable_offset].pack("n") + pairpos
|
|
135
|
+
|
|
136
|
+
# 5. Assemble GPOS header + ScriptList + FeatureList + LookupList
|
|
137
|
+
|
|
138
|
+
# ScriptList (minimal: DFLT script, default LangSys)
|
|
139
|
+
script_list = build_script_list
|
|
140
|
+
|
|
141
|
+
# FeatureList (minimal: kern feature)
|
|
142
|
+
feature_list = build_feature_list
|
|
143
|
+
|
|
144
|
+
# LookupList (minimal: one lookup)
|
|
145
|
+
lookup_list_header = [1].pack("n") # lookupCount
|
|
146
|
+
lookup_offset_in_list = lookup_list_header.bytesize + 2 # +2 for the offset
|
|
147
|
+
lookup_list = lookup_list_header + [lookup_offset_in_list].pack("n") + lookup
|
|
148
|
+
|
|
149
|
+
# GPOS header (version 1.0)
|
|
150
|
+
header_size = 10 # version(4) + scriptListOffset(2) + featureListOffset(2) + lookupListOffset(2)
|
|
151
|
+
script_list_offset = header_size
|
|
152
|
+
feature_list_offset = script_list_offset + script_list.bytesize
|
|
153
|
+
lookup_list_offset = feature_list_offset + feature_list.bytesize
|
|
154
|
+
|
|
155
|
+
header = [
|
|
156
|
+
0x00010000, # version 1.0
|
|
157
|
+
script_list_offset,
|
|
158
|
+
feature_list_offset,
|
|
159
|
+
lookup_list_offset,
|
|
160
|
+
].pack("Nnnn")
|
|
161
|
+
|
|
162
|
+
header + script_list + feature_list + lookup_list
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Coverage Format 1: list of individual glyph IDs.
|
|
166
|
+
def self.build_coverage_format1(gids)
|
|
167
|
+
[1, gids.size].pack("nn") + gids.pack("n*")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# ScriptList: DFLT script with a single default LangSys.
|
|
171
|
+
# The LangSys references feature index 0 (kern).
|
|
172
|
+
def self.build_script_list
|
|
173
|
+
# ScriptList header: scriptCount(2)
|
|
174
|
+
# ScriptRecord: scriptTag(4) + scriptOffset(2)
|
|
175
|
+
# Script table: defaultLangSysOffset(2) + langSysCount(2) = 0
|
|
176
|
+
# LangSys: lookupOrder(2)=0 + reqFeatureIndex(2)=0xFFFF + featureIndexCount(2)=1 + featureIndex(2)=0
|
|
177
|
+
|
|
178
|
+
script_list_header_size = 2 + (4 + 2) # scriptCount + 1 ScriptRecord
|
|
179
|
+
script_offset = script_list_header_size
|
|
180
|
+
langsys_offset = script_offset + 4 # script table size (defaultLangSysOffset + langSysCount)
|
|
181
|
+
|
|
182
|
+
script_list = +""
|
|
183
|
+
script_list << [1].pack("n") # scriptCount
|
|
184
|
+
script_list << SCRIPT_DFLT # scriptTag
|
|
185
|
+
script_list << [script_offset].pack("n") # scriptOffset
|
|
186
|
+
|
|
187
|
+
# Script table
|
|
188
|
+
script_list << [langsys_offset - script_offset].pack("n") # defaultLangSysOffset (relative)
|
|
189
|
+
script_list << [0].pack("n") # langSysCount
|
|
190
|
+
|
|
191
|
+
# Default LangSys
|
|
192
|
+
script_list << [0].pack("n") # lookupOrder (reserved)
|
|
193
|
+
script_list << [0xFFFF].pack("n") # reqFeatureIndex (none)
|
|
194
|
+
script_list << [1].pack("n") # featureIndexCount
|
|
195
|
+
script_list << [0].pack("n") # featureIndex[0] = kern
|
|
196
|
+
|
|
197
|
+
script_list
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# FeatureList: one feature record (kern) referencing lookup 0.
|
|
201
|
+
def self.build_feature_list
|
|
202
|
+
# FeatureList header: featureCount(2)
|
|
203
|
+
# FeatureRecord: featureTag(4) + featureOffset(2)
|
|
204
|
+
# Feature table: featureParams(2)=0 + lookupIndexCount(2)=1 + lookupIndex(2)=0
|
|
205
|
+
|
|
206
|
+
feature_list_header_size = 2 + (4 + 2) # featureCount + 1 FeatureRecord
|
|
207
|
+
feature_offset = feature_list_header_size
|
|
208
|
+
|
|
209
|
+
feature_list = +""
|
|
210
|
+
feature_list << [1].pack("n") # featureCount
|
|
211
|
+
feature_list << FEATURE_KERN # featureTag
|
|
212
|
+
feature_list << [feature_offset].pack("n") # featureOffset
|
|
213
|
+
|
|
214
|
+
# Feature table
|
|
215
|
+
feature_list << [0].pack("n") # featureParams (null)
|
|
216
|
+
feature_list << [1].pack("n") # lookupIndexCount
|
|
217
|
+
feature_list << [0].pack("n") # lookupIndex[0]
|
|
218
|
+
|
|
219
|
+
feature_list
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private_class_method :build_gpos_table, :build_coverage_format1,
|
|
223
|
+
:build_script_list, :build_feature_list
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
data/lib/fontisan/ufo/compile.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Fontisan
|
|
|
33
33
|
autoload :TtfCompiler, "fontisan/ufo/compile/ttf_compiler"
|
|
34
34
|
autoload :OtfCompiler, "fontisan/ufo/compile/otf_compiler"
|
|
35
35
|
autoload :Filters, "fontisan/ufo/compile/filters"
|
|
36
|
+
autoload :Gpos, "fontisan/ufo/compile/gpos"
|
|
36
37
|
autoload :Head, "fontisan/ufo/compile/head"
|
|
37
38
|
autoload :Hhea, "fontisan/ufo/compile/hhea"
|
|
38
39
|
autoload :Maxp, "fontisan/ufo/compile/maxp"
|
|
@@ -179,12 +179,15 @@ module Fontisan
|
|
|
179
179
|
rescue StandardError
|
|
180
180
|
nil
|
|
181
181
|
end
|
|
182
|
-
next unless simple
|
|
183
182
|
|
|
184
183
|
if simple.is_a?(Fontisan::Tables::SimpleGlyph)
|
|
185
184
|
extract_simple_contours(simple, ufo_glyph)
|
|
186
185
|
end
|
|
187
186
|
|
|
187
|
+
# Always add the glyph, even if it has no contours. This
|
|
188
|
+
# ensures gid → array index alignment (no gaps from skipped
|
|
189
|
+
# glyphs). Without this, high-gid Plane 1 codepoints are
|
|
190
|
+
# silently dropped because the array is shorter than maxp.
|
|
188
191
|
ufo.layers.default_layer.add(ufo_glyph)
|
|
189
192
|
end
|
|
190
193
|
end
|
data/lib/fontisan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -564,6 +564,7 @@ files:
|
|
|
564
564
|
- lib/fontisan/ufo/compile/filters/flatten_components.rb
|
|
565
565
|
- lib/fontisan/ufo/compile/filters/reverse_contour_direction.rb
|
|
566
566
|
- lib/fontisan/ufo/compile/glyf_loca.rb
|
|
567
|
+
- lib/fontisan/ufo/compile/gpos.rb
|
|
567
568
|
- lib/fontisan/ufo/compile/head.rb
|
|
568
569
|
- lib/fontisan/ufo/compile/hhea.rb
|
|
569
570
|
- lib/fontisan/ufo/compile/hmtx.rb
|