Class: Fontisan::Tables::Cff2::PrivateDictBlendHandler

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

Overview

Private DICT blend handler for CFF2

Handles blend operators in Private DICT which allow hint parameters to vary across the design space in variable fonts.

Blend in Private DICT format:

base_value delta1 delta2 ... deltaN num_axes blend

Example for BlueValues with 2 axes:

-10 2 1   0 1 0   500 10 5   510 12 6   2 blend
This creates BlueValues that vary across the design space.

Reference: Adobe Technical Note #5177 (CFF2)

Examples:

Parsing blend in Private DICT

handler = PrivateDictBlendHandler.new(private_dict)
blue_values = handler.parse_blend_array(:blue_values, num_axes: 2)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(private_dict) ⇒ PrivateDictBlendHandler

Initialize handler with Private DICT data

Parameters:

  • private_dict (Hash)

    Parsed Private DICT



30
31
32
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 30

def initialize(private_dict)
  @private_dict = private_dict
end

Instance Attribute Details

#private_dictHash (readonly)

Returns Private DICT data.

Returns:

  • (Hash)

    Private DICT data



25
26
27
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 25

def private_dict
  @private_dict
end

Instance Method Details

#apply_blend(blend_data, scalars) ⇒ Array<Float>, Float

Apply blend at specific coordinates

Parameters:

  • blend_data (Hash)

    Parsed blend data

  • scalars (Array<Float>)

    Region scalars for each axis

Returns:

  • (Array<Float>, Float)

    Blended values



102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 102

def apply_blend(blend_data, scalars)
  return nil unless blend_data

  if blend_data.key?(:blends)
    # Array of blended values
    blend_data[:blends].map do |blend|
      apply_single_blend(blend, scalars)
    end
  else
    # Single blended value
    apply_single_blend(blend_data, scalars)
  end
end

#apply_single_blend(blend, scalars) ⇒ Float

Apply blend to a single value

Parameters:

  • blend (Hash)

    Single blend with :base and :deltas

  • scalars (Array<Float>)

    Region scalars

Returns:

  • (Float)

    Blended value



121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 121

def apply_single_blend(blend, scalars)
  base = blend[:base].to_f
  deltas = blend[:deltas]

  # Apply formula: result = base + Σ(delta[i] * scalar[i])
  result = base
  deltas.each_with_index do |delta, i|
    scalar = scalars[i] || 0.0
    result += delta.to_f * scalar
  end

  result
end

#blend_value?(value) ⇒ Boolean

Check if value looks like blend data

Parameters:

  • value (Object)

    Value to check

Returns:

  • (Boolean)

    True if value could be blend data



168
169
170
171
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 168

def blend_value?(value)
  # Blend values are arrays with multiple elements
  value.is_a?(Array) && value.size > 1
end

#blended_dict(num_axes:, scalars:) ⇒ Hash

Get blended Private DICT values at coordinates

Parameters:

  • num_axes (Integer)

    Number of variation axes

  • scalars (Array<Float>)

    Region scalars

Returns:

  • (Hash)

    Private DICT with blended values



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 140

def blended_dict(num_axes:, scalars:)
  result = {}

  @private_dict.each do |key, value|
    if value.is_a?(Array) && blend_value?(value)
      # Try parsing as blend array
      blend_data = parse_blend_array(key, num_axes: num_axes)
      if blend_data
        result[key] = apply_blend(blend_data, scalars)
      else
        # Try as single blend value
        blend_data = parse_blend_value(key, num_axes: num_axes)
        result[key] =
          blend_data ? apply_blend(blend_data, scalars) : value
      end
    else
      # Non-blend value, copy as-is
      result[key] = value
    end
  end

  result
end

#flatten_blend(blend_data, num_axes:) ⇒ Array

Flatten blend data to array format

Parameters:

  • blend_data (Hash)

    Blend data with :base and :deltas

  • num_axes (Integer)

    Number of variation axes

Returns:

  • (Array)

    Flattened array



207
208
209
210
211
212
213
214
215
216
217
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 207

def flatten_blend(blend_data, num_axes:)
  if blend_data.key?(:blends)
    # Array of blends
    blend_data[:blends].flat_map do |blend|
      [blend[:base]] + blend[:deltas]
    end
  else
    # Single blend
    [blend_data[:base]] + blend_data[:deltas]
  end
end

#has_blend?Boolean

Check if Private DICT contains blend data

Returns:

  • (Boolean)

    True if blend operators are present



37
38
39
40
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 37

def has_blend?
  # In a DICT with blend, values are arrays with blend data
  @private_dict.values.any? { |v| blend_value?(v) }
end

#parse_blend_array(key, num_axes:) ⇒ Hash?

Parse blended array (like BlueValues)

Parameters:

  • key (Symbol, Integer)

    DICT operator key

  • num_axes (Integer)

    Number of variation axes

Returns:

  • (Hash, nil)

    Parsed blend data or nil if not present



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 47

def parse_blend_array(key, num_axes:)
  value = @private_dict[key]
  return nil unless value.is_a?(Array)

  # Check if this is blend data
  # Format: base1 delta1_1 ... delta1_N base2 delta2_1 ... delta2_N ...
  # The array must be divisible by (num_axes + 1)
  return nil unless value.size % (num_axes + 1) == 0

  num_values = value.size / (num_axes + 1)
  blends = []

  num_values.times do |i|
    offset = i * (num_axes + 1)
    base = value[offset]
    deltas = value[offset + 1, num_axes] || []

    blends << {
      base: base,
      deltas: deltas,
    }
  end

  {
    num_values: num_values,
    num_axes: num_axes,
    blends: blends,
  }
end

#parse_blend_value(key, num_axes:) ⇒ Hash?

Parse single blended value

Parameters:

  • key (Symbol, Integer)

    DICT operator key

  • num_axes (Integer)

    Number of variation axes

Returns:

  • (Hash, nil)

    Parsed blend data or nil if not present



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 82

def parse_blend_value(key, num_axes:)
  value = @private_dict[key]
  return nil unless value.is_a?(Array)

  # Single value format: base delta1 delta2 ... deltaN
  expected_size = num_axes + 1
  return nil unless value.size == expected_size

  {
    base: value[0],
    deltas: value[1..num_axes],
    num_axes: num_axes,
  }
end

#rebuild_with_hints(hints, num_axes:) ⇒ Hash

Rebuild Private DICT with hints injected

This method prepares Private DICT for rebuilding, preserving blend operators while incorporating new hint values.

Parameters:

  • hints (Hash)

    Hint values to inject

  • num_axes (Integer)

    Number of variation axes

Returns:

  • (Hash)

    Modified Private DICT



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 181

def rebuild_with_hints(hints, num_axes:)
  result = @private_dict.dup

  # Inject hint values
  hints.each do |key, value|
    if value.is_a?(Hash) && (value.key?(:base) || value.key?("base")) && (value.key?(:deltas) || value.key?("deltas"))
      # Hint with blend data - normalize and flatten for DICT storage
      normalized_value = {
        base: value[:base] || value["base"],
        deltas: value[:deltas] || value["deltas"],
      }
      result[key] = flatten_blend(normalized_value, num_axes: num_axes)
    else
      # Simple hint value
      result[key] = value
    end
  end

  result
end

#validate(num_axes:) ⇒ Array<String>

Validate blend data structure

Parameters:

  • num_axes (Integer)

    Expected number of axes

Returns:

  • (Array<String>)

    Validation errors (empty if valid)



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb', line 223

def validate(num_axes:)
  errors = []

  @private_dict.each do |key, value|
    next unless value.is_a?(Array)
    next unless blend_value?(value)

    # Try parsing as blend array
    blend_data = parse_blend_array(key, num_axes: num_axes)
    unless blend_data
      # Try as single blend value
      blend_data = parse_blend_value(key, num_axes: num_axes)
      unless blend_data
        errors << "Key #{key} has array value that doesn't match " \
                  "blend format for #{num_axes} axes"
      end
    end
  end

  errors
end