# -*- encoding: utf-8; frozen_string_literal: true -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2014-2019 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON # INFRINGEMENT OF THIRD PARTY RIGHTS. # # HexaPDF is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with HexaPDF. If not, see . # # The interactive user interfaces in modified source and object code # versions of HexaPDF must display Appropriate Legal Notices, as required # under Section 5 of the GNU Affero General Public License version 3. # # In accordance with Section 7(b) of the GNU Affero General Public # License, a covered work must retain the producer line in every PDF that # is created or manipulated using HexaPDF. # # If the GNU Affero General Public License doesn't fit your need, # commercial licenses are available at . #++ require 'hexapdf/error' require 'hexapdf/parser' require 'hexapdf/revision' require 'hexapdf/type/trailer' module HexaPDF # 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 Revisions class << self # 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. 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) 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 document.version = parser.file_header_version new(document, initial_revisions: revisions, parser: parser) end end include Enumerable # The Parser instance used for reading the initial revisions. attr_reader :parser # 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. def initialize(document, initial_revisions: nil, parser: nil) @document = document @parser = parser @revisions = [] if initial_revisions @revisions += initial_revisions else add end end # Returns the revision at the specified index. def revision(index) @revisions[index] end alias [] revision # Returns the current revision. def current @revisions.last end # Returns the number of HexaPDF::Revision objects managed by this object. def size @revisions.size end # Adds a new empty revision to the document and returns it. 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 # :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! 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 # :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. 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 # :call-seq: # revisions.each {|rev| block } -> revisions # revisions.each -> Enumerator # # Iterates over all revisions from oldest to current one. def each(&block) return to_enum(__method__) unless block_given? @revisions.each(&block) self end end end