Class: Fontisan::Variation::Subsetter

Inherits:
Object
  • Object
show all
Includes:
TableAccessor
Defined in:
lib/fontisan/variation/subsetter.rb

Overview

Subset variable fonts while preserving variation

This class enables subsetting operations on variable fonts while maintaining variation capabilities. It can subset by glyphs, axes, or both, and includes validation to ensure the resulting subset remains a valid variable font.

Subsetting operations:

  1. Glyph subsetting - Keep only specified glyphs with their variations

  2. Axis subsetting - Keep only specified axes

  3. Region simplification - Deduplicate and merge similar regions

  4. Validation - Ensure subset integrity

Examples:

Subset to specific glyphs

subsetter = Fontisan::Variation::Subsetter.new(font)
result = subsetter.subset_glyphs([0, 1, 2, 3])

Subset to specific axes

subsetter = Fontisan::Variation::Subsetter.new(font)
result = subsetter.subset_axes(["wght", "wdth"])

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from TableAccessor

#clear_variation_cache, #clear_variation_table, #has_variation_table?, #require_variation_table, #variation_table

Constructor Details

#initialize(font, options = {}) ⇒ Subsetter

Initialize subsetter

Parameters:

Options Hash (options):

  • :validate (Boolean)

    Validate before/after subsetting (default: true)

  • :optimize (Boolean)

    Optimize after subsetting (default: true)

  • :region_threshold (Float)

    Region similarity threshold (default: 0.01)



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/fontisan/variation/subsetter.rb', line 51

def initialize(font, options = {})
  @font = font
  @validator = Validator.new(font)
  @options = {
    validate: true,
    optimize: true,
    region_threshold: 0.01,
  }.merge(options)
  @report = {}
  @variation_tables = {}
end

Instance Attribute Details

#fontTrueTypeFont, OpenTypeFont (readonly)

Returns Font being subset.

Returns:



33
34
35
# File 'lib/fontisan/variation/subsetter.rb', line 33

def font
  @font
end

#optionsHash (readonly)

Returns Subsetter options.

Returns:

  • (Hash)

    Subsetter options



39
40
41
# File 'lib/fontisan/variation/subsetter.rb', line 39

def options
  @options
end

#reportHash (readonly)

Returns Last operation report.

Returns:

  • (Hash)

    Last operation report



42
43
44
# File 'lib/fontisan/variation/subsetter.rb', line 42

def report
  @report
end

#validatorValidator (readonly)

Returns Validation utility.

Returns:



36
37
38
# File 'lib/fontisan/variation/subsetter.rb', line 36

def validator
  @validator
end

Instance Method Details

#simplify_regions(threshold: nil) ⇒ Hash

Simplify regions within threshold

Uses VariationOptimizer to deduplicate regions.

Parameters:

  • threshold (Float) (defaults to: nil)

    Similarity threshold (default: from options)

Returns:

  • (Hash)

    Simplification result with :tables and :report



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/fontisan/variation/subsetter.rb', line 165

def simplify_regions(threshold: nil)
  threshold ||= @options[:region_threshold]

  @report = {
    operation: :simplify_regions,
    threshold: threshold,
  }

  tables = @font.table_data.dup

  # Optimize CFF2 if present
  if has_variation_table?("CFF2")
    cff2 = variation_table("CFF2")
    optimizer = Optimizer.new(cff2, region_threshold: threshold)
    optimizer.optimize

    @report[:regions_deduplicated] =
      optimizer.stats[:regions_deduplicated]
    @report[:cff2_optimized] = true
  end

  # Simplify metrics table regions
  simplify_metrics_regions(tables, threshold)

  validate_output(tables) if @options[:validate]

  { tables: tables, report: @report }
end

#subset(glyphs: nil, axes: nil, simplify: true) ⇒ Hash

Combined subset operation

Performs multiple subsetting operations in sequence.

Parameters:

  • glyphs (Array<Integer>, nil) (defaults to: nil)

    Glyph IDs to keep (nil = all)

  • axes (Array<String>, nil) (defaults to: nil)

    Axis tags to keep (nil = all)

  • simplify (Boolean) (defaults to: true)

    Simplify regions after subsetting

Returns:

  • (Hash)

    Combined result with :tables and :report



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/fontisan/variation/subsetter.rb', line 202

def subset(glyphs: nil, axes: nil, simplify: true)
  # Don't validate input here - let sub-methods handle it
  # to avoid multiple validations

  steps = []
  tables = @font.table_data.dup

  # Step 1: Subset glyphs if specified
  if glyphs
    subsetter = Subsetter.new(@font, @options)
    glyph_result = subsetter.subset_glyphs(glyphs)
    tables = glyph_result[:tables]
    steps << { step: :subset_glyphs, report: glyph_result[:report] }
  end

  # Step 2: Subset axes if specified
  if axes
    # Create temporary font wrapper with subset tables
    temp_font = create_temp_font(tables)
    axis_subsetter = Subsetter.new(temp_font, @options)
    axis_result = axis_subsetter.subset_axes(axes)
    tables = axis_result[:tables]
    steps << { step: :subset_axes, report: axis_result[:report] }
  end

  # Step 3: Simplify regions if requested
  if simplify && @options[:optimize]
    temp_font = create_temp_font(tables)
    region_subsetter = Subsetter.new(temp_font, @options)
    simplify_result = region_subsetter.simplify_regions
    tables = simplify_result[:tables]
    steps << { step: :simplify_regions, report: simplify_result[:report] }
  end

  # Create combined report
  @report = {
    operation: :combined_subset,
    steps: steps,
  }

  # Validate final output if requested
  if @options[:validate]
    validate_output(tables)
  end

  { tables: tables, report: @report }
end

#subset_axes(axis_tags) ⇒ Hash

Filter to specific axes

Removes unused axes and updates all variation tables.

Parameters:

  • axis_tags (Array<String>)

    Axis tags to keep (e.g., [“wght”, “wdth”])

Returns:

  • (Hash)

    Subset result with :tables and :report



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/fontisan/variation/subsetter.rb', line 111

def subset_axes(axis_tags)
  validate_input if @options[:validate]

  fvar = variation_table("fvar")
  unless fvar
    return { tables: @font.table_data.dup,
             report: { error: "No fvar table" } }
  end

  # Find axes to keep
  all_axes = fvar.axes
  keep_axes = all_axes.select { |axis| axis_tags.include?(axis.axis_tag) }
  keep_indices = keep_axes.map { |axis| all_axes.index(axis) }

  @report = {
    operation: :subset_axes,
    original_axis_count: all_axes.length,
    subset_axis_count: keep_axes.length,
    axes_removed: all_axes.length - keep_axes.length,
    removed_axes: (all_axes.map(&:axis_tag) - axis_tags),
  }

  # Start with all tables
  tables = @font.table_data.dup

  # Update fvar table
  subset_fvar_table(tables, keep_axes, keep_indices)

  # Update gvar if present
  if has_variation_table?("gvar")
    subset_gvar_axes(tables, keep_indices)
    @report[:gvar_updated] = true
  end

  # Update CFF2 if present
  if has_variation_table?("CFF2")
    subset_cff2_axes(tables, keep_indices)
    @report[:cff2_updated] = true
  end

  # Update metrics variation tables
  subset_metrics_axes(tables, keep_indices)

  validate_output(tables) if @options[:validate]

  { tables: tables, report: @report }
end

#subset_glyphs(glyph_ids) ⇒ Hash

Subset glyphs while preserving variation

Filters variation data to keep only specified glyphs.

Parameters:

  • glyph_ids (Array<Integer>)

    Glyph IDs to keep

Returns:

  • (Hash)

    Subset result with :tables and :report



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
96
97
98
99
100
101
102
103
# File 'lib/fontisan/variation/subsetter.rb', line 69

def subset_glyphs(glyph_ids)
  validate_input if @options[:validate]

  @report = {
    operation: :subset_glyphs,
    original_glyph_count: get_glyph_count,
    subset_glyph_count: glyph_ids.length,
    glyphs_removed: get_glyph_count - glyph_ids.length,
  }

  # Start with all tables
  tables = @font.table_data.dup

  # Subset gvar if present
  if has_variation_table?("gvar")
    subset_gvar_table(tables, glyph_ids)
    @report[:gvar_updated] = true
  end

  # Subset CFF2 if present
  if has_variation_table?("CFF2")
    subset_cff2_table(tables, glyph_ids)
    @report[:cff2_updated] = true
  end

  # Subset metrics variations
  subset_metrics_variations(tables, glyph_ids)

  # Update non-variation tables
  update_glyph_tables(tables, glyph_ids)

  validate_output(tables) if @options[:validate]

  { tables: tables, report: @report }
end