# -*- 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-2019 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/error' require 'hexapdf/layout/text_fragment' module HexaPDF module Layout # A Line describes a line of text and can contain TextFragment objects or InlineBox objects. # # The items of a line fragment are aligned along the x-axis which coincides with the text # baseline. The vertical alignment is determined by the value of the #valign method: # # :text_top:: # Align the top of the box with the top of the text of the Line. # # :text_bottom:: # Align the bottom of the box with the bottom of the text of the Line. # # :baseline:: # Align the bottom of the box with the baseline of the Line. # # :top:: # Align the top of the box with the top of the Line. # # :bottom:: # Align the bottom of the box with the bottom of the Line. # # :text:: # This is a special alignment value for text fragment objects. The text fragment is aligned # on the baseline and its minimum and maximum y-coordinates are used when calculating the # line's #text_y_min and #text_y_max. # # This value may be used by other objects if they should be handled similar to text # fragments, e.g. graphical representation of characters (think: emoji fonts). # # == Item Requirements # # Each item of a line fragment has to respond to the following methods: # # #x_min:: The minimum x-coordinate of the item. # #x_max:: The maximum x-coordinate of the item. # #width:: The width of the item. # #valign:: The vertical alignment of the item (see above). # #draw(canvas, x, y):: Should draw the item onto the canvas at the position (x, y). # # If an item has a vertical alignment of :text, it additionally has to respond to the following # methods: # # #y_min:: The minimum y-coordinate of the item. # #y_max:: The maximum y-coordinate of the item. # # Otherwise (i.e. a vertical alignment different from :text), the following method must be # implemented: # # #height:: The height of the item. class Line # Helper class for calculating the needed vertical dimensions of a line. class HeightCalculator # Creates a new calculator with the given initial items. def initialize(items = []) reset items.each {|item| add(item) } end # Adds a new item to be considered when calculating the various dimensions. def add(item) case item.valign when :text @text_y_min = item.y_min if item.y_min < @text_y_min @text_y_max = item.y_max if item.y_max > @text_y_max when :baseline @max_base_height = item.height if @max_base_height < item.height when :top @max_top_height = item.height if @max_top_height < item.height when :text_top @max_text_top_height = item.height if @max_text_top_height < item.height when :bottom @max_bottom_height = item.height if @max_bottom_height < item.height when :text_bottom @max_text_bottom_height = item.height if @max_text_bottom_height < item.height else raise HexaPDF::Error, "Unknown inline box alignment #{item.valign}" end self end alias << add # Returns the result of the calculations, the array [y_min, y_max, text_y_min, text_y_max]. # # See Line for their meaning. def result y_min = [@text_y_max - @max_text_top_height, @text_y_min].min y_max = [@text_y_min + @max_text_bottom_height, @max_base_height, @text_y_max].max y_min = [y_max - @max_top_height, y_min].min y_max = [y_min + @max_bottom_height, y_max].max [y_min, y_max, @text_y_min, @text_y_max] end # Resets the calculation. def reset @text_y_min = 0 @text_y_max = 0 @max_base_height = 0 @max_top_height = 0 @max_text_top_height = 0 @max_bottom_height = 0 @max_text_bottom_height = 0 end # Returns the height of the line as if +item+ was part of it but doesn't change the internal # state. def simulate_height(item) text_y_min = @text_y_min text_y_max = @text_y_max max_base_height = @max_base_height max_top_height = @max_top_height max_text_top_height = @max_text_top_height max_bottom_height = @max_bottom_height max_text_bottom_height = @max_text_bottom_height y_min, y_max, = add(item).result [y_min, y_max, y_max - y_min] ensure @text_y_min = text_y_min @text_y_max = text_y_max @max_base_height = max_base_height @max_top_height = max_top_height @max_text_top_height = max_text_top_height @max_bottom_height = max_bottom_height @max_text_bottom_height = max_text_bottom_height end end # The items: TextFragment and InlineBox objects attr_accessor :items # An optional horizontal offset that should be taken into account when positioning the line. attr_accessor :x_offset # An optional vertical offset that should be taken into account when positioning the line. # # For the first line in a paragraph this describes the offset from the top of the box to the # baseline of the line. For all other lines it describes the offset from the previous baseline # to the baseline of this line. attr_accessor :y_offset # Creates a new Line object, adding all given items to it. def initialize(items = []) @items = [] items.each {|i| add(i) } @x_offset = 0 @y_offset = 0 end # Adds the given item at the end of the item list. # # If both the item and the last item in the item list are TextFragment objects and they have # the same style, they are combined. # # Note: The cache is not cleared! def add(item) last = @items.last if last.class == item.class && item.kind_of?(TextFragment) && last.style == item.style if last.items.frozen? @items[-1] = last = last.dup last.items = last.items.dup end last.items[last.items.length, 0] = item.items last.clear_cache else @items << item end self end alias << add # :call-seq: # line.each {|item, x, y| block } # # Yields each item together with its horizontal offset from 0 and vertical offset from the # baseline. def each x = 0 @items.each do |item| y = case item.valign when :text, :baseline then 0 when :top then y_max - item.height when :text_top then text_y_max - item.height when :text_bottom then text_y_min when :bottom then y_min else raise HexaPDF::Error, "Unknown inline box alignment #{item.valign}" end yield(item, x, y) x += item.width end end # The minimum x-coordinate of the whole line. def x_min @items[0].x_min end # The maximum x-coordinate of the whole line. def x_max @x_max ||= width + (items[-1].x_max - items[-1].width) end # The minimum y-coordinate of any item of the line. # # It is always lower than or equal to zero. def y_min @y_min ||= calculate_y_dimensions[0] end # The minimum y-coordinate of any TextFragment item of the line. def text_y_min @text_y_min ||= calculate_y_dimensions[2] end # The maximum y-coordinate of any item of the line. # # It is always greater than or equal to zero. def y_max @y_max ||= calculate_y_dimensions[1] end # The maximum y-coordinate of any TextFragment item of the line. def text_y_max @text_y_max ||= calculate_y_dimensions[3] end # The width of the line fragment. def width @width ||= @items.sum(&:width) end # The height of the line fragment. def height y_max - y_min end # Specifies that this line should not be justified if line justification is used. def ignore_justification! @ignore_justification = true end # Returns +true+ if justification should be ignored for this line. def ignore_justification? defined?(@ignore_justification) && @ignore_justification end # :call-seq: # line.clear_cache -> line # # Clears all cached values. # # This method needs to be called if the line's items are changed! def clear_cache @x_max = @y_min = @y_max = @text_y_min = @text_y_max = @width = nil self end private # :call-seq: # line.calculate_y_dimensions -> [y_min, y_max, text_y_min, text_y_max] # # Calculates all y-values and returns them as array. # # The following algorithm is used for the calculations: # # 1. Calculate #text_y_min and #text_y_max by using only the items with valign :text. # # 2. Calculate the temporary #y_min by using either the maximum height of all items with # valign :text_top subtraced from #text_y_max, or #text_y_min, whichever is smaller. # # For the temporary #y_max, use either the maximum height of all items with valign equal to # :text_bottom added to #text_y_min, or the maximum height of all items with valign # :baseline, or #text_y_max, whichever is larger. # # 3. Calculate the final #y_min by using either the maximum height of all items with valign # :top subtracted from the temporary #y_min, or the temporary #y_min, whichever is smaller. # # Calculate the final #y_max by using either the maximum height of all items with valign # :bottom added to #y_min, or the temporary #y_max, whichever is larger. # # In certain cases there is no unique solution to the values of #y_min and #y_max, for # example, it depends on the order of the calculations in part 3. def calculate_y_dimensions @y_min, @y_max, @text_y_min, @text_y_max = HeightCalculator.new(@items).result end end end end