# -*- encoding: utf-8; frozen_string_literal: true -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2014-2024 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON # INFRINGEMENT OF THIRD PARTY RIGHTS. # # HexaPDF is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with HexaPDF. If not, see . # # The interactive user interfaces in modified source and object code # versions of HexaPDF must display Appropriate Legal Notices, as required # under Section 5 of the GNU Affero General Public License version 3. # # In accordance with Section 7(b) of the GNU Affero General Public # License, a covered work must retain the producer line in every PDF that # is created or manipulated using HexaPDF. # # If the GNU Affero General Public License doesn't fit your need, # commercial licenses are available at . #++ require 'hexapdf/layout/numeric_refinements' module HexaPDF module Layout using NumericRefinements # 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: # # * All text shaping functionality possible for Type1 fonts is implemented, i.e. kerning and # ligature substitution. # # * For TrueType fonts only kerning via the 'kern' table is implemented. class TextShaper # 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. 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 # Processes the text fragment and substitutes ligatures. 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 # Processes the text fragment and does pair-wise kerning. 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 # Processes the text fragment and does pair-wise kerning. 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 # :call-seq: # each_glyph_pair(items) {|left_item, right_item, left, right} # # 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. 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 end end end