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)



47
48
49
50
51
52
53
54
55
56
57
# File 'lib/fontisan/variation/subsetter.rb', line 47

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:



29
30
31
# File 'lib/fontisan/variation/subsetter.rb', line 29

def font
  @font
end

#optionsHash (readonly)

Returns Subsetter options.

Returns:

  • (Hash)

    Subsetter options



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

def options
  @options
end

#reportHash (readonly)

Returns Last operation report.

Returns:

  • (Hash)

    Last operation report



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

def report
  @report
end

#validatorValidator (readonly)

Returns Validation utility.

Returns:



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

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



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

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



198
199
200
201
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
# File 'lib/fontisan/variation/subsetter.rb', line 198

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



107
108
109
110
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
# File 'lib/fontisan/variation/subsetter.rb', line 107

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



65
66
67
68
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
# File 'lib/fontisan/variation/subsetter.rb', line 65

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