class HexaPDF::Layout::TextShaper

This class is used to perform text shaping, i.e. changing the position of glyphs (e.g. for kerning) or substituting one or more glyphs for other glyphs (e.g. for ligatures).

Status of the implementation:

Public Instance Methods

shape_text(text_fragment) click to toggle source

Shapes the given text fragment in-place.

The following shaping options, retrieved from the text fragment's Style#font_features, are supported:

:kern

Pair-wise kerning.

:liga

Ligature substitution.

# File lib/hexapdf/layout/text_shaper.rb, line 61
def shape_text(text_fragment)
  font = text_fragment.style.font
  if text_fragment.style.font_features[:liga] && font.wrapped_font.features.include?(:liga)
    if font.font_type == :Type1
      process_type1_ligatures(text_fragment)
    end
    text_fragment.clear_cache
  end
  if text_fragment.style.font_features[:kern] && font.wrapped_font.features.include?(:kern)
    case font.font_type
    when :TrueType
      process_true_type_kerning(text_fragment)
    when :Type1
      process_type1_kerning(text_fragment)
    end
    text_fragment.clear_cache
  end
  text_fragment
end

Private Instance Methods

each_glyph_pair(items) {|left_item, right_item, left, right} click to toggle source

Yields each pair of glyphs of the items array (so left must not be right + 1 if between two glyphs are one or more kerning values).

The return value of the block is taken as the next left item position.

# File lib/hexapdf/layout/text_shaper.rb, line 134
def each_glyph_pair(items)
  left = 0
  left_item = items[left]
  right = 1
  right_item = items[right]
  while left_item && right_item
    if left_item.kind_of?(Numeric)
      left += 1
      left_item = items[left]
      right = left + 1
    elsif right_item.kind_of?(Numeric)
      right += 1
    else
      left = yield(left_item, right_item, left, right)
      left_item = items[left]
      right = left + 1
    end
    right_item = items[right]
  end
end
process_true_type_kerning(text_fragment) click to toggle source

Processes the text fragment and does pair-wise kerning.

# File lib/hexapdf/layout/text_shaper.rb, line 113
def process_true_type_kerning(text_fragment)
  font = text_fragment.style.font
  table = font.wrapped_font[:kern].horizontal_kerning_subtable
  items = text_fragment.items
  each_glyph_pair(items) do |left_item, right_item, left, right|
    if (left + 1 == right) && (kerning = table.kern(left_item.id, right_item.id))
      items.insert(right, -kerning * font.scaling_factor)
      right + 1
    else
      right
    end
  end
end
process_type1_kerning(text_fragment) click to toggle source

Processes the text fragment and does pair-wise kerning.

# File lib/hexapdf/layout/text_shaper.rb, line 99
def process_type1_kerning(text_fragment)
  pairs = text_fragment.style.font.wrapped_font.metrics.kerning_pairs
  items = text_fragment.items
  each_glyph_pair(items) do |left_item, right_item, left, right|
    if (left + 1 == right) && (kerning = pairs.dig(left_item.id, right_item.id))
      items.insert(right, -kerning)
      right + 1
    else
      right
    end
  end
end
process_type1_ligatures(text_fragment) click to toggle source

Processes the text fragment and substitutes ligatures.

# File lib/hexapdf/layout/text_shaper.rb, line 84
def process_type1_ligatures(text_fragment)
  items = text_fragment.items
  font = text_fragment.style.font
  pairs = font.wrapped_font.metrics.ligature_pairs
  each_glyph_pair(items) do |left_item, right_item, left, right|
    if (ligature = pairs.dig(left_item.id, right_item.id))
      items[left..right] = font.glyph(ligature)
      left
    else
      right
    end
  end
end