Skip to content

Commit

Permalink
Add Layout::ContainerBox for laying out boxes together
Browse files Browse the repository at this point in the history
  • Loading branch information
gettalong committed Jan 20, 2024
1 parent be43fd9 commit ca38f5c
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased

### Added

* [HexaPDF::Layout::ContainerBox] for grouping child boxes together

### Changed

* [HexaPDF::Layout::Frame::FitResult#draw] to allow drawing at an offset
Expand Down
1 change: 1 addition & 0 deletions lib/hexapdf/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@ def self.font_on_invalid_glyph(codepoint, invalid_glyph)
column: 'HexaPDF::Layout::ColumnBox',
list: 'HexaPDF::Layout::ListBox',
table: 'HexaPDF::Layout::TableBox',
container: 'HexaPDF::Layout::ContainerBox',
},
'page.default_media_box' => :A4,
'page.default_media_orientation' => :portrait,
Expand Down
1 change: 1 addition & 0 deletions lib/hexapdf/layout.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ module Layout
autoload(:ListBox, 'hexapdf/layout/list_box')
autoload(:PageStyle, 'hexapdf/layout/page_style')
autoload(:TableBox, 'hexapdf/layout/table_box')
autoload(:ContainerBox, 'hexapdf/layout/container_box')

end

Expand Down
159 changes: 159 additions & 0 deletions lib/hexapdf/layout/container_box.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# -*- 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-2023 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 <http://www.gnu.org/licenses/>.
#
# 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 <https://gettalong.at/hexapdf/>.
#++
require 'hexapdf/layout/box'
require 'hexapdf/layout/box_fitter'
require 'hexapdf/layout/frame'

module HexaPDF
module Layout

# This is a simple container box for laying out a number of boxes together. It is registered
# under the :container name.
#
# The box does not support the value :flow for the style property position, so the child boxes
# are laid out in the current region only. Since the boxes should be laid out together, if any
# box doesn't fit, the whole container doesn't fit. Splitting the container is also not possible
# for the same reason.
#
# By default the child boxes are laid out from top to bottom by default. By appropriately
# setting the style properties 'mask_mode', 'align' and 'valign', it is possible to lay out the
# children bottom to top, left to right, or right to left:
#
# * The standard top to bottom layout:
#
# #>pdf-composer100
# composer.container do |container|
# container.box(:base, height: 20, style: {background_color: "hp-blue-dark"})
# container.box(:base, height: 20, style: {background_color: "hp-blue"})
# container.box(:base, height: 20, style: {background_color: "hp-blue-light"})
# end
#
# * The bottom to top layout (using valign = :bottom to fill up from the bottom and mask_mode =
# :fill_horizontal to only remove the area to the left and right of the box):
#
# #>pdf-composer100
# composer.container do |container|
# container.box(:base, height: 20, style: {background_color: "hp-blue-dark",
# mask_mode: :fill_horizontal, valign: :bottom})
# container.box(:base, height: 20, style: {background_color: "hp-blue",
# mask_mode: :fill_horizontal, valign: :bottom})
# container.box(:base, height: 20, style: {background_color: "hp-blue-light",
# mask_mode: :fill_horizontal, valign: :bottom})
# end
#
# * The left to right layout (using mask_mode = :fill_vertical to fill the area to the top and
# bottom of the box):
#
# #>pdf-composer100
# composer.container do |container|
# container.box(:base, width: 20, style: {background_color: "hp-blue-dark",
# mask_mode: :fill_vertical})
# container.box(:base, width: 20, style: {background_color: "hp-blue",
# mask_mode: :fill_vertical})
# container.box(:base, width: 20, style: {background_color: "hp-blue-light",
# mask_mode: :fill_vertical})
# end
#
# * The right to left layout (using align = :right to fill up from the right and mask_mode =
# :fill_vertical to fill the area to the top and bottom of the box):
#
# #>pdf-composer100
# composer.container do |container|
# container.box(:base, width: 20, style: {background_color: "hp-blue-dark",
# mask_mode: :fill_vertical, align: :right})
# container.box(:base, width: 20, style: {background_color: "hp-blue",
# mask_mode: :fill_vertical, align: :right})
# container.box(:base, width: 20, style: {background_color: "hp-blue-light",
# mask_mode: :fill_vertical, align: :right})
# end
class ContainerBox < Box

# The child boxes of this ContainerBox. They need to be finalized before #fit is called.
attr_reader :children

# Creates a new container box, optionally accepting an array of child boxes.
#
# Example:
#
# #>pdf-composer100
# composer.text("A paragraph here")
# composer.container(height: 40, style: {border: {width: 1}, padding: 5,
# align: :center}) do |container|
# container.text("Some", mask_mode: :fill_vertical)
# container.text("text", mask_mode: :fill_vertical, valign: :center)
# container.text("here", mask_mode: :fill_vertical, valign: :bottom)
# end
# composer.text("Another paragraph")
def initialize(children: [], **kwargs)
super(**kwargs)
@children = children
end

# Returns +true+ if no box was fitted into the container.
def empty?
super && (!@box_fitter || @box_fitter.fit_results.empty?)
end

private

# Fits the children into the container.
def fit_content(available_width, available_height, frame)
my_frame = Frame.new(frame.x + reserved_width_left, frame.y - @height + reserved_height_bottom,
content_width, content_height, context: frame.context)
@box_fitter = BoxFitter.new([my_frame])
children.each {|box| @box_fitter.fit(box) }

if @box_fitter.fit_successful?
update_content_width do
result = @box_fitter.fit_results.max_by {|result| result.mask.x + result.mask.width }
children.size > 0 ? result.mask.x + result.mask.width - my_frame.left : 0
end
update_content_height { @box_fitter.content_heights.max }
true
end
end

# Draws the image onto the canvas at position [x, y].
def draw_content(canvas, x, y)
dx = x - @fit_x
dy = y - @fit_y
@box_fitter.fit_results.each {|result| result.draw(canvas, dx: dx, dy: dy) }
end

end

end
end
84 changes: 84 additions & 0 deletions test/hexapdf/layout/test_container_box.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# -*- encoding: utf-8 -*-

require 'test_helper'
require 'hexapdf/document'
require 'hexapdf/layout/container_box'

describe HexaPDF::Layout::ContainerBox do
before do
@doc = HexaPDF::Document.new
@frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100)
end

def create_box(children, **kwargs)
HexaPDF::Layout::ContainerBox.new(children: Array(children), **kwargs)
end

def check_box(box, width, height, fit_pos = nil)
assert(box.fit(@frame.available_width, @frame.available_height, @frame), "box didn't fit")
assert_equal(width, box.width, "box width")
assert_equal(height, box.height, "box height")
if fit_pos
box_fitter = box.instance_variable_get(:@box_fitter)
assert_equal(fit_pos.size, box_fitter.fit_results.size)
fit_pos.each_with_index do |(x, y), index|
assert_equal(x, box_fitter.fit_results[index].x, "result[#{index}].x")
assert_equal(y, box_fitter.fit_results[index].y, "result[#{index}].y")
end
end
end

describe "empty?" do
it "is empty if nothing is fit yet" do
assert(create_box([]).empty?)
end

it "is empty if no box fits" do
box = create_box(@doc.layout.box(width: 110))
box.fit(@frame.available_width, @frame.available_height, @frame)
assert(box.empty?)
end

it "is not empty if at least one box fits" do
box = create_box(@doc.layout.box(width: 50, height: 20))
check_box(box, 100, 20)
refute(box.empty?)
end
end

describe "fit_content" do
it "fits the children according to their mask_mode, valign, and align style properties" do
box = create_box([@doc.layout.box(height: 20),
@doc.layout.box(height: 20, style: {valign: :bottom, mask_mode: :fill_horizontal}),
@doc.layout.box(width: 20, style: {align: :right, mask_mode: :fill_vertical})])
check_box(box, 100, 100, [[0, 80], [0, 0], [80, 20]])
end

it "respects the initially set width/height as well as border/padding" do
box = create_box(@doc.layout.box(height: 20), height: 50, width: 40,
style: {padding: 2, border: {width: 3}})
check_box(box, 40, 50, [[5, 75]])
end
end

describe "draw_content" do
before do
@canvas = @doc.pages.add.canvas
end

it "draws the result onto the canvas" do
child_box = @doc.layout.box(height: 20) do |canvas, b|
canvas.line(0, 0, b.content_width, b.content_height).end_path
end
box = create_box(child_box)
box.fit(100, 100, @frame)
box.draw(@canvas, 0, 50)
assert_operators(@canvas.contents, [[:save_graphics_state],
[:concatenate_matrix, [1, 0, 0, 1, 0, 50]],
[:move_to, [0, 0]],
[:line_to, [100, 20]],
[:end_path],
[:restore_graphics_state]])
end
end
end

0 comments on commit ca38f5c

Please sign in to comment.