Class: HexaPDF::Composer

Inherits:
Object
  • Object
show all
Defined in:
lib/hexapdf/composer.rb

Overview

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 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.

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(page_size: :A4, page_orientation: :portrait, margin: 36) {|_self| ... } ⇒ Composer

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 HexaPDF::Layout::Style::Quad#set for possible values.

Example:

composer = HexaPDF::Composer.new            # uses the default values
HexaPDF::Composer.new(page_size: :Letter, margin: 72) do |composer|
  #...
end

Yields:

  • (_self)

Yield Parameters:



126
127
128
129
130
131
132
133
134
# File 'lib/hexapdf/composer.rb', line 126

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
  yield(self) if block_given?
end

Instance Attribute Details

#canvasObject (readonly)

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



102
103
104
# File 'lib/hexapdf/composer.rb', line 102

def canvas
  @canvas
end

#documentObject (readonly)

The PDF document that is created.



96
97
98
# File 'lib/hexapdf/composer.rb', line 96

def document
  @document
end

#frameObject (readonly)

The HexaPDF::Layout::Frame for automatic box placement.



105
106
107
# File 'lib/hexapdf/composer.rb', line 105

def frame
  @frame
end

#pageObject (readonly)

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



99
100
101
# File 'lib/hexapdf/composer.rb', line 99

def page
  @page
end

Class Method Details

.create(output, **options, &block) ⇒ Object

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


91
92
93
# File 'lib/hexapdf/composer.rb', line 91

def self.create(output, **options, &block)
  new(**options, &block).write(output)
end

Instance Method Details

#box(name, width: 0, height: 0, style: nil, **box_options, &block) ⇒ Object

Draws the named box at the current position.

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



269
270
271
# File 'lib/hexapdf/composer.rb', line 269

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

#create_stamp(width, height) {|stamp.canvas| ... } ⇒ Object

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("red").line_width(5).
    rectangle(10, 10, 30, 30).fill_stroke
end
composer.image(stamp, width: 20, height: 20)
composer.image(stamp, width: 50)

Yields:



322
323
324
325
326
# File 'lib/hexapdf/composer.rb', line 322

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

#draw_box(box) ⇒ Object

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



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/hexapdf/composer.rb', line 281

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
end

#formatted_text(data, width: 0, height: 0, style: nil, box_style: nil, **style_properties) ⇒ Object

Draws text like #text but allows parts of the text to be formatted differently.

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: 128}])
composer.formatted_text(["Some ", {link: "https://example.com",
                                   fill_color: 'blue', text: "Example"}])
composer.formatted_text(["Some ", {text: "string", style: {font_size: 20}}])

See: #text, HexaPDF::Layout::TextBox, HexaPDF::Layout::TextFragment



235
236
237
238
# File 'lib/hexapdf/composer.rb', line 235

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

#image(file, width: 0, height: 0, style: nil, **style_properties) ⇒ Object

Draws the given image at the current position.

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



253
254
255
256
# File 'lib/hexapdf/composer.rb', line 253

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

#new_page(page_size: nil, page_orientation: nil, margin: nil) ⇒ Object

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])


145
146
147
148
149
150
151
152
153
# 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

#style(name, base: :base, **properties) ⇒ Object

: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.

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



189
190
191
# File 'lib/hexapdf/composer.rb', line 189

def style(name, base: :base, **properties)
  @document.layout.style(name, base: base, **properties)
end

#text(str, width: 0, height: 0, style: nil, box_style: nil, **style_properties) ⇒ Object

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

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.

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 " * 15)
composer.text("Now " * 7, width: 100)
composer.text("Another test", font_size: 15, fill_color: "green")
composer.text("Different box style", fill_color: 'white', box_style: {
  underlays: [->(c, b) { c.rectangle(0, 0, b.content_width, b.content_height).fill }]
})


214
215
216
217
# File 'lib/hexapdf/composer.rb', line 214

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

#write(output, optimize: true, **options) ⇒ Object

Writes the PDF document to the given output.

See Document#write for details.



168
169
170
# File 'lib/hexapdf/composer.rb', line 168

def write(output, optimize: true, **options)
  @document.write(output, optimize: optimize, **options)
end

#xObject

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



156
157
158
# File 'lib/hexapdf/composer.rb', line 156

def x
  @frame.x
end

#yObject

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



161
162
163
# File 'lib/hexapdf/composer.rb', line 161

def y
  @frame.y
end