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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58c1e2768684af1fcdc55fdc2d5b3e47bbd89b0277794dc64ff627ef8d571c3d
4
- data.tar.gz: 8d1da959f850b3872acb01e96deb098ea2cbb0d596a0923b5db4960edfd818bf
3
+ metadata.gz: 14c31b390c5721b221f5c04239baf1d2330b668d142addd3e321b3bd868f7b7a
4
+ data.tar.gz: 726fe81623ef7f410aa09e3b7a7838b8e0df1c2304cf9b36d1ad0727bdcf03e8
5
5
  SHA512:
6
- metadata.gz: 3e46507d14358098c09f5ee2a3584c601e3d28e6a762f506962007445fb9fcd4432db7f591601575fac87d9cad93ae7c57d2c464728888e33dd6b242387c5611
7
- data.tar.gz: 1e2dc941fe06dc9769aad1477c970ac48e799d62f3340a08448c0528290562ce2f9ccfd0e021615ebc57a26bcdd19773fd51135d4d214ef31e2a927576c7d06f
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.1"
4
+ VERSION = "0.4.2"
5
5
  end
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.1
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