def parse
      return [] if @string_to_parse.empty?
      return @parsed unless @parsed.empty?
      @scanner = StringScanner.new @string_to_parse
      @scanner.pos = 0
      @scanner.skip(/[^%]*/) if @scanner.exist?(/%PDF/i)
      if @scanner.scan(/\%PDF\-[\d\-\.]+/)
        @version = @scanner.matched.scan(/[\d\.]+/)[0].to_f
        loop do
          break unless @scanner.scan(/[^\d\r\n]+/)
          break if @scanner.check(/([\d]+[\s]+[\d]+[\s]+obj[\s]+\<\<)|([\n\r]+)/)
          break if @scanner.eos?
          @scanner.pos += 1
        end
      end
      @parsed = _parse_
      # puts @parsed

      unless (@parsed.select { |i| !i.is_a?(Hash) }).empty?
        # p @parsed.select
        raise ParsingError, 'Unknown PDF parsing error - malformed PDF file?'
      end

      if @root_object == {}.freeze
        xref_streams = @parsed.select { |obj| obj.is_a?(Hash) && obj[:Type] == :XRef }
        xref_streams.each do |xref_dictionary|
          @root_object.merge! xref_dictionary
        end
      end

      if @root_object == {}.freeze
        raise ParsingError, 'root is unknown - cannot determine if file is Encrypted'
      end

      if @root_object[:Encrypt]
        # change_references_to_actual_values @root_object
        warn 'PDF is Encrypted! Attempting to decrypt - not yet fully supported.'
        decryptor = PDFDecrypt.new @parsed, @root_object
        decryptor.decrypt
        # do we really need to apply to @parsed? No, there is no need.
      end

      # search for objects streams and replace them "in-place"
      # the inplace resolution prevents versioning errors
      while (true)
        found_object_streams = false
        @parsed.length.times do |i|
          o = @parsed[i]
          next unless o.is_a?(Hash) && o[:Type] == :ObjStm
          ## un-encode (using the correct filter) the object streams
          PDFFilter.inflate_object o
          # puts "Object Stream Found:", o[:raw_stream_content]
          ## extract objects from stream
          @scanner = StringScanner.new o[:raw_stream_content]
          stream_data = _parse_
          id_array = []
          collection = [nil]
          while (stream_data[0].is_a?(Numeric) && stream_data[1].is_a?(Numeric))
            id_array << stream_data.shift
            stream_data.shift
          end
          while id_array[0] && stream_data[0]
            stream_data[0] = { indirect_without_dictionary: stream_data[0] } unless stream_data[0].is_a?(Hash)
            stream_data[0][:indirect_reference_id] = id_array.shift
            stream_data[0][:indirect_generation_number] = 0
            collection << (stream_data.shift)
          end
          # place new objects right after this one (removing this one as well)
          @parsed[i] = collection
          found_object_streams = true
        end
        break unless found_object_streams
        @parsed.flatten!
        @parsed.compact!
      end

      #
      # object_streams = @parsed.select { |obj| obj.is_a?(Hash) && obj[:Type] == :ObjStm }
      # unless object_streams.empty?
      #   warn 'PDF 1.5 Object streams found - they are not fully supported! attempting to extract objects.'
      #
      #   object_streams.each do |o|
      #     ## un-encode (using the correct filter) the object streams
      #     PDFFilter.inflate_object o
      #     ## extract objects from stream to top level arry @parsed
      #     @scanner = StringScanner.new o[:raw_stream_content]
      #     stream_data = _parse_
      #     id_array = []
      #     while stream_data[0].is_a? (Numeric)
      #       id_array << stream_data.shift
      #       stream_data.shift
      #     end
      #     while id_array[0] && stream_data[0]
      #       stream_data[0] = { indirect_without_dictionary: stream_data[0] } unless stream_data[0].is_a?(Hash)
      #       stream_data[0][:indirect_reference_id] = id_array.shift
      #       stream_data[0][:indirect_generation_number] = 0
      #       @parsed << stream_data.shift
      #     end
      #   end
      # end

      # serialize_objects_and_references.catalog_pages

      # Benchmark.bm do |bm|
      # 	bm.report("serialize") {1000.times {serialize_objects_and_references} }
      # 	bm.report("serialize - old") {1000.times {old_serialize_objects_and_references} }
      # 	bm.report("catalog") {1000.times {catalog_pages} }
      # end

      serialize_objects_and_references

      catalog_pages

      # Strings were unified, we can let them go..
      @strings_dictionary.clear

      # collect any missing objects from the forms_data
      unless @forms_object.nil? || @forms_object.empty?
        @forms_object[:related_objects] = (@parsed.select { |o| o[:FT] }).map! { |o| { is_reference_only: true, referenced_object: o } }
        @forms_object[:related_objects].delete @forms_object
      end

      @info_object = @root_object[:Info] ? (@root_object[:Info][:referenced_object] || @root_object[:Info]) : false
      if @info_object && @info_object.is_a?(Hash)
        @parsed.delete @info_object
        CombinePDF::PDF::PRIVATE_HASH_KEYS.each { |key| @info_object.delete key }
        @info_object.each { |_k, v| @info_object = v[:referenced_object] if v.is_a?(Hash) && v[:referenced_object] }
      else
        @info_object = {}
      end

      # we can clear the resolution hash now
      @resolution_hash.clear if @resolution_hash
      # # # ## remove object streams - if they exist
      # @parsed.reject! {|obj| object_streams << obj if obj.is_a?(Hash) && obj[:Type] == :ObjStm}
      # # # ## remove XREF dictionaries - if they exist
      # @parsed.reject! {|obj| object_streams << obj if obj.is_a?(Hash) && obj[:Type] == :XRef}

      @parsed
    end
def parse
      return [] if @string_to_parse.empty?
      return @parsed unless @parsed.empty?
      @scanner = StringScanner.new @string_to_parse
      @scanner.pos = 0
      @scanner.skip(/[^%]*/) if @scanner.exist?(/%PDF/i)
      if @scanner.scan(/\%PDF\-[\d\-\.]+/)
        @version = @scanner.matched.scan(/[\d\.]+/)[0].to_f
        loop do
          break unless @scanner.scan(/[^\d\r\n]+/)
          break if @scanner.check(/([\d]+[\s]+[\d]+[\s]+obj[\s]+\<\<)|([\n\r]+)/)
          break if @scanner.eos?
          @scanner.pos += 1
        end
      end
      @parsed = _parse_
      # puts @parsed

      unless (@parsed.select { |i| !i.is_a?(Hash) }).empty?
        # p @parsed.select
        raise ParsingError, 'Unknown PDF parsing error - malformed PDF file?'
      end

      if @root_object == {}.freeze
        xref_streams = @parsed.select { |obj| obj.is_a?(Hash) && obj[:Type] == :XRef }
        xref_streams.each do |xref_dictionary|
          @root_object.merge! xref_dictionary
        end
      end

      if @root_object == {}.freeze
        raise ParsingError, 'root is unknown - cannot determine if file is Encrypted'
      end

      if @root_object[:Encrypt]
        # change_references_to_actual_values @root_object
        warn 'PDF is Encrypted! Attempting to decrypt - not yet fully supported.'
        decryptor = PDFDecrypt.new @parsed, @root_object
        decryptor.decrypt
        # do we really need to apply to @parsed? No, there is no need.
      end

      # search for objects streams and replace them "in-place"
      # the inplace resolution prevents versioning errors
      while (true)
        found_object_streams = false
        @parsed.length.times do |i|
          o = @parsed[i]
          next unless o.is_a?(Hash) && o[:Type] == :ObjStm
          ## un-encode (using the correct filter) the object streams
          PDFFilter.inflate_object o
          # puts "Object Stream Found:", o[:raw_stream_content]
          ## extract objects from stream
          @scanner = StringScanner.new o[:raw_stream_content]
          stream_data = _parse_
          id_array = []
          collection = [nil]
          while (stream_data[0].is_a?(Numeric) && stream_data[1].is_a?(Numeric))
            id_array << stream_data.shift
            stream_data.shift
          end
          while id_array[0] && stream_data[0]
            stream_data[0] = { indirect_without_dictionary: stream_data[0] } unless stream_data[0].is_a?(Hash)
            stream_data[0][:indirect_reference_id] = id_array.shift
            stream_data[0][:indirect_generation_number] = 0
            collection << (stream_data.shift)
          end
          # place new objects right after this one (removing this one as well)
          @parsed[i] = collection
          found_object_streams = true
        end
        break unless found_object_streams
        @parsed.flatten!
        @parsed.compact!
      end

      #
      # object_streams = @parsed.select { |obj| obj.is_a?(Hash) && obj[:Type] == :ObjStm }
      # unless object_streams.empty?
      #   warn 'PDF 1.5 Object streams found - they are not fully supported! attempting to extract objects.'
      #
      #   object_streams.each do |o|
      #     ## un-encode (using the correct filter) the object streams
      #     PDFFilter.inflate_object o
      #     ## extract objects from stream to top level arry @parsed
      #     @scanner = StringScanner.new o[:raw_stream_content]
      #     stream_data = _parse_
      #     id_array = []
      #     while stream_data[0].is_a? (Numeric)
      #       id_array << stream_data.shift
      #       stream_data.shift
      #     end
      #     while id_array[0] && stream_data[0]
      #       stream_data[0] = { indirect_without_dictionary: stream_data[0] } unless stream_data[0].is_a?(Hash)
      #       stream_data[0][:indirect_reference_id] = id_array.shift
      #       stream_data[0][:indirect_generation_number] = 0
      #       @parsed << stream_data.shift
      #     end
      #   end
      # end

      # serialize_objects_and_references.catalog_pages

      # Benchmark.bm do |bm|
      # 	bm.report("serialize") {1000.times {serialize_objects_and_references} }
      # 	bm.report("serialize - old") {1000.times {old_serialize_objects_and_references} }
      # 	bm.report("catalog") {1000.times {catalog_pages} }
      # end

      serialize_objects_and_references

      catalog_pages

      # Strings were unified, we can let them go..
      @strings_dictionary.clear

      # collect any missing objects from the forms_data
      unless @forms_object.nil? || @forms_object.empty?
        @forms_object[:related_objects] = (@parsed.select { |o| o[:FT] }).map! { |o| { is_reference_only: true, referenced_object: o } }
        @forms_object[:related_objects].delete @forms_object
      end

      @info_object = @root_object[:Info] ? (@root_object[:Info][:referenced_object] || @root_object[:Info]) : false
      if @info_object && @info_object.is_a?(Hash)
        @parsed.delete @info_object
        CombinePDF::PDF::PRIVATE_HASH_KEYS.each { |key| @info_object.delete key }
        @info_object.each { |_k, v| @info_object = v[:referenced_object] if v.is_a?(Hash) && v[:referenced_object] }
      else
        @info_object = {}
      end

      # we can clear the resolution hash now
      @resolution_hash.clear if @resolution_hash
      # # # ## remove object streams - if they exist
      # @parsed.reject! {|obj| object_streams << obj if obj.is_a?(Hash) && obj[:Type] == :ObjStm}
      # # # ## remove XREF dictionaries - if they exist
      # @parsed.reject! {|obj| object_streams << obj if obj.is_a?(Hash) && obj[:Type] == :XRef}

      @parsed
    end
when creating a parser, it is important to set the data (String) we wish to parse.

 <b>the data is required and it is not possible to set the data at a later stage</b>

 string:: the data to be parsed, as a String object.
 parse the data in the new parser (the data already set through the initialize / new method)