lib/hexapdf/document/layout.rb in hexapdf-0.31.0 vs lib/hexapdf/document/layout.rb in hexapdf-0.32.0
- old
+ new
@@ -2,11 +2,11 @@
#
#--
# This file is part of HexaPDF.
#
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
-# Copyright (C) 2014-2022 Thomas Leitner
+# 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):
@@ -42,25 +42,35 @@
# This class provides methods for working with classes in the HexaPDF::Layout module.
#
# Often times the layout related classes are used through HexaPDF::Composer which makes it easy
# to create documents. However, sometimes one wants to have a bit more control or do something
# special and use the HexaPDF::Layout classes directly. This is possible but it is better to use
- # those classes through an instance of this classs because it makes it more convenient and ties
+ # those classes through an instance of this class because it makes it more convenient and ties
# everything together. Incidentally, HexaPDF::Composer relies on this class for a good part of
# its work.
#
#
# == Boxes
#
# The main focus of the class is on providing convenience methods for creating box objects. The
- # most often used box classes like HexaPDF::Layout::TextBox or HexaPDF::Layout::ImagebBox can be
- # created through dedicated methods.
+ # most often used box classes like HexaPDF::Layout::TextBox or HexaPDF::Layout::ImageBox can be
+ # created through dedicated methods:
#
+ # * #text_box
+ # * #formatted_text_box
+ # * #image_box
+ # * #lorem_ipsum_box
+ #
# Other, more general boxes don't have their own method but can be created through the general
- # #box method.
+ # #box method. This method uses the 'layout.boxes.map' configuration option.
#
+ # Additionally, the +_box+ suffix can be omitted, so calling #text, #formatted_text and #image
+ # also works. Furthermore, all box names defined in the 'layout.boxes.map' configuration option
+ # can be used as method names (with or without a +_box+ suffix) and will invoke #box, i.e.
+ # #column and #column_box will also work.
#
+ #
# == Box Styles
#
# All box creation methods accept HexaPDF::Layout::Style objects or names for style objects
# (defined via #style). This allows one to predefine certain styles (like first level heading,
# second level heading, paragraph, ...) and consistently use them throughout the document
@@ -124,33 +134,25 @@
# Create a new ChildrenCollector for the given +layout+ (a HexaPDF::Document::Layout)
# instance.
def initialize(layout)
@layout = layout
- @layout_boxes_map = layout.instance_variable_get(:@document).config['layout.boxes.map']
@children = []
end
# :nodoc:
def method_missing(name, *args, **kwargs, &block)
- if @layout.respond_to?(name)
+ if @layout.box_creation_method?(name)
@children << @layout.send(name, *args, **kwargs, &block)
- elsif @layout.respond_to?("#{name}_box")
- @children << @layout.send("#{name}_box", *args, **kwargs, &block)
- elsif @layout_boxes_map.key?(name)
- @children << @layout.box(name, *args, **kwargs, &block)
else
super
end
end
# :nodoc:
def respond_to_missing?(name, _private)
- @layout.respond_to?(name) ||
- @layout.respond_to?("#{name}_box") ||
- @layout_boxes_map.key?(name) ||
- super
+ @layout.box_creation_method?(name) || super
end
# Appends the given box to the list of collected children.
def <<(box)
@children << box
@@ -206,10 +208,32 @@
style = @styles[name] ||= (@styles.key?(base) ? @styles[base].dup : HexaPDF::Layout::Style.new)
style.update(**properties) unless properties.empty?
style
end
+ # Creates an inline box for use together with text fragments.
+ #
+ # The +valign+ argument ist used to specify the vertical alignment of the box within the text
+ # line. See HexaPDF::Layout::Line for details.
+ #
+ # If a box instance is provided as first argument, it is used. Otherwise the first argument
+ # has to be the name of a box creation method and +args+, +kwargs+ and +block+ are passed to
+ # it.
+ #
+ # Example:
+ #
+ # layout.inline_box(:text, "Hallo")
+ # layout.inline_box(:list) {|list| list.text("Hallo") }
+ def inline_box(box_or_name, *args, valign: :baseline, **kwargs, &block)
+ box = if box_or_name.kind_of?(HexaPDF::Layout::Box)
+ box_or_name
+ else
+ send(box_or_name, *args, **kwargs, &block)
+ end
+ HexaPDF::Layout::InlineBox.new(box, valign: valign)
+ end
+
# Creates the named box and returns it.
#
# The +name+ argument refers to the registered name of the box class that is looked up in the
# 'layout.boxes.map' configuration option. The +box_options+ are passed as-is to the
# initialization method of that box class
@@ -220,12 +244,12 @@
# See #text_box for details on +width+, +height+ and +style+ (note that there is no
# +style_properties+ argument).
#
# Example:
#
- # doc.layout.box(:column, columns: 2, gap: 15) # => column_box_instance
- # doc.layout.box(:column) do |column| # column box with one child
+ # layout.box(:column, columns: 2, gap: 15) # => column_box_instance
+ # layout.box(:column) do |column| # column box with one child
# column.lorem_ipsum
# end
def box(name, width: 0, height: 0, style: nil, **box_options, &block)
if block_given? && !box_options.key?(:children)
box_options[:children] = ChildrenCollector.collect(self, &block)
@@ -260,14 +284,14 @@
#
# The +style+ together with the +style_properties+ will be used for the text style.
#
# Examples:
#
- # layout.text("Test " * 15)
- # layout.text("Now " * 7, width: 100)
- # layout.text("Another test", font_size: 15, fill_color: "green")
- # layout.text("Different box style", fill_color: 'white', box_style: {
+ # layout.text_box("Test " * 15)
+ # layout.text_box("Now " * 7, width: 100)
+ # layout.text_box("Another test", font_size: 15, fill_color: "green")
+ # layout.text_box("Different box style", fill_color: 'white', box_style: {
# underlays: [->(c, b) { c.rectangle(0, 0, b.content_width, b.content_height).fill }]
# })
#
# See: #formatted_text_box, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment
def text_box(text, width: 0, height: 0, style: nil, properties: nil, box_style: nil,
@@ -280,56 +304,83 @@
end
# Creates a HexaPDF::Layout::TextBox like #text_box but allows parts of the text to be
# formatted differently.
#
- # The argument +data+ needs to be an array of String and/or Hash objects:
+ # The argument +data+ needs to be an array of String, HexaPDF::Layout::InlineBox and/or Hash
+ # objects and is transformed so that it is suitable as argument for the text box:
#
# * A String object is treated like {text: data}.
#
+ # * A HexaPDF::Layout::InlineBox is used without modification.
+ #
# * Hashes can contain any style properties and the following special keys:
#
- # text:: The text to be formatted.
+ # text:: The text to be formatted. If this is set and :box is not, the hash will be
+ # transformed into a text fragment.
#
# link:: A URL that should be linked to. If no text is provided but a link, the link is used
- # as text.
+ # for the text. If this is set and :box is not, the hash will be transformed into a
+ # text fragment with an appropriate link overlay.
#
# style:: The style to be use as base style instead of the style created from the +style+
# and +style_properties+ arguments. See HexaPDF::Layout::Style::create for allowed
# values.
#
- # If any style properties are set, the used style is duplicated and the additional
- # properties applied.
+ # If any style properties are set, the used style is duplicated and the additional
+ # properties applied.
#
+ # The final style is used for a created text fragment.
+ #
+ # box:: An inline box to be used. If this is set, the hash will be transformed into an
+ # inline box.
+ #
+ # The value must be one or more (as an array) positional arguments to be used with the
+ # #inline_box method. The rest of the hash keys are passed as keyword arguments to
+ # #inline_box except for :block that value of which would be passed as the block.
+ #
# See #text_box for details on +width+, +height+, +style+, +style_properties+, +properties+
# and +box_style+.
#
# Examples:
#
# layout.formatted_text_box(["Some string"])
# layout.formatted_text_box(["Some ", {text: "string", fill_color: 128}])
# layout.formatted_text_box(["Some ", {link: "https://example.com",
# fill_color: 'blue', text: "Example"}])
# layout.formatted_text_box(["Some ", {text: "string", style: {font_size: 20}}])
+ # layout.formatted_text_box(["Some ", {box: [:text, "string"], valign: :top}])
+ # block = lambda {|list| list.text("First item"); list.text("Second item") }
+ # layout.formatted_text_box(["Some ", {box: :list, item_spacing: 10, block: block}])
#
# See: #text_box, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment
def formatted_text_box(data, width: 0, height: 0, style: nil, properties: nil, box_style: nil,
**style_properties)
style = retrieve_style(style, style_properties)
box_style = (box_style ? retrieve_style(box_style) : style)
- data.map! do |hash|
- if hash.kind_of?(String)
- HexaPDF::Layout::TextFragment.create(hash, style)
+ data.map! do |item|
+ case item
+ when String
+ HexaPDF::Layout::TextFragment.create(item, style)
+ when Hash
+ if (args = item.delete(:box))
+ block = item.delete(:block)
+ inline_box(*args, **item, &block)
+ else
+ link = item.delete(:link)
+ (item[:overlays] ||= []) << [:link, {uri: link}] if link
+ text = item.delete(:text) || link || ""
+ properties = item.delete(:properties)
+ frag_style = retrieve_style(item.delete(:style) || style, item)
+ fragment = HexaPDF::Layout::TextFragment.create(text, frag_style)
+ fragment.properties.update(properties) if properties
+ fragment
+ end
+ when HexaPDF::Layout::InlineBox
+ item
else
- link = hash.delete(:link)
- (hash[:overlays] ||= []) << [:link, {uri: link}] if link
- text = hash.delete(:text) || link || ""
- properties = hash.delete(:properties)
- frag_style = retrieve_style(hash.delete(:style) || style, hash)
- fragment = HexaPDF::Layout::TextFragment.create(text, frag_style)
- fragment.properties.update(properties) if properties
- fragment
+ raise ArgumentError, "Invalid item of class #{item.class} in data array"
end
end
box_class_for_name(:text).new(items: data, width: width, height: height,
properties: properties, style: box_style)
end
@@ -353,12 +404,11 @@
image = file.kind_of?(HexaPDF::Stream) ? file : @document.images.add(file)
box_class_for_name(:image).new(image: image, width: width, height: height,
properties: properties, style: style)
end
- # :nodoc:
- LOREM_IPSUM = [
+ LOREM_IPSUM = [ # :nodoc:
"Lorem ipsum dolor sit amet, con\u{00AD}sectetur adipis\u{00AD}cing elit, sed " \
"do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Ut enim ad minim veniam, quis nostrud exer\u{00AD}citation ullamco laboris nisi ut " \
"aliquip ex ea commodo consequat. ",
"Duis aute irure dolor in reprehen\u{00AD}derit in voluptate velit esse cillum dolore " \
@@ -370,9 +420,42 @@
# Uses #text_box to create +count+ paragraphs of lorem ipsum text.
#
# The +text_box_properties+ arguments are passed as is to #text_box.
def lorem_ipsum_box(sentences: 4, count: 1, **text_box_properties)
text_box(([LOREM_IPSUM[0, sentences].join(" ")] * count).join("\n\n"), **text_box_properties)
+ end
+
+ BOX_METHOD_NAMES = [:text, :formatted_text, :image, :lorem_ipsum] #:nodoc:
+
+ # Allows creating boxes using more convenient method names:
+ #
+ # * #text for #text_box
+ # * #formatted_text for #formatted_text_box
+ # * #image for #image_box
+ # * #lorem_ipsum for #lorem_ipsum_box
+ # * The name of a pre-defined box class like #column will invoke #box appropriately. Same if
+ # used with a '_box' suffix.
+ def method_missing(name, *args, **kwargs, &block)
+ name_without_box = name.to_s.sub(/_box$/, '').intern
+ if BOX_METHOD_NAMES.include?(name)
+ send("#{name}_box", *args, **kwargs, &block)
+ elsif @document.config['layout.boxes.map'].key?(name_without_box)
+ box(name_without_box, *args, **kwargs, &block)
+ else
+ super
+ end
+ end
+
+ # :nodoc:
+ def respond_to_missing?(name, _private)
+ box_creation_method?(name) || super
+ end
+
+ # :nodoc:
+ def box_creation_method?(name)
+ name = name.to_s.sub(/_box$/, '').intern
+ BOX_METHOD_NAMES.include?(name) || @document.config['layout.boxes.map'].key?(name) ||
+ name == :box
end
private
# Returns the configured box class for the given +name+.