class HexaPDF::Composer

The composer class can be used to create PDF documents from scratch. It uses Frame and Box objects underneath.

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 overriding the create_frame method.

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 and drawn on the page via the frame.

The base style that is used by all these boxes can be defined using the base_style method which returns a HexaPDF::Layout::Style object. The only style property that is set by default is the font (Times) because otherwise there would be problems with text drawing operations (font is the only style property that has no valid default value).

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.

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

HexaPDF::Composer.create('output.pdf', margin: 36) do |pdf|
  pdf.base_style.font_size(20).align(:center)
  pdf.text("Hello World", valign: :center)
end

Attributes

base_style[R]

The base style which is used when no explicit style is provided to methods (e.g. to text).

canvas[R]

The Content::Canvas of the current page. Can be used to perform arbitrary drawing operations.

document[R]

The PDF document that is created.

frame[R]

The Layout::Frame for automatic box placement.

page[R]

The current page (a HexaPDF::Type::Page object).

Public Class Methods

create(output, **options, &block) click to toggle source

Creates a new PDF document and writes it to output. The options are passed to ::new.

Example:

HexaPDF::Composer.create('output.pdf', margin: 36) do |pdf|
  ...
end
# File lib/hexapdf/composer.rb, line 94
def self.create(output, **options, &block)
  new(**options, &block).write(output)
end
new(page_size: :A4, page_orientation: :portrait, margin: 36) { |composer| ... } click to toggle source

Creates a new Composer object and optionally yields it to the given block.

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.

page_orientation

Specifies the orientation of the page, either :portrait or :landscape. Only used if page_size is one of the predefined page sizes.

margin

The margin to use. See Layout::Style::Quad#set for possible values.

# File lib/hexapdf/composer.rb, line 125
def initialize(page_size: :A4, page_orientation: :portrait, margin: 36) #:yields: composer
  @document = HexaPDF::Document.new
  @page_size = page_size
  @page_orientation = page_orientation
  @margin = Layout::Style::Quad.new(margin)

  new_page
  @base_style = Layout::Style.new(font: 'Times')
  yield(self) if block_given?
end

Public Instance Methods

draw_box(box) click to toggle source

Draws the given Layout::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.

# File lib/hexapdf/composer.rb, line 257
def draw_box(box)
  drawn_on_page = true
  while true
    if @frame.fit(box)
      @frame.draw(@canvas, box)
      break
    elsif @frame.full?
      new_page
      drawn_on_page = false
    else
      draw_box, box = @frame.split(box)
      if draw_box
        @frame.draw(@canvas, draw_box)
        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
end
formatted_text(data, width: 0, height: 0, style: nil, **options) click to toggle source

Draws text like text but where parts of it can be formatted differently.

The argument data needs to be an array of String or Hash objects:

  • A String object is treated like {text: data}.

  • Hashes can contain any style properties and the following special keys:

    text

    The text to be formatted.

    link

    A URL that should be linked to. If no text is provided but a link, the link is used as text.

    style

    A Layout::Style object to use as basis instead of the style created from the style and options arguments.

    If any style properties are set, the used style is copied and the additional properties applied.

Examples:

composer.formatted_text(["Some string"])   # The same as #text
composer.formatted_text(["Some ", {text: "string", fill_color: 128}]
composer.formatted_text(["Some ", {link: "https://example.com", text: "Example"}])
composer.formatted_text(["Some ", {text: "string", style: my_style}])
# File lib/hexapdf/composer.rb, line 220
def formatted_text(data, width: 0, height: 0, style: nil, **options)
  style = update_style(style, options)
  data.map! do |hash|
    if hash.kind_of?(String)
      Layout::TextFragment.create(hash, style)
    else
      link = hash.delete(:link)
      text = hash.delete(:text) || link || ""
      used_style = update_style(hash.delete(:style), options) || style
      if link || !hash.empty?
        used_style = used_style.dup
        hash.each {|key, value| used_style.send(key, value) }
        used_style.overlays.add(:link, uri: link) if link
      end
      Layout::TextFragment.create(text, used_style)
    end
  end
  draw_box(Layout::TextBox.new(data, width: width, height: height, style: style))
end
image(file, width: 0, height: 0, style: nil, **options) click to toggle source

Draws the given image file at the current position.

See text for details on width, height, style and options.

# File lib/hexapdf/composer.rb, line 243
def image(file, width: 0, height: 0, style: nil, **options)
  style = update_style(style, options)
  image = document.images.add(file)
  draw_box(Layout::ImageBox.new(image, width: width, height: height, style: style))
end
new_page(page_size: nil, page_orientation: nil, margin: nil) click to toggle source

Creates a new page, making it the current one.

If any of page_size, page_orientation or margin are set, they will be used instead of the default values and will become the default values.

Examples:

composer.new_page  # uses the default values
composer.new_page(page_size: :A5, margin: [72, 36])
# File lib/hexapdf/composer.rb, line 145
def new_page(page_size: nil, page_orientation: nil, margin: nil)
  @page_size = page_size if page_size
  @page_orientation = page_orientation if page_orientation
  @margin = Layout::Style::Quad.new(margin) if margin

  @page = @document.pages.add(@page_size, orientation: @page_orientation)
  @canvas = @page.canvas
  create_frame
end
text(str, width: 0, height: 0, style: nil, **options) click to toggle source

Draws the given text at the current position into the current frame.

This method is the main method for displaying text on a PDF page. It uses a Layout::TextBox behind the scenes to do the actual work.

The text will be positioned at the current position if possible. Otherwise the next best position is used. If the text doesn't fit onto the current page or only partially, new pages are created automatically.

The arguments width and height are used as constraints and are respected when fitting the box.

The text is styled using the given style object (see Layout::Style) or, if no style object is specified, the base style (see base_style). If any additional style options are specified, the used style is copied and the additional styles are applied.

See HexaPDF::Layout::TextBox for details.

# File lib/hexapdf/composer.rb, line 189
def text(str, width: 0, height: 0, style: nil, **options)
  style = update_style(style, options)
  draw_box(Layout::TextBox.new([Layout::TextFragment.create(str, style)],
                               width: width, height: height, style: style))
end
write(output, optimize: true, **options) click to toggle source

Writes the PDF document to the given output.

See Document#write for details.

# File lib/hexapdf/composer.rb, line 168
def write(output, optimize: true, **options)
  @document.write(output, optimize: optimize, **options)
end
x() click to toggle source

The x-position of the cursor inside the current frame.

# File lib/hexapdf/composer.rb, line 156
def x
  @frame.x
end
y() click to toggle source

The y-position of the cursor inside the current frame.

# File lib/hexapdf/composer.rb, line 161
def y
  @frame.y
end

Private Instance Methods

create_frame() click to toggle source

Creates the frame into which boxes are layed out when a new page is created.

# File lib/hexapdf/composer.rb, line 285
def create_frame
  media_box = @page.box
  @frame = Layout::Frame.new(media_box.left + @margin.left,
                             media_box.bottom + @margin.bottom,
                             media_box.width - @margin.left - @margin.right,
                             media_box.height - @margin.bottom - @margin.top)
end
update_style(style, options = {}) click to toggle source

Updates the Layout::Style object style if one is provided, or the base style, with the style options to make it work in all cases.

# File lib/hexapdf/composer.rb, line 295
def update_style(style, options = {})
  style ||= base_style
  style = style.dup.update(**options) unless options.empty?
  style.font(base_style.font) unless style.font?
  style.font(@document.fonts.add(style.font)) unless style.font.respond_to?(:pdf_object)
  style
end