Class: Fontisan::Subset::TableSubsetter

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/subset/table_subsetter.rb

Overview

Table-specific subsetting strategies

This class provides methods for subsetting individual font tables according to the glyph mapping. Each table type has different subsetting requirements:

  • maxp: Update glyph count
  • hhea: Update horizontal metrics count
  • hmtx: Subset horizontal metrics
  • glyf: Subset glyph data and remap component references
  • loca: Rebuild glyph location index
  • cmap: Remap character to glyph mappings
  • post: Optionally drop glyph names
  • name: Pass through (no subsetting needed)
  • head: Update checksum adjustment (handled by FontWriter)
  • OS/2: Optionally prune Unicode ranges

The subsetting process preserves font validity by updating all references and recalculating offsets and checksums.

Examples:

Subset a single table

subsetter = TableSubsetter.new(font, mapping, options)
maxp_data = subsetter.subset_maxp(maxp_table)

Subset all tables

subsetter = TableSubsetter.new(font, mapping, options)
subset_tables = {}
profile_tables.each do |tag|
  table = font.table(tag)
  subset_tables[tag] = subsetter.subset_table(tag, table) if table
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(font, mapping, options) ⇒ TableSubsetter

Initialize table subsetter

Parameters:



53
54
55
56
57
58
59
60
61
# File 'lib/fontisan/subset/table_subsetter.rb', line 53

def initialize(font, mapping, options)
  @font = font
  @mapping = mapping
  @options = options
  @glyf_data = nil
  @loca_offsets = nil
  @subset_bbox = nil      # [xMin, yMin, xMax, yMax] over actual subset glyphs
  @subset_max_advance = 0 # largest advanceWidth in subset hmtx
end

Instance Attribute Details

#fontTrueTypeFont, OpenTypeFont (readonly)

Font instance being subset



38
39
40
# File 'lib/fontisan/subset/table_subsetter.rb', line 38

def font
  @font
end

#mappingGlyphMapping (readonly)

Glyph ID mapping (old GID → new GID)

Returns:



42
43
44
# File 'lib/fontisan/subset/table_subsetter.rb', line 42

def mapping
  @mapping
end

#optionsOptions (readonly)

Subsetting options

Returns:



46
47
48
# File 'lib/fontisan/subset/table_subsetter.rb', line 46

def options
  @options
end

Instance Method Details

#subset_cmap(table) ⇒ String

Subset cmap table (remap character to glyph mappings)

Builds new cmap table with only mappings for glyphs in the subset. Updates glyph IDs to new values from the mapping.

Parameters:

  • table (Cmap)

    Parsed cmap table

Returns:

  • (String)

    Binary data of subset cmap table



235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/fontisan/subset/table_subsetter.rb', line 235

def subset_cmap(table)
  # Get old mappings
  old_mappings = table.unicode_mappings
  new_mappings = {}

  # Remap to new glyph IDs
  old_mappings.each do |char_code, old_gid|
    new_gid = mapping.new_id(old_gid)
    new_mappings[char_code] = new_gid if new_gid
  end

  # Build cmap binary with new mappings
  build_cmap_binary(new_mappings)
end

#subset_glyf(table) ⇒ String

Subset glyf table (subset glyph data)

Extracts glyph data for subset glyphs and remaps component references in compound glyphs. Also builds loca offsets.

Parameters:

  • table (Glyf)

    Parsed glyf table

Returns:

  • (String)

    Binary data of subset glyf table



190
191
192
193
194
# File 'lib/fontisan/subset/table_subsetter.rb', line 190

def subset_glyf(table)
  # Build glyf and loca together
  build_glyf_and_loca(table)
  @glyf_data
end

#subset_head(_table) ⇒ String

Subset head table (pass through)

head table will have checksum updated by FontWriter, no subsetting needed.

Subset head table (recompute bbox from actual subset glyphs)

The source TTC's head.xMin/yMin/xMax/yMax covers every donor font in the collection — far larger than any per-block subset. Browsers and layout engines use head bbox (plus hhea + OS/2) to compute line height and clip text; a too-large bbox makes glyphs render at the wrong visual size.

Parameters:

  • table (Head)

    Parsed head table

  • _table (Head)

    Parsed head table (unused; we re-serialize directly from font.table_data so we don't lose other fields)

Returns:

  • (String)

    Binary data of subset head table

  • (String)

    Binary data of subset head table



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/fontisan/subset/table_subsetter.rb', line 295

def subset_head(_table)
  # Trigger glyf build (which populates @subset_bbox) if not done.
  glyf = font.table("glyf")
  build_glyf_and_loca(glyf) unless @glyf_data

  data = font.table_data["head"].dup

  if @subset_bbox
    x_min, y_min, x_max, y_max = @subset_bbox
    # head layout: xMin at offset 36, yMin 38, xMax 40, yMax 42
    # (each int16, big-endian, signed)
    data[36, 8] = [x_min, y_min, x_max, y_max].pack("n4")
  end

  data
end

#subset_hhea(table, hmtx = nil) ⇒ String

Subset hhea table (update numberOfHMetrics + advanceWidthMax)

Updates numberOfHMetrics to reflect the subset's glyph count and recomputes advanceWidthMax from the subset's actual hmtx. The source TTC's advanceWidthMax covers every donor font and is far larger than any per-block subset needs.

Parameters:

  • table (Hhea)

    Parsed hhea table

  • hmtx (Hmtx, nil) (defaults to: nil)

    Optional parsed hmtx table (for calculating metrics)

Returns:

  • (String)

    Binary data of subset hhea table



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/fontisan/subset/table_subsetter.rb', line 125

def subset_hhea(table, hmtx = nil)
  data = table.to_binary_s.dup

  # Calculate new numberOfHMetrics
  new_num_h_metrics = if hmtx&.h_metrics
                        hmtx.h_metrics.size
                      else
                        calculate_number_of_h_metrics
                      end

  # numberOfHMetrics at offset 34 (uint16)
  data[34, 2] = [new_num_h_metrics].pack("n")

  # advanceWidthMax at offset 10 (uint16). Recompute from subset
  # hmtx so a per-block subset doesn't keep the source TTC's
  # max (which can be 4x larger than any glyph in the subset).
  # Tables are processed alphabetically (hhea before hmtx), so
  # we read hmtx directly here rather than relying on a cached
  # value from subset_hmtx.
  new_max = compute_subset_max_advance
  data[10, 2] = [new_max].pack("n") if new_max.positive?

  data
end

#subset_hmtx(table) ⇒ String

Subset hmtx table (subset horizontal metrics)

Builds new hmtx table with metrics for subset glyphs only, preserving the order of the glyph mapping.

Parameters:

  • table (Hmtx)

    Parsed hmtx table

Returns:

  • (String)

    Binary data of subset hmtx table



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/fontisan/subset/table_subsetter.rb', line 157

def subset_hmtx(table)
  # Ensure hmtx is parsed
  unless table.parsed?
    hhea = font.table("hhea")
    maxp = font.table("maxp")
    table.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
  end

  # Build new hmtx data
  data = String.new(encoding: Encoding::BINARY)

  max_advance = 0
  mapping.old_ids.each do |old_id|
    metric = table.metric_for(old_id)
    next unless metric

    advance = metric[:advance_width]
    max_advance = advance if advance && advance > max_advance
    data << [advance].pack("n")
    data << [metric[:lsb]].pack("n")
  end

  @subset_max_advance = max_advance
  data
end

#subset_loca(_table) ⇒ String

Subset loca table (rebuild glyph location index)

Builds new loca table based on subset glyph offsets. Must be called after subset_glyf.

Parameters:

  • table (Loca)

    Parsed loca table

Returns:

  • (String)

    Binary data of subset loca table



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/fontisan/subset/table_subsetter.rb', line 203

def subset_loca(_table)
  # Build glyf and loca together if not already done
  glyf = font.table("glyf")
  build_glyf_and_loca(glyf) unless @loca_offsets

  head = font.table("head")
  format = head.index_to_loc_format

  data = String.new(encoding: Encoding::BINARY)

  if format.zero?
    # Short format: offsets / 2 as uint16
    @loca_offsets.each do |offset|
      data << [offset / 2].pack("n")
    end
  else
    # Long format: offsets as uint32
    @loca_offsets.each do |offset|
      data << [offset].pack("N")
    end
  end

  data
end

#subset_maxp(table) ⇒ String

Subset maxp table (update numGlyphs)

Updates the numGlyphs field to reflect the number of glyphs in the subset font.

Parameters:

  • table (Maxp)

    Parsed maxp table

Returns:

  • (String)

    Binary data of subset maxp table



106
107
108
109
110
111
112
113
# File 'lib/fontisan/subset/table_subsetter.rb', line 106

def subset_maxp(table)
  data = table.to_binary_s.dup

  # Update numGlyphs field (at offset 4, uint16)
  data[4, 2] = [mapping.size].pack("n")

  data
end

#subset_name(_table) ⇒ String

Subset name table (pass through)

Name table doesn't require subsetting, pass through unchanged.

Parameters:

  • table (Name)

    Parsed name table

Returns:

  • (String)

    Binary data of subset name table



273
274
275
# File 'lib/fontisan/subset/table_subsetter.rb', line 273

def subset_name(_table)
  font.table_data["name"]
end

#subset_os2(_table) ⇒ String

Subset OS/2 table (optionally prune Unicode ranges)

If unicode_ranges option is set, updates Unicode range bits to reflect only the characters in the subset.

Parameters:

  • table (Os2)

    Parsed OS/2 table

Returns:

  • (String)

    Binary data of subset OS/2 table



319
320
321
322
323
324
325
# File 'lib/fontisan/subset/table_subsetter.rb', line 319

def subset_os2(_table)
  if options.unicode_ranges
    # TODO: Implement Unicode range pruning
    # For now, pass through
  end
  font.table_data["OS/2"]
end

#subset_post(table) ⇒ String

Subset post table (optionally drop glyph names)

If drop_names option is set, converts to post version 3.0 (no glyph names). Otherwise passes through unchanged.

Parameters:

  • table (Post)

    Parsed post table

Returns:

  • (String)

    Binary data of subset post table



257
258
259
260
261
262
263
264
265
# File 'lib/fontisan/subset/table_subsetter.rb', line 257

def subset_post(table)
  if options.drop_names
    # Build post table version 3.0 (no glyph names)
    build_post_v3(table)
  else
    # Keep as-is
    font.table_data["post"]
  end
end

#subset_table(tag, table) ⇒ String

Subset a table by tag

Delegates to table-specific subsetting methods. Unknown tables are passed through unchanged.

Parameters:

  • tag (String)

    Table tag (e.g., "glyf", "hmtx")

  • table (Object)

    Parsed table object

Returns:

  • (String)

    Binary data of subset table



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/fontisan/subset/table_subsetter.rb', line 71

def subset_table(tag, table)
  case tag
  when "maxp"
    subset_maxp(table)
  when "hhea"
    subset_hhea(table)
  when "hmtx"
    subset_hmtx(table)
  when "loca"
    subset_loca(table)
  when "glyf"
    subset_glyf(table)
  when "cmap"
    subset_cmap(table)
  when "post"
    subset_post(table)
  when "name"
    subset_name(table)
  when "head"
    subset_head(table)
  when "OS/2"
    subset_os2(table)
  else
    # Unknown tables pass through unchanged
    font.table_data[tag]
  end
end