Class: HexaPDF::Revision

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

Overview

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: PDF2.0 s7.5.6, Revisions

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(trailer, xref_section: nil, loader: nil, &block) ⇒ Revision

:call-seq:

Revision.new(trailer)                                           -> revision
Revision.new(trailer, xref_section: section, loader: loader)    -> revision
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.



83
84
85
86
87
88
89
# File 'lib/hexapdf/revision.rb', line 83

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
  @all_objects_loaded = false
end

Instance Attribute Details

#loaderObject

The callable object responsible for loading objects.



60
61
62
# File 'lib/hexapdf/revision.rb', line 60

def loader
  @loader
end

#trailerObject (readonly)

The trailer dictionary



57
58
59
# File 'lib/hexapdf/revision.rb', line 57

def trailer
  @trailer
end

#xref_sectionObject (readonly)

The associated XRefSection object.



63
64
65
# File 'lib/hexapdf/revision.rb', line 63

def xref_section
  @xref_section
end

Instance Method Details

#add(obj) ⇒ Object

:call-seq:

revision.add(obj)   -> obj

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



156
157
158
159
160
161
162
163
# File 'lib/hexapdf/revision.rb', line 156

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_or_oid, mark_as_free: true) ⇒ Object

:call-seq:

revision.delete(ref, mark_as_free: true)
revision.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.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/hexapdf/revision.rb', line 189

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(obj.data))
  else
    @xref_section.delete(ref_or_oid)
    @objects.delete(ref_or_oid)
  end
end

#each(only_loaded: false) ⇒ Object

:call-seq:

revision.each(only_loaded: false) {|obj| block }   -> revision
revision.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.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/hexapdf/revision.rb', line 213

def each(only_loaded: false)
  return to_enum(__method__, only_loaded: only_loaded) unless block_given?

  if @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(delete: false, all: false) ⇒ Object

:call-seq:

revision.each_modified_object(delete: false, all: all) {|obj| block }   -> revision
revision.each_modified_object(delete: false, all: all)                  -> Enumerator

Calls the given block once for each object that has been modified since it was loaded. Added or eleted object and cross-reference streams as well as signature dictionaries are ignored.

delete

If the delete argument is set to true, each modified object is deleted from the active objects.

all

If the all argument is set to true, added object and cross-reference streams are also yielded.

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



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/hexapdf/revision.rb', line 246

def each_modified_object(delete: false, all: false)
  return to_enum(__method__, delete: delete, all: all) unless block_given?

  @objects.each do |oid, gen, obj|
    if @xref_section.entry?(oid, gen)
      stored_obj = @loader.call(@xref_section[oid, gen])
      next if (stored_obj.type == :ObjStm || stored_obj.type == :XRef) && obj.null? ||
        stored_obj.type == :Sig || stored_obj.type == :DocTimeStamp

      streams_are_same = (obj.data.stream == stored_obj.data.stream)
      next if obj.value == stored_obj.value && streams_are_same

      if obj.value.kind_of?(Hash) && stored_obj.value.kind_of?(Hash)
        keys = obj.value.keys | stored_obj.value.keys
        values_unchanged = keys.all? do |key|
          other = stored_obj[key]
          # Force comparison of values if both are indirect objects
          other = other.value if other.kind_of?(Object) && !other.indirect?
          obj[key] == other
        end
        next if values_unchanged && streams_are_same
      end
    elsif !all && (obj.type == :XRef || obj.type == :ObjStm)
      next
    end

    yield(obj)
    @objects.delete(oid) if delete
  end

  self
end

#next_free_oidObject

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



92
93
94
# File 'lib/hexapdf/revision.rb', line 92

def next_free_oid
  ((a = @xref_section.max_oid) < (b = @objects.max_oid) ? b : a) + 1
end

#object(ref) ⇒ Object

:call-seq:

revision.object(ref)    -> obj or nil
revision.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.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/hexapdf/revision.rb', line 119

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) ⇒ Boolean

:call-seq:

revision.object?(ref)    -> true or false
revision.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.

Returns:

  • (Boolean)


144
145
146
147
148
149
150
# File 'lib/hexapdf/revision.rb', line 144

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) ⇒ Object

:call-seq:

revision.update(obj)   -> obj or nil

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.



176
177
178
179
# File 'lib/hexapdf/revision.rb', line 176

def update(obj)
  return nil if object(obj)&.data != obj.data
  add_without_check(obj)
end

#xref(ref) ⇒ Object

:call-seq:

revision.xref(ref)    -> xref_entry or nil
revision.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.



102
103
104
105
106
107
108
# File 'lib/hexapdf/revision.rb', line 102

def xref(ref)
  if ref.respond_to?(:oid)
    @xref_section[ref.oid, ref.gen]
  else
    @xref_section[ref, nil]
  end
end