# -*- 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-2025 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/box' require 'hexapdf/layout/text_layouter' module HexaPDF module Layout # A TextBox is used for drawing text, either inside a rectangular box or by flowing it around # objects of a Frame. # # The standard usage is through the helper methods Document::Layout#text and # Document::Layout#formatted_text. # # This class uses TextLayouter behind the scenes to do the hard work. # # == Used Box Properties # # The spacing after the last line can be controlled via the style property +last_line_gap+. Also # see TextLayouter#style for other style properties taken into account. # # == Limitations # # When setting the style property 'position' to +:flow+, padding and border to the left and # right as well as a predefined fixed width are not respected and the result will look wrong. # # == Examples # # Showing some text: # # #>pdf-composer # composer.box(:text, items: layout.text_fragments("This is some text.")) # # Or easier with the provided convenience method # composer.text("This is also some text") # # It is possible to flow the text around other objects by using the style property # 'position' with the value +:flow+: # # #>pdf-composer # composer.box(:base, width: 30, height: 30, # style: {margin: 5, position: :float, background_color: "hp-blue-light"}) # composer.text("This is some text. " * 20, position: :flow) # # While top and bottom padding and border can be used with flow positioning, left and right # padding and border are not supported and the result will look wrong: # # #>pdf-composer # composer.box(:base, width: 30, height: 30, # style: {margin: 5, position: :float, background_color: "hp-blue-light"}) # composer.text("This is some text. " * 20, padding: 10, position: :flow, # text_align: :justify) class TextBox < Box # Creates a new TextBox object with the given inline items (e.g. TextFragment and InlineBox # objects). def initialize(items:, **kwargs) super(**kwargs) @tl = TextLayouter.new(style) @items = items @result = nil @x_offset = 0 end # Returns the text that will be drawn. # # This will ignore any inline boxes or kerning values. def text @items.map {|item| item.kind_of?(TextFragment) ? item.text : '' }.join end # Returns +true+ as the 'position' style property value :flow is supported. def supports_position_flow? true end # :nodoc: def empty? super && (!@result || @result.lines.empty?) end private # Fits the text box into the Frame. # # Depending on the 'position' style property, the text is either fit into the current region # of the frame using +available_width+ and +available_height+, or fit to the shape of the # frame starting from the top (when 'position' is set to :flow). def fit_content(_available_width, _available_height, frame) frame = frame.child_frame(box: self) @x_offset = 0 if style.position == :flow height = (@initial_height > 0 ? @initial_height : frame.shape.bbox.height) - reserved_height @result = @tl.fit(@items, frame.width_specification(reserved_height_top), height, apply_first_text_indent: !split_box?, frame: frame) min_x = +Float::INFINITY max_x = -Float::INFINITY @result.lines.each do |line| min_x = [min_x, line.x_offset].min max_x = [max_x, line.x_offset + line.width].max end @width = (min_x.finite? ? max_x - min_x : 0) + reserved_width fit_result.x = @x_offset = min_x @height = @initial_height > 0 ? @initial_height : @result.height + reserved_height else @result = @tl.fit(@items, @width - reserved_width, @height - reserved_height, apply_first_text_indent: !split_box?, frame: frame) if style.text_align == :left && @initial_width == 0 @width = (@result.lines.max_by(&:width)&.width || 0) + reserved_width end if style.text_valign == :top && @initial_height == 0 @height = @result.height + reserved_height end end if style.last_line_gap && @result.lines.last && @initial_height == 0 @height += style.line_spacing.gap(@result.lines.last, @result.lines.last) end if @result.status == :success fit_result.success! elsif @result.status == :height && !@result.lines.empty? fit_result.overflow! end end # Splits the text box into two. def split_content [self, create_box_for_remaining_items] end # Draws the text into the box. def draw_content(canvas, x, y) return if @result.lines.empty? @result.draw(canvas, x - @x_offset, y + content_height) end # Creates a new TextBox instance for the items remaining after fitting the box. def create_box_for_remaining_items box = create_split_box box.instance_variable_set(:@result, nil) box.instance_variable_set(:@items, @result.remaining_items) box end end end end