fontisan 0.4.0 → 0.4.1

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: '020862878eef5569b82edb88989afdc3b7c21d3f01d19b605132af70cabe524f'
4
- data.tar.gz: d8d685a687c94f987835e440619aca344a615368ea78d6caf6a882c34c2900d3
3
+ metadata.gz: 58c1e2768684af1fcdc55fdc2d5b3e47bbd89b0277794dc64ff627ef8d571c3d
4
+ data.tar.gz: 8d1da959f850b3872acb01e96deb098ea2cbb0d596a0923b5db4960edfd818bf
5
5
  SHA512:
6
- metadata.gz: a98055b753907dedfd44fd89c61fd3d5d797d405d702e65730bae1e3299665354f385b135529ca6b48708672e37cfa0e10a7b518ce0b624ccb46fae9459cfb22
7
- data.tar.gz: 46af4e780f4a1786501e2b874d4b439614c212b839b0b398316806a2baa9348b67c8e4db3e63b85142d79ae015df3f093db957f055ddbd3f4955268caa2c1dfa
6
+ metadata.gz: 3e46507d14358098c09f5ee2a3584c601e3d28e6a762f506962007445fb9fcd4432db7f591601575fac87d9cad93ae7c57d2c464728888e33dd6b242387c5611
7
+ data.tar.gz: 1e2dc941fe06dc9769aad1477c970ac48e799d62f3340a08448c0528290562ce2f9ccfd0e021615ebc57a26bcdd19773fd51135d4d214ef31e2a927576c7d06f
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, the source is lazily converted to a UFO::Font
10
- # via Ufo::Convert::FromBinData on first glyph access, then cached.
11
- # This is O(n) in donor glyph count but amortized across all
12
- # codepoint extractions from that donor.
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
- @ufo_cache = nil
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 lives in the bitmap tables, not outlines.
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 converted_ufo_glyph_at(gid)
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,21 +131,111 @@ module Fontisan
117
131
  cmap.unicode_mappings[codepoint]
118
132
  end
119
133
 
120
- # Lazily convert the loaded TTF/OTF to a UFO::Font, then
121
- # extract glyphs from the cached UFO model.
122
- def converted_ufo
123
- return @ufo_cache if @ufo_cache
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
- @ufo_cache = Fontisan::Ufo::Convert::FromBinData.convert(@font)
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 converted_ufo_glyph_at(gid)
129
- ufo = converted_ufo
130
- names = ufo.glyphs.keys
131
- name = names[gid]
132
- return nil unless name
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
+ def extract_single_glyph_from_bindata(gid)
189
+ cache = bin_data_cache
190
+
191
+ if cache[:glyf] && cache[:loca] && cache[:head]
192
+ extract_truetype_glyph(gid, cache)
193
+ end
194
+ end
195
+
196
+ def extract_truetype_glyph(gid, cache)
197
+ simple = cache[:glyf].glyph_for(gid, cache[:loca], cache[:head])
198
+ return nil unless simple
199
+ return nil unless simple.respond_to?(:simple?) && simple.simple?
200
+
201
+ name = gid.zero? ? ".notdef" : "gid#{gid}"
202
+ glyph = Fontisan::Ufo::Glyph.new(name: name)
203
+ glyph.width = glyph_width(gid)
204
+ copy_simple_contours(simple, glyph)
205
+ add_cmap_unicodes(gid, glyph)
206
+ glyph
207
+ rescue StandardError
208
+ nil
209
+ end
210
+
211
+ # Copy a SimpleGlyph's contours + points into a Ufo::Glyph.
212
+ def copy_simple_contours(simple, ufo_glyph)
213
+ num_contours = simple.end_pts_of_contours&.size || 0
214
+ return if num_contours.zero?
215
+
216
+ num_contours.times do |ci|
217
+ points = simple.points_for_contour(ci)
218
+ next unless points && !points.empty?
219
+
220
+ ufo_points = points.map do |pt|
221
+ x = pt[:x] || pt["x"]
222
+ y = pt[:y] || pt["y"]
223
+ on_curve = pt[:on_curve].nil? || pt[:on_curve]
224
+ type = on_curve ? "line" : "offcurve"
225
+ Fontisan::Ufo::Point.new(x: x.to_f, y: y.to_f, type: type)
226
+ end
227
+ ufo_glyph.add_contour(Fontisan::Ufo::Contour.new(ufo_points))
228
+ end
229
+ end
230
+
231
+ # Add Unicode codepoints from the cmap that map to this gid.
232
+ def add_cmap_unicodes(gid, glyph)
233
+ cmap = @font.table("cmap")
234
+ return unless cmap
133
235
 
134
- ufo.glyph(name)
236
+ (cmap.unicode_mappings || {}).each do |cp, g|
237
+ glyph.add_unicode(cp) if g == gid
238
+ end
135
239
  end
136
240
  end
137
241
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
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.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.