fontisan 0.4.1 → 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/lib/fontisan/stitcher/source.rb +25 -0
- 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
|
|
@@ -185,14 +185,39 @@ module Fontisan
|
|
|
185
185
|
|
|
186
186
|
# Extract a single glyph by gid, parsing just the relevant bytes.
|
|
187
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.
|
|
188
193
|
def extract_single_glyph_from_bindata(gid)
|
|
189
194
|
cache = bin_data_cache
|
|
190
195
|
|
|
191
196
|
if cache[:glyf] && cache[:loca] && cache[:head]
|
|
192
197
|
extract_truetype_glyph(gid, cache)
|
|
198
|
+
elsif @font.has_table?("CFF ")
|
|
199
|
+
extract_cff_glyph_safe(gid)
|
|
193
200
|
end
|
|
194
201
|
end
|
|
195
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)
|
|
207
|
+
ufo = converted_ufo
|
|
208
|
+
names = ufo.glyphs.keys
|
|
209
|
+
return nil if gid >= names.size
|
|
210
|
+
|
|
211
|
+
name = names[gid]
|
|
212
|
+
ufo.glyph(name)
|
|
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
|
+
|
|
196
221
|
def extract_truetype_glyph(gid, cache)
|
|
197
222
|
simple = cache[:glyf].glyph_for(gid, cache[:loca], cache[:head])
|
|
198
223
|
return nil unless simple
|
|
@@ -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
|