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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

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

Yields:

  • (_self)

Yield Parameters:



159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/hexapdf/composer.rb', line 159

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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, **kwargs, &block) ⇒ Object

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



422
423
424
425
426
427
428
# File 'lib/hexapdf/composer.rb', line 422

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

Instance Attribute Details

#canvasObject (readonly)

The canvas instance (a Content::Canvas object) of the current page.

Can be used to perform arbitrary drawing operations.



109
110
111
# File 'lib/hexapdf/composer.rb', line 109

def canvas
  @canvas
end

#documentObject (readonly)

The PDF document (HexaPDF::Document) that is created.



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

def document
  @document
end

#frameObject (readonly)

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



112
113
114
# File 'lib/hexapdf/composer.rb', line 112

def frame
  @frame
end

#pageObject (readonly)

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



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

def page
  @page
end

Class Method Details

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

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


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

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



402
403
404
# File 'lib/hexapdf/composer.rb', line 402

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("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)

Yields:



485
486
487
488
489
# File 'lib/hexapdf/composer.rb', line 485

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



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/hexapdf/composer.rb', line 442

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
        (box = draw_box; break) unless box
      elsif !@frame.find_next_region
        unless drawn_on_page
          raise HexaPDF::Error, "Box didn't fit multiple times, even on empty page"
        end
        new_page
        drawn_on_page = false
      end
    end
  end
  box
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 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



368
369
370
371
# File 'lib/hexapdf/composer.rb', line 368

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



386
387
388
389
# File 'lib/hexapdf/composer.rb', line 386

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(style = @next_page_style) ⇒ Object

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


191
192
193
194
195
196
197
198
199
# File 'lib/hexapdf/composer.rb', line 191

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

#page_style(name, **attributes, &block) ⇒ Object

: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



312
313
314
315
316
317
318
# File 'lib/hexapdf/composer.rb', line 312

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

#respond_to_missing?(name, _private) ⇒ Boolean

:nodoc:

Returns:

  • (Boolean)


430
431
432
# File 'lib/hexapdf/composer.rb', line 430

def respond_to_missing?(name, _private) # :nodoc:
  @document.layout.box_creation_method?(name) || super
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.

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



260
261
262
# File 'lib/hexapdf/composer.rb', line 260

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

#styles(**mapping) ⇒ Object

:call-seq:

composer.styles              -> styles
composer.styles(**mapping)   -> styles

Creates multiple named styles at once if mapping is provided, and returns the style mapping.

See HexaPDF::Document::Layout#styles for details; this method is just a thin wrapper around that method.

Example:

composer.styles(
  base: {font_size: 12, leading: 1.2},
  header: {font: 'Helvetica', fill_color: "008"},
  header1: {base: :header, font_size: 30}
)

See: HexaPDF::Layout::Style



282
283
284
# File 'lib/hexapdf/composer.rb', line 282

def styles(**mapping)
  @document.layout.styles(**mapping)
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 (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



343
344
345
346
# File 'lib/hexapdf/composer.rb', line 343

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 created PDF document to the given output.

See HexaPDF::Document#write for details.



230
231
232
# File 'lib/hexapdf/composer.rb', line 230

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

#write_to_string(optimize: true, **options) ⇒ Object

Writes the created PDF document to a string and returns that string.

See HexaPDF::Document#write for details.



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

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

#xObject

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


210
211
212
# File 'lib/hexapdf/composer.rb', line 210

def x
  @frame.x
end

#yObject

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


223
224
225
# File 'lib/hexapdf/composer.rb', line 223

def y
  @frame.y
end