Class: Fontisan::Tables::Cff2::RegionMatcher

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/tables/cff2/region_matcher.rb

Overview

Region matcher for calculating variation scalars

Maps design space coordinates to region scalars based on the Variable Store region definitions. Each region defines a range (start, peak, end) for each variation axis.

Scalar Calculation:

  • If coordinate is at peak: scalar = 1.0

  • If coordinate is between start and peak: linear interpolation

  • If coordinate is between peak and end: linear interpolation

  • If coordinate is outside [start, end]: scalar = 0.0

Reference: OpenType Font Variations Overview Reference: Adobe Technical Note #5177 (CFF2)

Examples:

Calculating scalars

matcher = RegionMatcher.new(regions)
scalars = matcher.calculate_scalars({ "wght" => 0.5, "wdth" => 0.3 })

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(regions) ⇒ RegionMatcher

Initialize matcher with regions

Parameters:

  • regions (Array<Hash>)

    Region definitions from Variable Store



31
32
33
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 31

def initialize(regions)
  @regions = regions
end

Instance Attribute Details

#regionsArray<Hash> (readonly)

Returns Regions from Variable Store.

Returns:

  • (Array<Hash>)

    Regions from Variable Store



26
27
28
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 26

def regions
  @regions
end

Instance Method Details

#active_regions(coordinates) ⇒ Array<Integer>

Get active regions for coordinates

Returns indices of regions that have non-zero scalars

Parameters:

  • coordinates (Array<Float>)

    Normalized coordinates

Returns:

  • (Array<Integer>)

    Indices of active regions



121
122
123
124
125
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 121

def active_regions(coordinates)
  scalars = calculate_scalars(coordinates)
  scalars.each_with_index.select { |scalar, _| scalar.positive? }
    .map(&:last)
end

#axis_countInteger

Get number of axes from first region

Returns:

  • (Integer)

    Number of axes



185
186
187
188
189
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 185

def axis_count
  return 0 if @regions.empty?

  @regions.first[:axis_count] || @regions.first[:axes]&.size || 0
end

#calculate_axis_scalar(axis, coordinate) ⇒ Float

Calculate scalar for a single axis

Parameters:

  • axis (Hash)

    Axis definition with :start_coord, :peak_coord, :end_coord

  • coordinate (Float)

    Normalized coordinate for this axis

Returns:

  • (Float)

    Scalar for this axis (0.0 to 1.0)



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
104
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 78

def calculate_axis_scalar(axis, coordinate)
  start_coord = axis[:start_coord]
  peak_coord = axis[:peak_coord]
  end_coord = axis[:end_coord]

  # Outside the region
  return 0.0 if coordinate < start_coord || coordinate > end_coord

  # At or beyond peak
  return 1.0 if coordinate == peak_coord

  # Between start and peak
  if coordinate < peak_coord
    # Linear interpolation: (coord - start) / (peak - start)
    range = peak_coord - start_coord
    return 1.0 if range.zero? # Avoid division by zero

    (coordinate - start_coord) / range
  else
    # Between peak and end
    # Linear interpolation: (end - coord) / (end - peak)
    range = end_coord - peak_coord
    return 1.0 if range.zero? # Avoid division by zero

    (end_coord - coordinate) / range
  end
end

#calculate_region_scalar(region, coordinates) ⇒ Float

Calculate scalar for a single region

The scalar is the product of scalars for all axes in the region. If any axis has scalar 0.0, the entire region scalar is 0.0.

Parameters:

  • region (Hash)

    Region definition

  • coordinates (Array<Float>)

    Normalized coordinates per axis

Returns:

  • (Float)

    Scalar for the region (0.0 to 1.0)



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 56

def calculate_region_scalar(region, coordinates)
  axes = region[:axes]

  # Multiply scalars for all axes
  scalar = 1.0
  axes.each_with_index do |axis, i|
    coord = coordinates[i] || 0.0
    axis_scalar = calculate_axis_scalar(axis, coord)
    scalar *= axis_scalar

    # Early exit if any axis is out of range
    return 0.0 if axis_scalar.zero?
  end

  scalar
end

#calculate_scalars(coordinates) ⇒ Array<Float>

Calculate scalars for all regions at given coordinates

Coordinates are normalized values in the range [-1.0, 1.0] where 0.0 represents the default/regular style.

Parameters:

  • coordinates (Array<Float>)

    Normalized coordinates per axis

Returns:

  • (Array<Float>)

    Scalars for each region



42
43
44
45
46
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 42

def calculate_scalars(coordinates)
  @regions.map do |region|
    calculate_region_scalar(region, coordinates)
  end
end

#coordinates_active?(coordinates) ⇒ Boolean

Check if coordinates are within any region

Parameters:

  • coordinates (Array<Float>)

    Normalized coordinates

Returns:

  • (Boolean)

    True if coordinates activate any region



110
111
112
113
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 110

def coordinates_active?(coordinates)
  scalars = calculate_scalars(coordinates)
  scalars.any?(&:positive?)
end

#has_regions?Boolean

Check if matcher has regions

Returns:

  • (Boolean)

    True if regions are present



194
195
196
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 194

def has_regions?
  !@regions.empty?
end

#scalar_for_region(region_index, coordinates) ⇒ Float?

Get scalar for specific region index

Parameters:

  • region_index (Integer)

    Region index

  • coordinates (Array<Float>)

    Normalized coordinates

Returns:

  • (Float, nil)

    Scalar for the region, or nil if index invalid



132
133
134
135
136
137
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 132

def scalar_for_region(region_index, coordinates)
  return nil if region_index >= @regions.size

  region = @regions[region_index]
  calculate_region_scalar(region, coordinates)
end

#validateArray<String>

Validate region structure

Returns:

  • (Array<String>)

    Array of validation errors (empty if valid)



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/fontisan/tables/cff2/region_matcher.rb', line 142

def validate
  errors = []

  @regions.each_with_index do |region, i|
    axes = region[:axes]
    unless axes.is_a?(Array)
      errors << "Region #{i} has invalid axes (not an array)"
      next
    end

    axes.each_with_index do |axis, j|
      unless axis.is_a?(Hash)
        errors << "Region #{i}, axis #{j} is not a hash"
        next
      end

      # Check required keys
      %i[start_coord peak_coord end_coord].each do |key|
        unless axis.key?(key)
          errors << "Region #{i}, axis #{j} missing #{key}"
        end
      end

      # Validate coordinate ordering
      if axis[:start_coord] && axis[:peak_coord] && axis[:end_coord]
        start = axis[:start_coord]
        peak = axis[:peak_coord]
        ending = axis[:end_coord]

        unless start <= peak && peak <= ending
          errors << "Region #{i}, axis #{j} has invalid ordering: " \
                    "#{start} > #{peak} > #{ending}"
        end
      end
    end
  end

  errors
end