# -*- encoding : utf-8 -*- # frozen_string_literal: true ######################################################## ## Thoughts from reading the ISO 32000-1:2008 ## this file is part of the CombinePDF library and the code ## is subject to the same license. ######################################################## module CombinePDF class PDF protected include Renderer # RECORSIVE_PROTECTION = { Parent: true, Last: true}.freeze # @private # Some PDF objects contain references to other PDF objects. # # this function adds the references contained in these objects. # # this is used for internal operations, such as injectng data using the << operator. def add_referenced() # an existing object map resolved = {}.compare_by_identity existing = {} should_resolve = [] #set all existing objects as resolved and register their children for future resolution @objects.each { |obj| existing[obj] = obj ; resolved[obj] = obj; should_resolve << obj.values} # loop until should_resolve is empty while should_resolve.any? obj = should_resolve.pop next if resolved[obj] # the object exists if obj.is_a?(Hash) referenced = obj[:referenced_object] if referenced && referenced.any? tmp = resolved[referenced] if !tmp && referenced[:raw_stream_content] tmp = existing[referenced[:raw_stream_content]] # Avoid endless recursion by limiting it to a number of layers (default == 2) tmp = nil unless equal_layers(tmp, referenced) end if tmp obj[:referenced_object] = tmp else resolved[obj] = referenced # existing[referenced] = referenced existing[referenced[:raw_stream_content]] = referenced should_resolve << referenced @objects << referenced end else resolved[obj] = obj obj.keys.each { |k| should_resolve << obj[k] unless !obj[k].is_a?(Enumerable) || resolved[obj[k]] } end elsif obj.is_a?(Array) resolved[obj] = obj should_resolve.concat obj end end resolved.clear existing.clear end # @private def rebuild_catalog(*with_pages) # # build page list v.1 Slow but WORKS # # Benchmark testing value: 26.708394 # old_catalogs = @objects.select {|obj| obj.is_a?(Hash) && obj[:Type] == :Catalog} # old_catalogs ||= [] # page_list = [] # PDFOperations._each_object(old_catalogs,false) { |p| page_list << p if p.is_a?(Hash) && p[:Type] == :Page } # build page list v.2 faster, better, and works # Benchmark testing value: 0.215114 page_list = pages # add pages to catalog, if requested page_list.concat(with_pages) unless with_pages.empty? # duplicate any non-unique pages - This is a special case to resolve Adobe Acrobat Reader issues (see issues #19 and #81) uniqueness = {}.compare_by_identity page_list.each { |page| page = page[:referenced_object] || page; page = page.dup if uniqueness[page]; uniqueness[page] = page } page_list.clear page_list = uniqueness.values uniqueness.clear # build new Pages object page_object_kids = [] pages_object = { Type: :Pages, Count: page_list.length, Kids: page_object_kids } pages_object_reference = { referenced_object: pages_object, is_reference_only: true } page_list.each { |pg| pg[:Parent] = pages_object_reference; page_object_kids << ({ referenced_object: pg, is_reference_only: true }) } # rebuild/rename the names dictionary rebuild_names # build new Catalog object catalog_object = { Type: :Catalog, Pages: { referenced_object: pages_object, is_reference_only: true } } # pages_object[:Parent] = { referenced_object: catalog_object, is_reference_only: true } # causes AcrobatReader to fail catalog_object[:ViewerPreferences] = @viewer_preferences unless @viewer_preferences.empty? # point old Pages pointers to new Pages object ## first point known pages objects - enough? pages.each { |p| p[:Parent] = { referenced_object: pages_object, is_reference_only: true } } ## or should we, go over structure? (fails) # each_object {|obj| obj[:Parent][:referenced_object] = pages_object if obj.is_a?(Hash) && obj[:Parent].is_a?(Hash) && obj[:Parent][:referenced_object] && obj[:Parent][:referenced_object][:Type] == :Pages} # # remove old catalog and pages objects # @objects.reject! { |obj| obj.is_a?(Hash) && (obj[:Type] == :Catalog || obj[:Type] == :Pages) } # remove old objects list and trees @objects.clear # inject new catalog and pages objects @objects << @info if @info @objects << catalog_object # @objects << pages_object # rebuild/rename the forms dictionary if @forms_data.nil? || @forms_data.empty? @forms_data = nil else @forms_data = { referenced_object: (@forms_data[:referenced_object] || @forms_data), is_reference_only: true } catalog_object[:AcroForm] = @forms_data @objects << @forms_data[:referenced_object] end # add the names dictionary if @names && @names.length > 1 @objects << @names catalog_object[:Names] = { referenced_object: @names, is_reference_only: true } end # add the outlines dictionary if @outlines && @outlines.any? @objects << @outlines catalog_object[:Outlines] = { referenced_object: @outlines, is_reference_only: true } end catalog_object end # Deprecation Notice def names_object puts "CombinePDF Deprecation Notice: the protected method `names_object` will be deprecated in the upcoming version. Use `names` instead." @names end def outlines_object puts "CombinePDF Deprecation Notice: the protected method `outlines_object` will be deprecated in the upcoming version. Use `oulines` instead." @outlines end # def forms_data # @forms_data # end # @private # this is an alternative to the rebuild_catalog catalog method # this method is used by the to_pdf method, for streamlining the PDF output. # there is no point is calling the method before preparing the output. def rebuild_catalog_and_objects catalog = rebuild_catalog catalog[:Pages][:referenced_object][:Kids].each { |e| @objects << e[:referenced_object]; e[:referenced_object] } # adds every referenced object to the @objects (root), addition is performed as pointers rather then copies add_referenced() catalog end def get_existing_catalogs (@objects.select { |obj| obj.is_a?(Hash) && obj[:Type] == :Catalog }) || (@objects.select { |obj| obj.is_a?(Hash) && obj[:Type] == :Page }) end # end # @private def renumber_object_ids(start = nil) @set_start_id = start || @set_start_id start = @set_start_id # history = {} @objects.each do |obj| obj[:indirect_reference_id] = start start += 1 end end def remove_old_ids @objects.each { |obj| obj.delete(:indirect_reference_id); obj.delete(:indirect_generation_number) } end POSSIBLE_NAME_TREES = [:Dests, :AP, :Pages, :IDS, :Templates, :URLS, :JavaScript, :EmbeddedFiles, :AlternatePresentations, :Renditions].to_set.freeze def rebuild_names(name_tree = nil, base = 'CombinePDF_0000000') base = +base if name_tree return nil unless name_tree.is_a?(Hash) name_tree = name_tree[:referenced_object] || name_tree dic = [] # map a names tree and return a valid name tree. Do not recourse. should_resolve = [name_tree[:Kids], name_tree[:Names]] resolved = Set.new.compare_by_identity while should_resolve.any? pos = should_resolve.pop if pos.is_a? Array next if resolved.include?(pos) if pos[0].is_a? String (pos.length / 2).times do |i| dic << (pos[i * 2].clear << base.next!) pos[(i * 2) + 1][0] = {is_reference_only: true, referenced_object: pages[pos[(i * 2) + 1][0]]} if(pos[(i * 2) + 1].is_a?(Array) && pos[(i * 2) + 1][0].is_a?(Numeric)) dic << (pos[(i * 2) + 1].is_a?(Array) ? { is_reference_only: true, referenced_object: { indirect_without_dictionary: pos[(i * 2) + 1] } } : pos[(i * 2) + 1]) # dic << pos[(i * 2) + 1] end else should_resolve.concat pos end elsif pos.is_a? Hash pos = pos[:referenced_object] || pos next if resolved.include?(pos) should_resolve << pos[:Kids] if pos[:Kids] should_resolve << pos[:Names] if pos[:Names] end resolved << pos end return { referenced_object: { Names: dic }, is_reference_only: true } end @names ||= @names[:referenced_object] new_names = { Type: :Names } POSSIBLE_NAME_TREES.each do |ntree| if @names[ntree] new_names[ntree] = rebuild_names(@names[ntree], base) @names[ntree].clear end end @names.clear @names = new_names end # @private # this method reviews a Hash and updates it by merging Hash data, # preffering the new over the old. # def self.hash_merge_new_no_page(_key = nil, old_data = nil, new_data = nil) # return old_data unless new_data # return new_data unless old_data # if old_data.is_a?(Hash) && new_data.is_a?(Hash) # return old_data if (old_data[:Type] == :Page) # old_data.merge(new_data, &(@hash_merge_new_no_page_proc ||= method(:hash_merge_new_no_page))) # elsif old_data.is_a? Array # return old_data + new_data if new_data.is_a?(Array) # return old_data.dup << new_data # elsif new_data.is_a? Array # new_data + [old_data] # else # new_data # end # end # @private # JRuby Alternative this method reviews a Hash and updates it by merging Hash data, # preffering the new over the old. HASH_MERGE_NEW_NO_PAGE = Proc.new do |_key = nil, old_data = nil, new_data = nil| if !new_data old_data elsif !old_data new_data elsif old_data.is_a?(Hash) && new_data.is_a?(Hash) if (old_data[:Type] == :Page) old_data else old_data.merge(new_data, &HASH_MERGE_NEW_NO_PAGE) end elsif old_data.is_a? Array if new_data.is_a?(Array) old_data + new_data else old_data.dup << new_data end elsif new_data.is_a? Array new_data + [old_data] else new_data end end # Merges 2 outlines by appending one to the end or start of the other. # old_data - the main outline, which is also the one that will be used in the resulting PDF. # new_data - the outline to be appended # position - an integer representing the position where a PDF is being inserted. # This method only differentiates between inserted at the beginning, or not. # Not at the beginning, means the new outline will be added to the end of the original outline. # An outline base node (tree base) has :Type, :Count, :First, :Last # Every node within the outline base node's :First or :Last can have also have the following pointers to other nodes: # :First or :Last (only if the node has a subtree / subsection) # :Parent (the node's parent) # :Prev, :Next (previous and next node) # Non-node-pointer data in these nodes: # :Title - the node's title displayed in the PDF outline # :Count - Number of nodes in it's subtree (0 if no subtree) # :Dest - node link destination (if the node is linking to something) def merge_outlines(old_data, new_data, position) old_data = actual_object(old_data) new_data = actual_object(new_data) if old_data.nil? || old_data.empty? || old_data[:First].nil? # old_data is a reference to the actual object, # so if we update old_data, we're done, no need to take any further action old_data.update new_data elsif new_data.nil? || new_data.empty? || new_data[:First].nil? return old_data else new_data = new_data.dup # avoid old data corruption # number of outline nodes, after the merge old_data[:Count] = old_data[:Count].to_i + new_data[:Count].to_i # walk the Hash here ... # I'm just using the start / end insert-position for now... # first - is going to be the start of the outline base node's :First, after the merge # last - is going to be the end of the outline base node's :Last, after the merge # median - the start of what will be appended to the end of the outline base node's :First # parent - the outline base node of the resulting merged outline # FIXME implement the possibility to insert somewhere in the middle of the outline prev = nil pos = first = actual_object((position.nonzero? ? old_data : new_data)[:First]) last = actual_object((position.nonzero? ? new_data : old_data)[:Last]) median = { is_reference_only: true, referenced_object: actual_object((position.nonzero? ? new_data : old_data)[:First]) } old_data[:First] = { is_reference_only: true, referenced_object: first } old_data[:Last] = { is_reference_only: true, referenced_object: last } parent = { is_reference_only: true, referenced_object: old_data } while pos # walking through old_data here and updating the :Parent as we go, # this updates the inserted new_data :Parent's as well once it is appended and the # loop keeps walking the appended data. pos[:Parent] = parent if pos[:Parent] # connect the two outlines # if there is no :Next, the end of the outline base node's :First is reached and this is # where the new data gets appended, the same way you would append to a two-way linked list. if pos[:Next].nil? median[:referenced_object][:Prev] = { is_reference_only: true, referenced_object: prev } if median pos[:Next] = median # midian becomes 'nil' because this loop keeps going after the appending is done, # to update the parents of the appended tree and we wouldn't want to keep appending it infinitely. median = nil end # iterating over the outlines main nodes (this is not going into subtrees) # while keeping every rotations previous node saved prev = pos pos = actual_object(pos[:Next]) end # make sure the last object doesn't have the :Next and the first no :Prev property prev.delete :Next actual_object(old_data[:First]).delete :Prev end end # Prints the whole outline hash to a file, # with basic indentation and replacing raw streams with "RAW STREAM" # (subbing doesn't allways work that great for big streams) # outline - outline hash # file - "filename.filetype" string def print_outline_to_file(outline, file) outline_subbed_str = outline.to_s.gsub(/\:raw_stream_content=\>"(?:(?!"}).)*+"\}\}/, ':raw_stream_content=> RAW STREAM}}') brace_cnt = 0 formatted_outline_str = '' outline_subbed_str.each_char do |c| if c == '{' formatted_outline_str << "\n" << "\t" * brace_cnt << c brace_cnt += 1 elsif c == '}' brace_cnt -= 1 brace_cnt = 0 if brace_cnt < 0 formatted_outline_str << c << "\n" << "\t" * brace_cnt elsif c == '\n' formatted_outline_str << c << "\t" * brace_cnt else formatted_outline_str << c end end formatted_outline_str << "\n" * 10 File.open(file, 'w') { |f| f.write(formatted_outline_str) } end private def equal_layers obj1, obj2, layer = CombinePDF.eq_depth_limit return true if obj1.equal?(obj2) if obj1.is_a? Hash return false unless obj2.is_a? Hash return false unless obj1.length == obj2.length keys = obj1.keys; keys2 = obj2.keys; return false if (keys - keys2).any? || (keys2 - keys).any? return (warn("CombinePDF nesting limit reached") || true) if(layer == 0) keys.each {|k| return false unless equal_layers( obj1[k], obj2[k], layer-1) } elsif obj1.is_a? Array return false unless obj2.is_a? Array return false unless obj1.length == obj2.length (obj1-obj2).any? || (obj2-obj1).any? else obj1 == obj2 end end def renaming_dictionary(object = nil, dictionary = {}) object ||= @names case object when Array object.length.times { |i| object[i].is_a?(String) ? (dictionary[object[i]] = (dictionary.last || 'Random_0001').next) : renaming_dictionary(object[i], dictionary) } when Hash object.values.each { |v| renaming_dictionary v, dictionary } end end def rename_object(object, _dictionary) case object when Array object.length.times { |i| } when Hash end end end end