Class: HexaPDF::Revisions

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/hexapdf/revisions.rb

Overview

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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document, initial_revisions: nil) ⇒ Revisions

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.



98
99
100
101
102
103
104
105
106
# File 'lib/hexapdf/revisions.rb', line 98

def initialize(document, initial_revisions: nil)
  @document = document
  @revisions = []
  if initial_revisions
    @revisions += initial_revisions
  else
    add
  end
end

Class Method Details

.from_io(document, io) ⇒ Object

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.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/hexapdf/revisions.rb', line 60

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 = []
  xref_section, trailer = parser.load_revision(parser.startxref_offset)
  revisions << Revision.new(document.wrap(trailer, type: :XXTrailer),
                            xref_section: xref_section, loader: object_loader)


  while (prev = revisions[0].trailer.value[: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)
    stm = revisions[0].trailer.value[:XRefStm]
    stm_xref_section, = parser.load_revision(stm) if stm
    xref_section.merge!(stm_xref_section) if stm
    revisions.unshift(Revision.new(document.wrap(trailer, type: :XXTrailer),
                                   xref_section: xref_section, loader: object_loader))
  end

  document.version = parser.file_header_version
  new(document, initial_revisions: revisions)
end

Instance Method Details

#addObject

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



125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/hexapdf/revisions.rb', line 125

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

#currentObject

Returns the current revision.



115
116
117
# File 'lib/hexapdf/revisions.rb', line 115

def current
  @revisions.last
end

#delete(index_or_rev) ⇒ Object

:call-seq:

revisions.delete(index)    -> rev or nil
revisions.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!



151
152
153
154
155
156
157
158
159
# File 'lib/hexapdf/revisions.rb', line 151

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(&block) ⇒ Object

:call-seq:

revisions.each {|rev| block }   -> revisions
revisions.each                  -> Enumerator

Iterates over all revisions from oldest to current one.



186
187
188
189
190
# File 'lib/hexapdf/revisions.rb', line 186

def each(&block)
  return to_enum(__method__) unless block_given?
  @revisions.each(&block)
  self
end

#merge(range = 0..-1)) ⇒ Object

:call-seq:

revisions.merge(range = 0..-1)    -> revisions

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



166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/hexapdf/revisions.rb', line 166

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) ⇒ Object Also known as: []

Returns the revision at the specified index.



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

def revision(index)
  @revisions[index]
end

#sizeObject

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



120
121
122
# File 'lib/hexapdf/revisions.rb', line 120

def size
  @revisions.size
end