fontisan 0.3.1 → 0.4.0

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: '0979321b74749068da214fbb4880701c1f78905cce2db2aa7a8a2263da669af3'
4
- data.tar.gz: 3ccbb49464b62e831475fe601fecc9b7085c6dda5a16ec66d9b9c56c14ff0742
3
+ metadata.gz: '020862878eef5569b82edb88989afdc3b7c21d3f01d19b605132af70cabe524f'
4
+ data.tar.gz: d8d685a687c94f987835e440619aca344a615368ea78d6caf6a882c34c2900d3
5
5
  SHA512:
6
- metadata.gz: 87538383c69009f3610778a717ae6263accb950e07b8803914358927bfc1e7510c18c664d88267b17dcabbeaab4fa97d4ac72b3294ae492472d0659843dbe540
7
- data.tar.gz: 459343281dd3b295141eed4c8091ccc3d9ce6228d49c293eb6a182d26676428eaf4cb571295b8f55ac4101ba88d879c7525b06f5ad1596a83ea544d2f88a8351
6
+ metadata.gz: a98055b753907dedfd44fd89c61fd3d5d797d405d702e65730bae1e3299665354f385b135529ca6b48708672e37cfa0e10a7b518ce0b624ccb46fae9459cfb22
7
+ data.tar.gz: 46af4e780f4a1786501e2b874d4b439614c212b839b0b398316806a2baa9348b67c8e4db3e63b85142d79ae015df3f093db957f055ddbd3f4955268caa2c1dfa
@@ -199,6 +199,13 @@ module Fontisan
199
199
  end
200
200
  end
201
201
 
202
+ # Multiple CBDT sources detected in Stitcher
203
+ #
204
+ # Raised when more than one CBDT/CBLC source is registered with the
205
+ # Stitcher. Only single-source CBDT passthrough is supported; merging
206
+ # CBDT/CBLC across multiple sources requires a dedicated rebuild.
207
+ class MultipleCbdtSourcesError < Error; end
208
+
202
209
  # Variation data corrupted (for use in data_extractor)
203
210
  #
204
211
  # Raised when extracted variation data appears corrupted.
@@ -10,6 +10,11 @@ module Fontisan
10
10
  # via Ufo::Convert::FromBinData on first glyph access, then cached.
11
11
  # This is O(n) in donor glyph count but amortized across all
12
12
  # codepoint extractions from that donor.
13
+ #
14
+ # CBDT/CBLC sources (e.g. NotoColorEmoji) are detected via
15
+ # #bitmap_mode. When a source is :cbdt, the Stitcher propagates
16
+ # the raw CBDT/CBLC tables into the output instead of extracting
17
+ # outlines. The glyph data lives in the bitmap tables, not in glyf.
13
18
  class Source
14
19
  attr_reader :font
15
20
 
@@ -28,6 +33,27 @@ module Fontisan
28
33
  end
29
34
  end
30
35
 
36
+ # Detect how this source stores glyph data.
37
+ #
38
+ # - :glyf — TrueType outlines (glyf table present)
39
+ # - :cbdt — Color bitmaps (CBDT + CBLC tables, no glyf)
40
+ # - :mixed — Both glyf and CBDT
41
+ # - :none — UFO source or neither table present
42
+ #
43
+ # @return [Symbol]
44
+ def bitmap_mode
45
+ return :none if @font.is_a?(Fontisan::Ufo::Font)
46
+ return :none unless @font.respond_to?(:has_table?)
47
+
48
+ has_cbdt = @font.has_table?("CBDT") && @font.has_table?("CBLC")
49
+ has_glyf = @font.has_table?("glyf") || @font.has_table?("CFF ")
50
+ return :mixed if has_cbdt && has_glyf
51
+ return :cbdt if has_cbdt
52
+ return :glyf if has_glyf
53
+
54
+ :none
55
+ end
56
+
31
57
  # Find the gid for a Unicode codepoint in this source.
32
58
  # @param codepoint [Integer]
33
59
  # @return [Integer, nil]
@@ -39,6 +65,9 @@ module Fontisan
39
65
  end
40
66
 
41
67
  # Extract a glyph by gid.
68
+ #
69
+ # For CBDT sources, returns a placeholder glyph (no contours)
70
+ # since the glyph data lives in the bitmap tables, not outlines.
42
71
  # @param gid [Integer]
43
72
  # @return [Fontisan::Ufo::Glyph, nil]
44
73
  def glyph_for_gid(gid)
@@ -48,6 +77,18 @@ module Fontisan
48
77
  end
49
78
  end
50
79
 
80
+ # Raw table bytes from the loaded font (for passthrough).
81
+ # @param tag [String] 4-byte table tag (e.g. "CBDT", "CBLC")
82
+ # @return [String, nil] raw bytes or nil if table not present
83
+ def raw_table_bytes(tag)
84
+ sfnt_table = @font.table(tag)
85
+ return nil unless sfnt_table
86
+
87
+ sfnt_table.raw_data
88
+ rescue StandardError
89
+ nil
90
+ end
91
+
51
92
  private
52
93
 
53
94
  # ---------- UFO source ----------
@@ -70,12 +70,23 @@ module Fontisan
70
70
  end
71
71
 
72
72
  # Write the stitched font to disk.
73
+ #
74
+ # For CBDT/CBLC sources (e.g. NotoColorEmoji), the raw CBDT and
75
+ # CBLC tables are copied byte-for-byte into the output. This works
76
+ # because CBDT-mode glyphs are processed first (GIDs 0..N-1),
77
+ # matching the source's GID layout. Only one CBDT source is
78
+ # supported; multiple CBDT sources raise MultipleCbdtSourcesError.
79
+ #
73
80
  # @param path [String] output file path
74
81
  # @param format [Symbol] :ttf or :otf
75
82
  def write_to(path, format: :ttf)
76
83
  build_target_font
77
84
  compiler = compiler_for(format)
78
- compiler.new(@target).compile(output_path: path)
85
+ compiler_instance = compiler.new(@target)
86
+ compiler_instance.compile(output_path: path)
87
+
88
+ propagate_cbdt_tables(path) if cbdt_source
89
+
79
90
  path
80
91
  end
81
92
 
@@ -96,6 +107,53 @@ module Fontisan
96
107
  end
97
108
  end
98
109
 
110
+ # Find the single CBDT source among registered sources, if any.
111
+ # Raises if more than one CBDT source is present (merge not supported).
112
+ # @return [Source, nil]
113
+ def cbdt_source
114
+ cbdts = @sources.values.select { |s| s.bitmap_mode == :cbdt }
115
+ if cbdts.size > 1
116
+ raise MultipleCbdtSourcesError,
117
+ "multiple CBDT sources not supported (found #{cbdts.size})"
118
+ end
119
+
120
+ cbdts.first
121
+ end
122
+
123
+ # Copy raw CBDT + CBLC table bytes from the CBDT source into the
124
+ # compiled output file. The GIDs must match (CBDT glyphs are at
125
+ # the same GIDs in both source and output because they were added
126
+ # first during build_target_font).
127
+ def propagate_cbdt_tables(path)
128
+ source = cbdt_source
129
+ return unless source
130
+
131
+ compiled = Fontisan::FontLoader.load(path)
132
+
133
+ tables = {}
134
+ compiled.table_names.each do |tag|
135
+ raw = extract_raw_table(compiled, tag)
136
+ tables[tag] = raw if raw
137
+ end
138
+
139
+ cbdt_bytes = source.raw_table_bytes("CBDT")
140
+ cblc_bytes = source.raw_table_bytes("CBLC")
141
+ tables["CBDT"] = cbdt_bytes if cbdt_bytes
142
+ tables["CBLC"] = cblc_bytes if cblc_bytes
143
+
144
+ sfnt = tables.key?("CFF ") ? 0x4F54544F : 0x00010000
145
+ Fontisan::FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
146
+ end
147
+
148
+ def extract_raw_table(font, tag)
149
+ sfnt_table = font.table(tag)
150
+ return nil unless sfnt_table
151
+
152
+ sfnt_table.raw_data
153
+ rescue StandardError
154
+ nil
155
+ end
156
+
99
157
  def compiler_for(format)
100
158
  case format.to_sym
101
159
  when :ttf then Ufo::Compile::TtfCompiler
@@ -107,26 +165,28 @@ module Fontisan
107
165
 
108
166
  # Walk bindings in codepoint order, assign sequential new gids,
109
167
  # copy each glyph into the target font's default layer.
168
+ #
169
+ # When a CBDT source is present, its glyphs are added FIRST (in
170
+ # source GID order) so that the CBLC's GID references remain valid.
171
+ # Glyf-source bindings are processed AFTER, appending new glyphs.
110
172
  def assign_gids_and_copy_glyphs
111
- # Always put .notdef at gid 0 first.
112
- notdef_binding = @bindings.find { |b| b[:donor_gid].zero? }
113
- if notdef_binding
114
- copy_glyph_into(@target, name: ".notdef",
115
- source: notdef_binding[:source],
116
- donor_gid: 0)
173
+ cbdt = cbdt_source
174
+
175
+ if cbdt
176
+ add_all_cbdt_glyphs(cbdt)
117
177
  else
118
- # Synthesize an empty .notdef
119
- @target.layers.default_layer.add(Ufo::Glyph.new(name: ".notdef"))
178
+ add_notdef_from_bindings
120
179
  end
121
180
 
122
181
  sorted_bindings.each do |binding|
123
- next if binding[:donor_gid].zero? # already handled
182
+ next if binding[:donor_gid].zero?
183
+
184
+ # Skip bindings from the CBDT source — its glyphs are already added.
185
+ next if cbdt && binding[:source].equal?(cbdt)
124
186
 
125
187
  glyph = binding[:source].glyph_for_gid(binding[:donor_gid])
126
188
  next unless glyph
127
189
 
128
- # If multiple codepoints map to the same glyph, only the first
129
- # binding creates the glyph; subsequent ones add unicode entries.
130
190
  if @target.glyphs.key?(glyph.name)
131
191
  add_extra_unicode(glyph.name, binding[:codepoint])
132
192
  else
@@ -159,6 +219,51 @@ module Fontisan
159
219
  glyph.add_unicode(codepoint) unless glyph.unicodes.include?(codepoint)
160
220
  end
161
221
 
222
+ # Add .notdef at GID 0 from the first binding that references gid 0.
223
+ # Falls back to a synthesized empty .notdef if none found.
224
+ def add_notdef_from_bindings
225
+ notdef_binding = @bindings.find { |b| b[:donor_gid].zero? }
226
+ if notdef_binding
227
+ copy_glyph_into(@target, name: ".notdef",
228
+ source: notdef_binding[:source],
229
+ donor_gid: 0)
230
+ else
231
+ @target.layers.default_layer.add(Ufo::Glyph.new(name: ".notdef"))
232
+ end
233
+ end
234
+
235
+ # Add ALL glyphs from a CBDT source in source GID order. This
236
+ # ensures the CBLC's GID references remain valid in the output
237
+ # without rewriting the table. Each glyph gets a placeholder
238
+ # (no contours) since the bitmap data is in CBDT, not glyf.
239
+ def add_all_cbdt_glyphs(source)
240
+ ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
241
+ if ufo
242
+ ufo.glyphs.each_value { |g| @target.layers.default_layer.add(clone_glyph(g, name: g.name)) }
243
+ return
244
+ end
245
+
246
+ # For loaded TTF/OTF sources, iterate via cmap to get glyph names.
247
+ # CBDT fonts (like NotoColorEmoji) may have thousands of glyphs;
248
+ # we add them all as placeholders.
249
+ maxp = source.font.table("maxp")
250
+ num_glyphs = maxp&.num_glyphs || 0
251
+ cmap = source.font.table("cmap")
252
+ mappings = cmap&.unicode_mappings || {}
253
+
254
+ # Build gid → [codepoints] from cmap
255
+ gid_cps = Hash.new { |h, k| h[k] = [] }
256
+ mappings.each { |cp, gid| gid_cps[gid] << cp }
257
+
258
+ num_glyphs.times do |gid|
259
+ name = gid.zero? ? ".notdef" : "gid#{gid}"
260
+ glyph = Ufo::Glyph.new(name: name)
261
+ glyph.width = 0
262
+ gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
263
+ @target.layers.default_layer.add(glyph)
264
+ end
265
+ end
266
+
162
267
  # Deep-copy a glyph with a new name. Used so multiple target
163
268
  # glyphs can share the same source outline without aliasing.
164
269
  def clone_glyph(original, name:)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.