# -*- 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-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):
# 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/error'
module HexaPDF
class Document
# This class provides methods for managing the pages of a PDF file.
#
# For page manipulation it uses the methods of HexaPDF::Type::PageTreeNode underneath but
# provides a more convenient interface.
#
# == Page Labels
#
# In addition to page manipulation, the class provides methods for managing the page labels
# which are alternative descriptions for the pages. In contrast to the page indices which are
# fixed the page labels can be freely defined.
#
# The way this works is that one can assign page label objects (HexaPDF::Type::PageLabel) to
# page ranges via the /PageLabels number tree in the catalog. The page label objects specify how
# the pages in their range shall be labeled. See HexaPDF::Type::PageLabel for examples of page
# labels.
#
# To facilitate the easy use of page labels the following methods are provided:
#
# * #page_label
# * #each_labelling_range
# * #add_labelling_range
# * #delete_labelling_range
class Pages
include Enumerable
# Creates a new Pages object for the given PDF document.
def initialize(document)
@document = document
end
# Returns the root of the page tree, a HexaPDF::Type::PageTreeNode object.
def root
@document.catalog.pages
end
# Creates a page object and returns it *without* adding it to the page tree.
#
# +media_box+::
# If this argument is +nil+/not specified, the value is taken from the configuration
# option 'page.default_media_box'.
#
# If the resulting value is an array with four numbers (specifying the media box), the new
# page will have these exact dimensions.
#
# If the value is a symbol, it is taken as a reference to a pre-defined media box in
# HexaPDF::Type::Page::PAPER_SIZE. The +orientation+ can then be used to specify the page
# orientation.
#
# +orientation+::
# If this argument is not specified, it is taken from 'page.default_media_orientation'. It
# is only used if +media_box+ is a symbol and not an array.
def create(media_box: nil, orientation: nil)
media_box ||= @document.config['page.default_media_box']
orientation ||= @document.config['page.default_media_orientation']
box = Type::Page.media_box(media_box, orientation: orientation)
@document.add({Type: :Page, MediaBox: box})
end
# :call-seq:
# pages.add -> new_page
# pages.add(page) -> page
# pages.add(media_box, orientation: nil) -> new_page
#
# Adds the given page or a new empty page at the end and returns it.
#
# If called with a page object as argument, that page object is used. Otherwise #create is
# called with the arguments +media_box+ and +orientation+ to create a new page.
def add(page = nil, orientation: nil)
unless page.kind_of?(HexaPDF::Type::Page)
page = create(media_box: page, orientation: orientation)
end
@document.catalog.pages.add_page(page)
end
# :call-seq:
# pages << page -> pages
#
# Appends the given page at the end and returns the pages object itself to allow chaining.
def <<(page)
add(page)
self
end
# Inserts the page or a new empty page at the zero-based index and returns it.
#
# Negative indices count backwards from the end, i.e. -1 is the last page. When using
# negative indices, the page will be inserted after that element. So using an index of -1
# will insert the page after the last page.
def insert(index, page = nil)
@document.catalog.pages.insert_page(index, page)
end
# :call-seq:
# pages.move(page, to_index)
# pages.move(index, to_index)
#
# Moves the given page or the page at the position specified by the zero-based index to the
# +to_index+ position.
#
# If the page that should be moved, doesn't exist or is invalid, an error is raised.
#
# Negative indices count backwards from the end, i.e. -1 is the last page. When using a
# negative index, the page will be moved after that element. So using an index of -1 will
# move the page after the last page.
def move(page, to_index)
@document.catalog.pages.move_page(page, to_index)
end
# Deletes the given page object from the document's page tree and the document.
#
# Also see: HexaPDF::Type::PageTreeNode#delete_page
def delete(page)
@document.catalog.pages.delete_page(page)
end
# Deletes the page object at the given index from the document's page tree and the document.
#
# Also see: HexaPDF::Type::PageTreeNode#delete_page
def delete_at(index)
@document.catalog.pages.delete_page(index)
end
# Returns the page for the zero-based index, or +nil+ if no such page exists.
#
# Negative indices count backwards from the end, i.e. -1 is the last page.
def [](index)
@document.catalog.pages.page(index)
end
# :call-seq:
# pages.each {|page| block } -> pages
# pages.each -> Enumerator
#
# Iterates over all pages inorder.
def each(&block)
@document.catalog.pages.each_page(&block)
end
# Returns the number of pages in the PDF document. May be zero if the document has no pages.
def count
@document.catalog.pages.page_count
end
alias size count
alias length count
# Returns the constructed page label for the given page index.
#
# If no page labels are defined, +nil+ is returned.
#
# See HexaPDF::Type::PageLabel for examples.
def page_label(page_index)
raise(ArgumentError, 'Page index out of range') if page_index < 0 || page_index >= count
each_labelling_range do |index, count, label|
if page_index < index + count
return label.construct_label(page_index - index)
end
end
end
# :call-seq:
# pages.each_labelling_range {|first_index, count, page_label| block } -> pages
# pages.each_labelling_range -> Enumerator
#
# Iterates over all defined labelling ranges inorder, yielding the page index of the first
# page in the labelling range, the number of pages in the range, and the associated page label
# object.
#
# The last yielded count might be equal or lower than zero in case the document has fewer
# pages than anticipated by the labelling ranges.
def each_labelling_range
return to_enum(__method__) unless block_given?
return unless @document.catalog.page_labels
last_start = nil
last_label = nil
@document.catalog.page_labels.each_entry do |s1, p1|
yield(last_start, s1 - last_start, @document.wrap(last_label, type: :PageLabel)) if last_start
last_start = s1
last_label = p1
end
if last_start
yield(last_start, count - last_start, @document.wrap(last_label, type: :PageLabel))
end
self
end
# Adds a new labelling range starting at +start_index+ and returns it.
#
# See HexaPDF::Type::PageLabel for information on the arguments +numbering_style+, +prefix+,
# and +start_number+.
#
# If a labelling range already exists for the given +start_index+, its value will be
# overwritten.
#
# If there are no existing labelling ranges and the given +start_index+ isn't 0, a default
# labelling range using start index 0 and numbering style :decimal is added.
def add_labelling_range(start_index, numbering_style: nil, prefix: nil, start_number: nil)
page_label = @document.wrap({}, type: :PageLabel)
page_label.numbering_style(numbering_style) if numbering_style
page_label.prefix(prefix) if prefix
page_label.start_number(start_number) if start_number
labels = @document.catalog.page_labels(create: true)
labels.add_entry(start_index, page_label)
labels.add_entry(0, {S: :d}) unless labels.find_entry(0)
page_label
end
# Deletes the page labelling range starting at +start_index+ and returns the associated page
# label object.
#
# Note: The page label for the range starting at zero can only be deleted last!
def delete_labelling_range(start_index)
return unless (labels = @document.catalog.page_labels)
if start_index == 0 && labels.each_entry.first(2).size == 2
raise HexaPDF::Error, "Page labelling range starting at 0 must be deleted last"
end
page_label = labels.delete_entry(start_index)
@document.catalog.delete(:PageLabels) if start_index == 0
page_label
end
end
end
end