Class: Fontisan::Hints::HintConverter
- Inherits:
-
Object
- Object
- Fontisan::Hints::HintConverter
- 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
Instance Method Summary collapse
-
#convert_hint_set(hint_set, target_format) ⇒ Models::HintSet
Convert entire HintSet between formats.
-
#convert_hint_to_postscript(hint) ⇒ Hint?
Convert a single hint to PostScript format.
-
#convert_hint_to_truetype(hint) ⇒ Hint?
Convert a single hint to TrueType format.
-
#convert_ps_dict_to_tt_programs(ps_dict) ⇒ Hash
Convert PostScript Private dict to TrueType font programs.
-
#convert_tt_programs_to_ps_dict(fpgm, prep, cvt) ⇒ Hash
Convert TrueType font programs to PostScript Private dict.
-
#hints_conflict?(hint1, hint2) ⇒ Boolean
Check if two hints conflict.
-
#optimize(hints) ⇒ Array<Hint>
Optimize hint set by removing redundant hints.
-
#remove_conflicts(hints) ⇒ Array<Hint>
Remove conflicting hints from set.
-
#stems_overlap?(stem1, stem2) ⇒ Boolean
Check if two stem hints overlap.
-
#to_postscript(hints) ⇒ Array<Hint>
Convert hints to PostScript format.
-
#to_truetype(hints) ⇒ Array<Hint>
Convert hints to TrueType format.
Instance Method Details
#convert_hint_set(hint_set, target_format) ⇒ Models::HintSet
Convert entire HintSet between formats
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
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
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.
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.
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
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
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
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
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
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
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 |