class HexaPDF::Revision

Embodies one revision of a PDF file, either the initial version or an incremental update.

The purpose of a Revision object is to manage the objects and the trailer of one revision. These objects can either be added manually or loaded from a cross-reference section or stream. Since a PDF file can be incrementally updated, it can have multiple revisions.

If a revision doesn't have an associated cross-reference section, it wasn't created from a PDF file.

See: PDF1.7 s7.5.6, Revisions

Attributes

loader[RW]

The callable object responsible for loading objects.

trailer[R]

The trailer dictionary

Public Class Methods

new(trailer) → revision click to toggle source
new(trailer, xref_section: section, loader: loader) → revision
new(trailer, xref_section: section) {|entry| block } → revision

Creates a new Revision object.

Options:

xref_section

An XRefSection object that contains information on how to load objects. If this option is specified, then a loader or a block also needs to be specified!

loader

The loader object needs to respond to call taking a cross-reference entry and returning the loaded object. If no xref_section is supplied, this value is not used.

If a block is given, it is used instead of the loader object.

# File lib/hexapdf/revision.rb, line 79
def initialize(trailer, xref_section: nil, loader: nil, &block)
  @trailer = trailer
  @loader = xref_section && (block || loader)
  @xref_section = xref_section || XRefSection.new
  @objects = HexaPDF::Utils::ObjectHash.new
end

Public Instance Methods

add(obj) → obj click to toggle source

Adds the given object (needs to be a HexaPDF::Object) to this revision and returns it.

# File lib/hexapdf/revision.rb, line 151
def add(obj)
  if object?(obj.oid)
    raise HexaPDF::Error, "A revision can only contain one object with a given object number"
  elsif !obj.indirect?
    raise HexaPDF::Error, "A revision can only contain indirect objects"
  end
  add_without_check(obj)
end
delete(ref, mark_as_free: true) click to toggle source
delete(oid, mark_as_free: true)

Deletes the object specified either by reference or by object number from this revision by marking it as free.

If the mark_as_free option is set to false, the object is really deleted.

# File lib/hexapdf/revision.rb, line 184
def delete(ref_or_oid, mark_as_free: true)
  return unless object?(ref_or_oid)
  ref_or_oid = ref_or_oid.oid if ref_or_oid.respond_to?(:oid)

  obj = object(ref_or_oid)
  obj.data.value = nil
  obj.document = nil
  if mark_as_free
    add_without_check(HexaPDF::Object.new(nil, oid: obj.oid, gen: obj.gen))
  else
    @xref_section.delete(ref_or_oid)
    @objects.delete(ref_or_oid)
  end
end
each(only_loaded: false) {|obj| block } → revision click to toggle source
each(only_loaded: false) → Enumerator

Calls the given block for every object of the revision, or, if only_loaded is true, for every already loaded object.

Objects that are loadable via an associated cross-reference section but are currently not loaded, are loaded automatically if only_loaded is false.

# File lib/hexapdf/revision.rb, line 208
def each(only_loaded: false)
  return to_enum(__method__, only_loaded: only_loaded) unless block_given?

  if defined?(@all_objects_loaded) || only_loaded
    @objects.each {|_oid, _gen, data| yield(data) }
  else
    seen = {}
    @objects.each {|oid, _gen, data| seen[oid] = true; yield(data) }
    @xref_section.each do |oid, _gen, data|
      next if seen.key?(oid)
      yield(@objects[oid] || load_object(data))
    end
    @all_objects_loaded = true
  end

  self
end
each_modified_object {|obj| block } → revision click to toggle source
each_modified_object → Enumerator

Calls the given block once for each object that has been modified since it was loaded.

Note that this also means that for revisions without an associated cross-reference section all loaded objects will be yielded.

# File lib/hexapdf/revision.rb, line 234
def each_modified_object
  return to_enum(__method__) unless block_given?

  @objects.each do |oid, gen, obj|
    if @xref_section.entry?(oid, gen)
      stored_obj = @loader.call(@xref_section[oid, gen])
      if obj.data.value != stored_obj.data.value || obj.data.stream != stored_obj.data.stream
        yield(obj)
      end
    else
      yield(obj)
    end
  end

  self
end
next_free_oid() click to toggle source

Returns the next free object number for adding an object to this revision.

# File lib/hexapdf/revision.rb, line 87
def next_free_oid
  ((a = @xref_section.max_oid) < (b = @objects.max_oid) ? b : a) + 1
end
object(ref) → obj or nil click to toggle source
object(oid) → obj or nil

Returns the object for the given reference or object number if such an object is available in this revision, or nil otherwise.

If the revision has an entry but one that is pointing to a free entry in the cross-reference section, an object representing PDF null is returned.

# File lib/hexapdf/revision.rb, line 114
def object(ref)
  if ref.respond_to?(:oid)
    oid = ref.oid
    gen = ref.gen
  else
    oid = ref
  end

  if @objects.entry?(oid, gen)
    @objects[oid, gen]
  elsif (xref_entry = @xref_section[oid, gen])
    load_object(xref_entry)
  else
    nil
  end
end
object?(ref) → true or false click to toggle source
object?(oid) → true or false

Returns true if the revision contains an object

  • for the exact reference if the argument responds to :oid, or else

  • for the given object number.

# File lib/hexapdf/revision.rb, line 139
def object?(ref)
  if ref.respond_to?(:oid)
    @objects.entry?(ref.oid, ref.gen) || @xref_section.entry?(ref.oid, ref.gen)
  else
    @objects.entry?(ref) || @xref_section.entry?(ref)
  end
end
update(obj) → obj or nil click to toggle source

Updates the stored object to point to the given HexaPDF::Object wrapper, returning the object if successful or nil otherwise.

If obj isn't stored in this revision or the stored object doesn't contain the same HexaPDF::PDFData object as the given object, nothing is done.

This method should only be used if the wrong wrapper class is stored (e.g. because auto-detection didn't or couldn't work correctly) and thus needs correction.

# File lib/hexapdf/revision.rb, line 171
def update(obj)
  return nil if object(obj)&.data != obj.data
  add_without_check(obj)
end
xref(ref) → xref_entry or nil click to toggle source
xref(oid) → xref_entry or nil

Returns an XRefSection::Entry structure for the given reference or object number if it is available, or nil otherwise.

# File lib/hexapdf/revision.rb, line 97
def xref(ref)
  if ref.respond_to?(:oid)
    @xref_section[ref.oid, ref.gen]
  else
    @xref_section[ref, nil]
  end
end

Private Instance Methods

add_without_check(obj) click to toggle source

Adds the object to the available objects of this revision and returns it.

# File lib/hexapdf/revision.rb, line 259
def add_without_check(obj)
  @objects[obj.oid, obj.gen] = obj
end
load_object(xref_entry) click to toggle source

Loads a single object from the associated cross-reference section.

# File lib/hexapdf/revision.rb, line 254
def load_object(xref_entry)
  add_without_check(@loader.call(xref_entry))
end