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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document, initial_revisions: nil, parser: 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.

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.



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

def initialize(document, initial_revisions: nil, parser: nil)
  @document = document
  @parser = parser

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

Instance Attribute Details

#parserObject (readonly)

The Parser instance used for reading the initial revisions.



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

def parser
  @parser
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.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/hexapdf/revisions.rb', line 63

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

Instance Method Details

#addObject

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



153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/hexapdf/revisions.rb', line 153

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.



143
144
145
# File 'lib/hexapdf/revisions.rb', line 143

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!



179
180
181
182
183
184
185
186
187
# File 'lib/hexapdf/revisions.rb', line 179

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.



214
215
216
217
218
# File 'lib/hexapdf/revisions.rb', line 214

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.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/hexapdf/revisions.rb', line 194

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.



137
138
139
# File 'lib/hexapdf/revisions.rb', line 137

def revision(index)
  @revisions[index]
end

#sizeObject

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



148
149
150
# File 'lib/hexapdf/revisions.rb', line 148

def size
  @revisions.size
end