# -*- 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-2024 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/document' require 'hexapdf/layout' module HexaPDF # The composer class can be used to create PDF documents from scratch. It uses # HexaPDF::Layout::Frame and HexaPDF::Layout::Box objects underneath and binds them together to # provide a convenient interface for working with them. # # == Usage # # First, a new Composer objects needs to be created, either using ::new or the utility method # ::create. # # On creation a HexaPDF::Document object is created as well the first page and an accompanying # HexaPDF::Layout::Frame object. The frame is used by the various methods for general document # layout tasks, like positioning of text, images, and so on. By default, it covers the whole page # except the margin area. How the frame gets created can be customized by defining a custom page # style, see #page_style. Use the +skip_page_creation+ argument to avoid the initial page # creation when creating a Composer instance. # # Once the Composer object is created, its methods can be used to draw text, images, ... on the # page. Behind the scenes HexaPDF::Layout::Box (and subclass) objects are created using the # HexaPDF::Document::Layout methods and drawn on the page via the frame. # # If the frame of a page is full and a box doesn't fit anymore, a new page is automatically # created. The box is either split into two boxes where one fits on the first page and the other # on the new page, or it is drawn completely on the new page. A new page can also be created by # calling the #new_page method, optionally providing a page style. # # The #x and #y methods provide the point where the next box would be drawn if it fits the # available space. This information can be used, for example, for custom drawing operations # through #canvas which provides direct access to the HexaPDF::Content::Canvas object of the # current page. # # When using #canvas and modifying the graphics state, care has to be taken to avoid problems with # later box drawing operations since the graphics state cannot completely be reset (e.g. # transformations of the canvas cannot always be undone). So it is best to save the graphics state # before and restore it afterwards. # # == Example # # #>pdf-full # HexaPDF::Composer.create('out.pdf', page_size: :A6, margin: 36) do |pdf| # pdf.style(:base, font_size: 20, text_align: :center) # pdf.text("Hello World", text_valign: :center) # end # # See: HexaPDF::Document::Layout, HexaPDF::Layout::Frame, HexaPDF::Layout::Box class Composer # Creates a new PDF document and writes it to +output+. The argument +options+ and +block+ are # passed to ::new. # # Example: # # HexaPDF::Composer.create('out.pdf', margin: 36) do |pdf| # ... # end def self.create(output, **options, &block) new(**options, &block).write(output) end # The PDF document (HexaPDF::Document) that is created. attr_reader :document # The current page (a HexaPDF::Type::Page object). attr_reader :page # The canvas instance (a Content::Canvas object) of the current page. # # Can be used to perform arbitrary drawing operations. attr_reader :canvas # The HexaPDF::Layout::Frame for automatic box placement. attr_reader :frame # Creates a new Composer object and optionally yields it to the given block. # # skip_page_creation:: # If this argument is +false+ (the default), the arguments +page_size+, +page_orientation+ # and +margin+ are used to create a page style with the name :default. Additionally, an # initial page/frame is created using this page style. # # Otherwise, i.e. when this argument is +true+, no initial page or default page style is # created. This is useful when the first page needs a custom page style. The #page_style # method needs to be used to define a page style which is then used with the #new_page # method to create the initial page/frame. # # page_size:: # Can be any valid predefined page size (see Type::Page::PAPER_SIZE) or an array [llx, lly, # urx, ury] specifying a custom page size. # # Only used if +skip_page_creation+ is +false+. # # page_orientation:: # Specifies the orientation of the page, either +:portrait+ or +:landscape+, if +page_size+ # is one of the predefined page sizes. # # Only used if +skip_page_creation+ is +false+. # # margin:: # The margin to use. See HexaPDF::Layout::Style::Quad#set for possible values. # # Only used if +skip_page_creation+ is +false+. # # Example: # # # Uses the default values # composer = HexaPDF::Composer.new # # HexaPDF::Composer.new(page_size: :Letter, margin: 72) do |composer| # #... # end # # HexaPDF::Composer.new(skip_page_creation: true) do |composer| # composer.page_style(:default) do |canvas, style| # style.frame = style.create_frame(canvas.context, 36) # end # composer.new_page # # ... # end def initialize(skip_page_creation: false, page_size: :A4, page_orientation: :portrait, margin: 36) #:yields: composer @document = HexaPDF::Document.new @page_styles = {} @next_page_style = :default unless skip_page_creation page_style(:default, page_size: page_size, orientation: page_orientation) do |canvas, style| style.frame = style.create_frame(canvas.context, margin) end new_page end yield(self) if block_given? end # Creates a new page, making it the current one. # # The page style (see #page_style) to use for the new page can be set via the +style+ argument. # If not provided, the currently set page style is used (:default is the initial value for # @next_page_style). # # The applied page style determines the page style that should be used for the following new # pages (see Layout::PageStyle#next_style). If this information is not provided by the applied # page style, that page style is used again. # # Examples: # # # Define two page styles # composer.page_style(:cover, page_size: :A4, next_style: :content) # composer.page_style(:content, page_size: :A4) # # composer.new_page(:cover) # uses the :cover style, set next style to :content # composer.new_page # uses the :content style, next style again :content def new_page(style = @next_page_style) page_style = @page_styles.fetch(style) do |key| raise ArgumentError, "Page style #{key} has not been defined" end @page = @document.pages.add(page_style.create_page(@document)) @canvas = @page.canvas @frame = page_style.frame @next_page_style = page_style.next_style || style end # The x-position inside the current frame where the next box (provided it fits) will be placed. # # Example: # # #>pdf-composer # composer.text("Hello", position: :float) # composer.canvas.stroke_color("hp-blue"). # circle(composer.x, composer.y, 0.5).fill. # circle(composer.x, composer.y, 5).stroke def x @frame.x end # The y-position inside the current frame.where the next box (provided it fits) will be placed. # # Example: # # #>pdf-composer # composer.text("Hello", position: :float) # composer.canvas.stroke_color("hp-blue"). # circle(composer.x, composer.y, 0.5).fill. # circle(composer.x, composer.y, 5).stroke def y @frame.y end # Writes the created PDF document to the given output. # # See HexaPDF::Document#write for details. def write(output, optimize: true, **options) @document.write(output, optimize: optimize, **options) end # :call-seq: # composer.style(name) -> style # composer.style(name, base: :base, **properties) -> style # # Creates or updates the HexaPDF::Layout::Style object called +name+ with the given property # values and returns it. # # If neither +base+ nor any style properties are specified, the style +name+ is just returned. # # See HexaPDF::Document::Layout#style for details; this method is just a thin wrapper around # that method. # # Example: # # composer.style(:base, font_size: 12, leading: 1.2) # composer.style(:header, font: 'Helvetica', fill_color: "008") # composer.style(:header1, base: :header, font_size: 30) # # See: HexaPDF::Layout::Style def style(name, base: :base, **properties) @document.layout.style(name, base: base, **properties) end # :call-seq: # composer.page_style(name) -> page_style # composer.page_style(name, **attributes, &template_block) -> page_style # # Creates and/or returns the page style +name+. # # If no attributes are given, the page style +name+ is returned. In case it does not exist, # +nil+ is returned. # # If one or more page style attributes are given, a new HexaPDF::Layout::PageStyle object with # those attribute values is created, stored under +name+ and returned. Additionally, if a block # is provided, it is used to define the page template. # # Example: # # composer.page_style(:default) # composer.page_style(:cover, page_size: :A4) do |canvas, style| # page_box = canvas.context.box # canvas.fill_color("green") do # canvas.rectangle(0, 0, page_box.width, page_box.height). # fill # end # style.frame = style.create_frame(canvas.context, 36) # end # # See: HexaPDF::Layout::PageStyle def page_style(name, **attributes, &block) if attributes.empty? && block.nil? @page_styles[name] else @page_styles[name] = HexaPDF::Layout::PageStyle.new(**attributes, &block) end end # Draws the given text at the current position into the current frame. # # The text will be positioned at the current position (see #x and #y) if possible. Otherwise the # next best position is used. If the text doesn't fit onto the current page or only partially, # one or more new pages are created automatically. # # This method is of the two main methods for creating text boxes, the other being # #formatted_text. It uses HexaPDF::Document::Layout#text_box behind the scenes to create the # HexaPDF::Layout::TextBox that does the actual work. # # See HexaPDF::Document::Layout#text_box for details on the arguments. # # Examples: # # #>pdf-composer # composer.text("Test it now " * 15) # composer.text("Now " * 7, width: 100) # composer.text("Another test", font_size: 15, fill_color: "hp-blue") # composer.text("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, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment def text(str, width: 0, height: 0, style: nil, box_style: nil, **style_properties) draw_box(@document.layout.text_box(str, width: width, height: height, style: style, box_style: box_style, **style_properties)) end # Draws text like #text but allows parts of the text to be formatted differently and # interspersing with inline boxes. # # It uses HexaPDF::Document::Layout#formatted_text_box behind the scenes to create the # HexaPDF::Layout::TextBox that does the actual work. See that method for details on the # arguments. # # Examples: # # #>pdf-composer # composer.formatted_text(["Some string"]) # composer.formatted_text(["Some ", {text: "string", fill_color: "hp-orange"}]) # composer.formatted_text(["Some ", {link: "https://example.com", # fill_color: 'hp-blue', text: "Example"}]) # composer.formatted_text(["Some ", {text: "string", style: {font_size: 20}}]) # block = lambda {|list| list.text("First item"); list.text("Second item") } # composer.formatted_text(["Some ", {box: :list, width: 50, # valign: :bottom, block: block}]) # # See: #text, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment def formatted_text(data, width: 0, height: 0, style: nil, box_style: nil, **style_properties) draw_box(@document.layout.formatted_text_box(data, width: width, height: height, style: style, box_style: box_style, **style_properties)) end # Draws the given image at the current position (see #x and #y). # # It uses HexaPDF::Document::Layout#image_box behind the scenes to create the # HexaPDF::Layout::ImageBox that does the actual work. See that method for details on the # arguments. # # Examples: # # #>pdf-composer # composer.image(machu_picchu, border: {width: 3}) # composer.image(machu_picchu, height: 30) # # See: HexaPDF::Layout::ImageBox def image(file, width: 0, height: 0, style: nil, **style_properties) draw_box(@document.layout.image_box(file, width: width, height: height, style: style, **style_properties)) end # Draws the named box at the current position (see #x and #y). # # It uses HexaPDF::Document::Layout#box behind the scenes to create the named box. See that # method for details on the arguments. # # Examples: # # #>pdf-composer # composer.box(:image, image: composer.document.images.add(machu_picchu)) # # See: HexaPDF::Document::Layout#box def box(name, width: 0, height: 0, style: nil, **box_options, &block) draw_box(@document.layout.box(name, width: width, height: height, style: style, **box_options, &block)) end # Draws any custom box that can be created using HexaPDF::Document::Layout. # # This includes all named boxes defined in the 'layout.boxes.map' configuration option. # # Examples: # # #>pdf-composer # composer.lorem_ipsum(sentences: 1, margin: [0, 0, 5]) # composer.list(item_spacing: 2) do |list| # composer.document.config['layout.boxes.map'].each do |name, klass| # list.formatted_text([{text: name.to_s, fill_color: "hp-blue-dark"}, "\n#{klass}"], # font_size: 8) # end # end # # See: HexaPDF::Document::Layout#box def method_missing(name, *args, **kwargs, &block) if @document.layout.box_creation_method?(name) draw_box(@document.layout.send(name, *args, **kwargs, &block)) else super end end def respond_to_missing?(name, _private) # :nodoc: @document.layout.box_creation_method?(name) || super end # Draws the given HexaPDF::Layout::Box and returns the last drawn box. # # The box is drawn into the current frame if possible. If it doesn't fit, the box is split. If # it still doesn't fit, a new region of the frame is determined and then the process starts # again. # # If none or only some parts of the box fit into the current frame, one or more new pages are # created for the rest of the box. def draw_box(box) drawn_on_page = true while true result = @frame.fit(box) if result.success? @frame.draw(@canvas, result) break elsif @frame.full? new_page drawn_on_page = false else draw_box, box = @frame.split(result) if draw_box @frame.draw(@canvas, result) drawn_on_page = true elsif !@frame.find_next_region unless drawn_on_page raise HexaPDF::Error, "Box doesn't fit on empty page" end new_page drawn_on_page = false end end end box end # Creates a stamp (Form XObject) which can be used like an image multiple times on a single page # or on multiple pages. # # The width and the height of the stamp need to be set (frame.width/height or # page.box.width/height might be good choices). # # Examples: # # #>pdf-composer # stamp = composer.create_stamp(50, 50) do |canvas| # canvas.fill_color("hp-blue").line_width(5). # rectangle(10, 10, 30, 30).fill_stroke # end # composer.image(stamp, width: 20, height: 20) # composer.image(stamp, width: 50) def create_stamp(width, height) # :yield: canvas stamp = @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height]}) yield(stamp.canvas) if block_given? stamp end end end