class HexaPDF::Revisions

Manages the revisions of a PDF document.

A PDF document has one revision when it is created. Later, new revisions are added when changes are made. This allows for adding information/content to a PDF file without changing the original content.

The order of the revisions is important. In HexaPDF the oldest revision always has index 0 and the newest revision the highest index. This is also the order in which the revisions get written.

See: PDF1.7 s7.5.6, HexaPDF::Revision

Attributes

parser[R]

The Parser instance used for reading the initial revisions.

Public Class Methods

from_io(document, io) click to toggle source

Loads all revisions for the document from the given IO and returns the created Revisions object.

If the io object is nil, an empty Revisions object is returned.

# File lib/hexapdf/revisions.rb, line 62
def from_io(document, io)
  return new(document) if io.nil?

  parser = Parser.new(io, document)
  object_loader = lambda {|xref_entry| parser.load_object(xref_entry) }

  revisions = []
  begin
    xref_section, trailer = parser.load_revision(parser.startxref_offset)
    revisions << Revision.new(document.wrap(trailer, type: :XXTrailer),
                              xref_section: xref_section, loader: object_loader)
    seen_xref_offsets = {parser.startxref_offset => true}

    while (prev = revisions[0].trailer.value[:Prev]) &&
        !seen_xref_offsets.key?(prev)
      # PDF1.7 s7.5.5 states that :Prev needs to be indirect, Adobe's reference 3.4.4 says it
      # should be direct. Adobe's POV is followed here. Same with :XRefStm.
      xref_section, trailer = parser.load_revision(prev)
      seen_xref_offsets[prev] = true

      stm = revisions[0].trailer.value[:XRefStm]
      if stm && !seen_xref_offsets.key?(stm)
        stm_xref_section, = parser.load_revision(stm)
        xref_section.merge!(stm_xref_section)
        seen_xref_offsets[stm] = true
      end

      revisions.unshift(Revision.new(document.wrap(trailer, type: :XXTrailer),
                                     xref_section: xref_section, loader: object_loader))
    end
  rescue HexaPDF::MalformedPDFError
    reconstructed_revision = parser.reconstructed_revision
    unless revisions.empty?
      reconstructed_revision.trailer.data.value = revisions.last.trailer.data.value
    end
    revisions << reconstructed_revision
  end

  document.version = parser.file_header_version rescue '1.0'
  new(document, initial_revisions: revisions, parser: parser)
end
new(document, initial_revisions: nil, parser: nil) click to toggle source

Creates a new revisions object for the given PDF document.

Options:

initial_revisions

An array of revisions that should initially be used. If this option is not specified, a single empty revision is added.

parser

The parser with which the initial revisions were read. If this option is not specified even though the document was read from an IO stream, some parts may not work, like incremental writing.

# File lib/hexapdf/revisions.rb, line 123
def initialize(document, initial_revisions: nil, parser: nil)
  @document = document
  @parser = parser

  @revisions = []
  if initial_revisions
    @revisions += initial_revisions
  else
    add
  end
end

Public Instance Methods

[](index)
Alias for: revision
add() click to toggle source

Adds a new empty revision to the document and returns it.

# File lib/hexapdf/revisions.rb, line 152
def add
  if @revisions.empty?
    trailer = {}
  else
    trailer = current.trailer.value.dup
    trailer.delete(:Prev)
    trailer.delete(:XRefStm)
  end

  rev = Revision.new(@document.wrap(trailer, type: :XXTrailer))
  @revisions.push(rev)
  rev
end
current() click to toggle source

Returns the current revision.

# File lib/hexapdf/revisions.rb, line 142
def current
  @revisions.last
end
delete(index) → rev or nil click to toggle source
delete(oid) → rev or nil

Deletes a revision from the document, either by index or by specifying the revision object itself.

Returns the deleted revision object, or nil if the index was out of range or no matching revision was found.

Regarding the index: The oldest revision has index 0 and the current revision the highest index!

# File lib/hexapdf/revisions.rb, line 178
def delete(index_or_rev)
  if @revisions.length == 1
    raise HexaPDF::Error, "A document must have a least one revision, can't delete last one"
  elsif index_or_rev.kind_of?(Integer)
    @revisions.delete_at(index_or_rev)
  else
    @revisions.delete(index_or_rev)
  end
end
each {|rev| block } → revisions click to toggle source
each → Enumerator

Iterates over all revisions from oldest to current one.

# File lib/hexapdf/revisions.rb, line 213
def each(&block)
  return to_enum(__method__) unless block_given?
  @revisions.each(&block)
  self
end
merge(range = 0..-1) → revisions click to toggle source

Merges the revisions specified by the given range into one. Objects from newer revisions overwrite those from older ones.

# File lib/hexapdf/revisions.rb, line 193
def merge(range = 0..-1)
  @revisions[range].reverse.each_cons(2) do |rev, prev_rev|
    prev_rev.trailer.value.replace(rev.trailer.value)
    rev.each do |obj|
      if obj.data != prev_rev.object(obj)&.data
        prev_rev.delete(obj.oid, mark_as_free: false)
        prev_rev.add(obj)
      end
    end
  end
  _first, *other = *@revisions[range]
  other.each {|rev| @revisions.delete(rev) }
  self
end
revision(index) click to toggle source

Returns the revision at the specified index.

# File lib/hexapdf/revisions.rb, line 136
def revision(index)
  @revisions[index]
end
Also aliased as: []
size() click to toggle source

Returns the number of HexaPDF::Revision objects managed by this object.

# File lib/hexapdf/revisions.rb, line 147
def size
  @revisions.size
end