# -*- 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-2022 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' module HexaPDF module Layout # A ColumnBox arranges boxes in one or more columns. # # The number of columns as well as the size of the gap between the columns can be modified. class ColumnBox < Box # The child boxes of this ColumnBox. attr_reader :children # The number of columns. # TODO: allow array with column widths later like [100, :*, :*]; same for gaps attr_reader :columns # The size of the gap between the columns. attr_reader :gap # Creates a new ColumnBox object for the given +children+ boxes. def initialize(children = [], columns = 2, gap: 36, **kwargs) super(**kwargs) @children = children @columns = columns @gap = gap end # Fits the column box into the available space. def fit(available_width, available_height, frame) last_height_difference = 1_000_000 height = if style.position == :flow frame.height else (@initial_height > 0 ? @initial_height : available_height) - reserved_height end while true p '-'*100 @frames = [] if style.position == :flow column_width = (frame.width - gap * (@columns - 1)).to_f / @columns @columns.times do |col_nr| left = (column_width + gap) * col_nr + frame.left bottom = frame.bottom rect = Geom2D::Polygon([left, bottom], [left + column_width, bottom], [left + column_width, bottom + height], [left, bottom + height]) shape = Geom2D::Algorithms::PolygonOperation.run(frame.shape, rect, :intersection) col_frame = Frame.new(left, bottom, column_width, height) col_frame.shape = shape @frames << col_frame end @frame_index = 0 @results = @children.map {|child_box| fit_box(child_box) } @width = frame.width @height = frame.height - @frames.min_by(&:y).y else width = (@initial_width > 0 ? @initial_width : available_width) - reserved_width column_width = (width - gap * (@columns - 1)).to_f / @columns @columns.times do |col_nr| @frames << Frame.new((column_width + gap) * col_nr, 0, column_width, height) end @frame_index = 0 @results = @children.map {|child_box| fit_box(child_box) } @width = width @height = height - @frames.min_by(&:y).y end min_y, max_y = @frames.minmax_by(&:y).map(&:y) p [height, @frames.map(&:y), last_height_difference, min_y, max_y] # TOOD: @result.any?(&:empty?) only for the first run!!!! if the first run fails, we # cannot balance the columns because there is too much content. # TODO: another break condition is if the @results didn't change since the last run p [:maybe_redo, min_y, max_y, height, last_height_difference] p [@results.map {|arr| arr.all? {|r| r.status }}] break if max_y != height && @results.all? {|arr| !arr.empty? && arr.all? {|r| r.success? }} && (@results.any?(&:empty?) || max_y - min_y >= last_height_difference || max_y - min_y < 0.5) if max_y == 0 && min_y == 0 height += last_height_difference / 4.0 else last_height_difference = max_y - min_y height -= last_height_difference / 2.0 end end @results.all? {|res| res.length == 1 } end private def fit_box(box) cur_frame = @frames[@frame_index] fit_results = [] while cur_frame result = cur_frame.fit(box) if result.success? cur_frame.remove_area(result.mask) fit_results << result break elsif cur_frame.full? @frame_index += 1 break if @frame_index == @frames.length cur_frame = @frames[@frame_index] else draw_box, box = cur_frame.split(result) if draw_box cur_frame.remove_area(result.mask) fit_results << result elsif !cur_frame.find_next_region @frame_index += 1 break if @frame_index == @frames.length cur_frame = @frames[@frame_index] end end end fit_results end # Draws the child boxes onto the canvas at position [x, y]. def draw_content(canvas, x, y) x = y = 0 if style.position == :flow @results.each do |result_boxes| result_boxes.each do |result| result.box.draw(canvas, x + result.x, y + result.y) end end end end end end