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
# 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
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



218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/fontisan/subset/table_subsetter.rb', line 218

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



173
174
175
176
177
# File 'lib/fontisan/subset/table_subsetter.rb', line 173

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.

Parameters:

  • table (Head)

    Parsed head table

Returns:

  • (String)

    Binary data of subset head table



267
268
269
# File 'lib/fontisan/subset/table_subsetter.rb', line 267

def subset_head(_table)
  font.table_data["head"]
end

#subset_hhea(table, hmtx = nil) ⇒ String

Subset hhea table (update numberOfHMetrics)

Updates the numberOfHMetrics field to reflect the number of horizontal metrics in the subset font.

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



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/fontisan/subset/table_subsetter.rb', line 121

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

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

  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



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/fontisan/subset/table_subsetter.rb', line 144

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)

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

    data << [metric[:advance_width]].pack("n")
    data << [metric[:lsb]].pack("n")
  end

  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



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/fontisan/subset/table_subsetter.rb', line 186

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



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

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



256
257
258
# File 'lib/fontisan/subset/table_subsetter.rb', line 256

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



278
279
280
281
282
283
284
# File 'lib/fontisan/subset/table_subsetter.rb', line 278

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



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

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



69
70
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
# File 'lib/fontisan/subset/table_subsetter.rb', line 69

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