Class: Fontisan::Hints::HintConverter

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/hints/hint_converter.rb

Overview

Converts hints between TrueType and PostScript formats

This converter handles bidirectional conversion of rendering hints, translating between TrueType instruction-based hinting and PostScript operator-based hinting while preserving intent where possible.

Conversion Strategy:

  • TrueType → PostScript: Extract semantic meaning from instructions and convert to corresponding PostScript operators
  • PostScript → TrueType: Analyze hint operators and generate equivalent TrueType instructions

Examples:

Convert TrueType hints to PostScript

converter = HintConverter.new
ps_hints = converter.to_postscript(tt_hints)

Convert PostScript hints to TrueType

converter = HintConverter.new
tt_hints = converter.to_truetype(ps_hints)

Instance Method Summary collapse

Instance Method Details

#convert_hint_set(hint_set, target_format) ⇒ Models::HintSet

Convert entire HintSet between formats

Parameters:

  • hint_set (Models::HintSet)

    Source hint set

  • target_format (Symbol)

    Target format (:truetype or :postscript)

Returns:



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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/fontisan/hints/hint_converter.rb', line 75

def convert_hint_set(hint_set, target_format)
  return hint_set if hint_set.format == target_format.to_s

  result = Models::HintSet.new(format: target_format.to_s)

  case target_format
  when :postscript
    # Convert font-level TT → PS
    if hint_set.font_program || hint_set.control_value_program ||
        hint_set.control_values&.any?
      ps_dict = convert_tt_programs_to_ps_dict(
        hint_set.font_program,
        hint_set.control_value_program,
        hint_set.control_values,
      )
      result.private_dict_hints = ps_dict.to_json
    end

    # Convert per-glyph hints
    hint_set.hinted_glyph_ids.each do |glyph_id|
      glyph_hints = hint_set.get_glyph_hints(glyph_id)
      ps_hints = to_postscript(glyph_hints)
      result.add_glyph_hints(glyph_id, ps_hints) unless ps_hints.empty?
    end

  when :truetype
    # Convert font-level PS → TT
    if hint_set.private_dict_hints && hint_set.private_dict_hints != "{}"
      tt_programs = convert_ps_dict_to_tt_programs(
        JSON.parse(hint_set.private_dict_hints),
      )
      result.font_program = tt_programs[:fpgm]
      result.control_value_program = tt_programs[:prep]
      result.control_values = tt_programs[:cvt]
    end

    # Convert per-glyph hints
    hint_set.hinted_glyph_ids.each do |glyph_id|
      glyph_hints = hint_set.get_glyph_hints(glyph_id)
      tt_hints = to_truetype(glyph_hints)
      result.add_glyph_hints(glyph_id, tt_hints) unless tt_hints.empty?
    end
  end

  result.has_hints = !result.empty?
  result
end

#convert_hint_to_postscript(hint) ⇒ Hint?

Convert a single hint to PostScript format

Parameters:

  • hint (Hint)

    Source hint

Returns:

  • (Hint, nil)

    Converted hint or nil if incompatible



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/fontisan/hints/hint_converter.rb', line 129

def convert_hint_to_postscript(hint)
  return nil unless hint.compatible_with?(:postscript)

  # Get PostScript representation from hint
  ps_data = hint.to_postscript

  # Create new hint with PostScript format
  Models::Hint.new(
    type: hint.type,
    data: ps_data,
    source_format: :postscript,
  )
rescue StandardError => e
  warn "Failed to convert hint to PostScript: #{e.message}"
  nil
end

#convert_hint_to_truetype(hint) ⇒ Hint?

Convert a single hint to TrueType format

Parameters:

  • hint (Hint)

    Source hint

Returns:

  • (Hint, nil)

    Converted hint or nil if incompatible



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/fontisan/hints/hint_converter.rb', line 150

def convert_hint_to_truetype(hint)
  return nil unless hint.compatible_with?(:truetype)

  # Get TrueType representation from hint
  tt_instructions = hint.to_truetype

  # Create new hint with TrueType format
  Models::Hint.new(
    type: hint.type,
    data: { instructions: tt_instructions },
    source_format: :truetype,
  )
rescue StandardError => e
  warn "Failed to convert hint to TrueType: #{e.message}"
  nil
end

#convert_ps_dict_to_tt_programs(ps_dict) ⇒ Hash

Convert PostScript Private dict to TrueType font programs

Generates TrueType control values and programs from PostScript hint parameters using the TrueTypeInstructionGenerator.

Parameters:

  • ps_dict (Hash)

    PostScript Private dict parameters

Returns:

  • (Hash)

    TrueType programs ({ fpgm:, prep:, cvt: })



299
300
301
302
303
304
305
306
# File 'lib/fontisan/hints/hint_converter.rb', line 299

def convert_ps_dict_to_tt_programs(ps_dict)
  # Use the instruction generator to create real TrueType programs
  generator = TrueTypeInstructionGenerator.new
  generator.generate(ps_dict)
rescue StandardError => e
  warn "Error converting PS dict to TT programs: #{e.message}"
  { fpgm: "".b, prep: "".b, cvt: [] }
end

#convert_tt_programs_to_ps_dict(fpgm, prep, cvt) ⇒ Hash

Convert TrueType font programs to PostScript Private dict

Analyzes TrueType fpgm, prep, and cvt to extract semantic intent and generate corresponding PostScript hint parameters using the TrueTypeInstructionAnalyzer.

Parameters:

  • fpgm (String)

    Font program bytecode

  • prep (String)

    Control value program bytecode

  • cvt (Array<Integer>)

    Control values

Returns:

  • (Hash)

    PostScript Private dict hint parameters



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/fontisan/hints/hint_converter.rb', line 239

def convert_tt_programs_to_ps_dict(fpgm, prep, cvt)
  hints = {}

  # Extract stem widths from CVT if present
  # CVT values typically contain standard widths at the beginning
  if cvt && !cvt.empty?
    # First CVT value often represents standard horizontal stem
    hints[:std_hw] = cvt[0].abs if cvt.length.positive?
    # Second CVT value often represents standard vertical stem
    hints[:std_vw] = cvt[1].abs if cvt.length > 1
  end

  # Use the instruction analyzer to extract additional hint parameters
  analyzer = TrueTypeInstructionAnalyzer.new

  # Analyze prep program if present
  prep_hints = if prep && !prep.empty?
                 analyzer.analyze_prep(prep, cvt)
               else
                 {}
               end

  # Analyze fpgm program complexity
  fpgm_hints = if fpgm && !fpgm.empty?
                 analyzer.analyze_fpgm(fpgm)
               else
                 {}
               end

  # Extract blue zones from CVT if present
  blue_zones = if cvt && !cvt.empty?
                 analyzer.extract_blue_zones_from_cvt(cvt)
               else
                 {}
               end

  # Merge all extracted hints (prep_hints and fpgm_hints override stem widths if present)
  # Note: fpgm_hints contains metadata (fpgm_size, has_functions, complexity)
  # which we must filter out before merging into PostScript dict hints
  fpgm_dict_hints = fpgm_hints.except(:fpgm_size, :has_functions,
                                      :complexity)
  hints.merge!(prep_hints).merge!(fpgm_dict_hints).merge!(blue_zones)

  # Provide default blue_values if none were detected
  # These are standard values that work for most Latin fonts
  hints[:blue_values] ||= [-20, 0, 706, 726]

  hints
rescue StandardError => e
  warn "Error converting TT programs to PS dict: #{e.message}"
  {}
end

#hints_conflict?(hint1, hint2) ⇒ Boolean

Check if two hints conflict

Parameters:

  • hint1 (Hint)

    First hint

  • hint2 (Hint)

    Second hint

Returns:

  • (Boolean)

    True if hints conflict



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/fontisan/hints/hint_converter.rb', line 191

def hints_conflict?(hint1, hint2)
  # Hints of different types don't conflict
  return false if hint1.type != hint2.type

  case hint1.type
  when :stem
    # Stem hints conflict if they overlap
    stems_overlap?(hint1.data, hint2.data)
  when :interpolate
    # Multiple interpolation hints on same axis conflict
    hint1.data[:axis] == hint2.data[:axis]
  else
    # Other hint types don't conflict
    false
  end
end

#optimize(hints) ⇒ Array<Hint>

Optimize hint set by removing redundant hints

Parameters:

  • hints (Array<Hint>)

    Hints to optimize

Returns:

  • (Array<Hint>)

    Optimized hints



60
61
62
63
64
65
66
67
68
# File 'lib/fontisan/hints/hint_converter.rb', line 60

def optimize(hints)
  return [] if hints.nil? || hints.empty?

  # Remove duplicate hints
  unique_hints = hints.uniq { |h| [h.type, h.data] }

  # Remove conflicting hints (keep first)
  remove_conflicts(unique_hints)
end

#remove_conflicts(hints) ⇒ Array<Hint>

Remove conflicting hints from set

Parameters:

  • hints (Array<Hint>)

    Hints to check

Returns:

  • (Array<Hint>)

    Non-conflicting hints



171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/fontisan/hints/hint_converter.rb', line 171

def remove_conflicts(hints)
  non_conflicting = []

  hints.each do |hint|
    # Check if this hint conflicts with any already selected
    conflicts = non_conflicting.any? do |existing|
      hints_conflict?(hint, existing)
    end

    non_conflicting << hint unless conflicts
  end

  non_conflicting
end

#stems_overlap?(stem1, stem2) ⇒ Boolean

Check if two stem hints overlap

Parameters:

  • stem1 (Hash)

    First stem data

  • stem2 (Hash)

    Second stem data

Returns:

  • (Boolean)

    True if stems overlap



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/fontisan/hints/hint_converter.rb', line 213

def stems_overlap?(stem1, stem2)
  # Must be same orientation to conflict
  return false if stem1[:orientation] != stem2[:orientation]

  pos1 = stem1[:position] || 0
  width1 = stem1[:width] || 0
  pos2 = stem2[:position] || 0
  width2 = stem2[:width] || 0

  # Check if ranges overlap
  end1 = pos1 + width1
  end2 = pos2 + width2

  pos1 < end2 && pos2 < end1
end

#to_postscript(hints) ⇒ Array<Hint>

Convert hints to PostScript format

Parameters:

  • hints (Array<Hint>)

    Source hints (any format)

Returns:

  • (Array<Hint>)

    Hints in PostScript format



32
33
34
35
36
37
38
39
40
# File 'lib/fontisan/hints/hint_converter.rb', line 32

def to_postscript(hints)
  return [] if hints.nil? || hints.empty?

  hints.map do |hint|
    next hint if hint.source_format == :postscript

    convert_hint_to_postscript(hint)
  end.compact
end

#to_truetype(hints) ⇒ Array<Hint>

Convert hints to TrueType format

Parameters:

  • hints (Array<Hint>)

    Source hints (any format)

Returns:

  • (Array<Hint>)

    Hints in TrueType format



46
47
48
49
50
51
52
53
54
# File 'lib/fontisan/hints/hint_converter.rb', line 46

def to_truetype(hints)
  return [] if hints.nil? || hints.empty?

  hints.map do |hint|
    next hint if hint.source_format == :truetype

    convert_hint_to_truetype(hint)
  end.compact
end