class HexaPDF::Writer

Writes the contents of a PDF document to an IO stream.

def self.write(document, io, incremental: false)

are appended to a full copy of the source document.
If +incremental+ is +true+ and the document was created from an existing PDF file, the changes

Writes the document to the IO object and returns the last XRefSection written.
def self.write(document, io, incremental: false)
  if incremental && document.revisions.parser
    new(document, io).write_incremental
  else
    new(document, io).write
  end
end

def initialize(document, io)

object.
Creates a new writer object for the given HexaPDF document that gets written to the IO
def initialize(document, io)
  @document = document
  @io = io
  @io.binmode
  @io.seek(0, IO::SEEK_SET) # TODO: incremental update!
  @serializer = Serializer.new
  @serializer.encrypter = @document.encrypted? ? @document.security_handler : nil
  @rev_size = 0
  @use_xref_streams = false
end

def move_modified_objects_into_current_revision

Moves all modified objects into the current revision to avoid invalid references and such.
def move_modified_objects_into_current_revision
  return if @document.revisions.count == 1
  revision = @document.revisions.add
  @document.revisions.all[0..-2].each do |rev|
    rev.each_modified_object(delete: true) {|obj| revision.send(:add_without_check, obj) }
  end
  @document.revisions.merge(-2..-1)
end

def write

cross-reference section and the last XRefSection written.
Writes the document to the IO object and returns the file position of the start of the last
def write
  move_modified_objects_into_current_revision
  write_file_header
  pos = xref_section = nil
  @document.trailer.info[:Producer] = "HexaPDF version #{HexaPDF::VERSION}"
  @document.revisions.each do |rev|
    pos, xref_section = write_revision(rev, pos)
  end
  [pos, xref_section]
end

def write_file_header

See: PDF2.0 s7.5.2

Writes the PDF file header.
def write_file_header
  @io << "%PDF-#{@document.version}\n%\xCF\xEC\xFF\xE8\xD7\xCB\xCD\n"
end

def write_incremental

For this method to work the document must have been created from an existing file.

XRefSection object of that one revision.
changes. Returns the file position of the start of the cross-reference section and the
Writes the complete source document unmodified to the IO and then one revision containing all
def write_incremental
  parser = @document.revisions.parser
  _, orig_trailer = parser.load_revision(parser.startxref_offset)
  orig_trailer = @document.wrap(orig_trailer, type: :XXTrailer)
  if @document.revisions.current.trailer[:Encrypt]&.value != orig_trailer[:Encrypt]&.value
    raise HexaPDF::Error, "Used encryption cannot be modified when doing incremental writing"
  end
  parser.io.seek(0, IO::SEEK_SET)
  IO.copy_stream(parser.io, @io)
  @io << "\n"
  @rev_size = @document.revisions.current.next_free_oid
  @use_xref_streams = @document.revisions.any? {|rev| rev.trailer[:Type] == :XRef }
  revision = Revision.new(@document.revisions.current.trailer)
  @document.trailer.info[:Producer] = "HexaPDF version #{HexaPDF::VERSION}"
  if parser.file_header_version < @document.version
    @document.catalog[:Version] = @document.version.to_sym
  end
  @document.revisions.each do |rev|
    rev.each_modified_object(all: true) {|obj| revision.send(:add_without_check, obj) }
  end
  write_revision(revision, parser.startxref_offset)
end

def write_indirect_object(obj)

Writes the single indirect object which may be a stream object or another object.
def write_indirect_object(obj)
  @io << "#{obj.oid} #{obj.gen} obj\n"
  @serializer.serialize_to_io(obj, @io)
  @io << "\nendobj\n"
end

def write_revision(rev, previous_xref_pos = nil)

cross-reference section or stream if applicable.
The optional +previous_xref_pos+ argument needs to contain the byte position of the previous

Writes the given revision.
def write_revision(rev, previous_xref_pos = nil)
  xref_stream, object_streams = xref_and_object_streams(rev)
  obj_to_stm = object_streams.each_with_object({}) {|stm, m| m.update(stm.write_objects(rev)) }
  xref_section = XRefSection.new
  xref_section.add_free_entry(0, 65535) if previous_xref_pos.nil?
  rev.each do |obj|
    if obj.null?
      xref_section.add_free_entry(obj.oid, obj.gen)
    elsif (objstm = obj_to_stm[obj])
      xref_section.add_compressed_entry(obj.oid, objstm.oid, objstm.object_index(obj))
    elsif obj != xref_stream
      xref_section.add_in_use_entry(obj.oid, obj.gen, @io.pos)
      write_indirect_object(obj)
    end
  end
  trailer = rev.trailer.value.dup
  trailer.delete(:XRefStm)
  if previous_xref_pos
    trailer[:Prev] = previous_xref_pos
  else
    trailer.delete(:Prev)
  end
  @rev_size = rev.next_free_oid if rev.next_free_oid > @rev_size
  trailer[:Size] = @rev_size
  startxref = @io.pos
  if xref_stream
    xref_section.add_in_use_entry(xref_stream.oid, xref_stream.gen, startxref)
    xref_stream.update_with_xref_section_and_trailer(xref_section, trailer)
    write_indirect_object(xref_stream)
  else
    write_xref_section(xref_section)
    write_trailer(trailer)
  end
  write_startxref(startxref)
  [startxref, xref_section]
end

def write_startxref(startxref)

See: PDF2.0 s7.5.5, s7.5.8

Writes the startxref line needed for cross-reference sections and cross-reference streams.
def write_startxref(startxref)
  @io << "startxref\n#{startxref}\n%%EOF\n"
end

def write_trailer(trailer)

See: PDF2.0 s7.5.5

Writes the trailer dictionary.
def write_trailer(trailer)
  @io << "trailer\n#{@serializer.serialize(trailer)}\n"
end

def write_xref_section(xref_section)

See: PDF2.0 s7.5.4

Writes the cross-reference section.
def write_xref_section(xref_section)
  @io << "xref\n"
  xref_section.each_subsection do |entries|
    @io << "#{entries.empty? ? 0 : entries.first.oid} #{entries.size}\n"
    entries.each do |entry|
      if entry.in_use?
        @io << sprintf("%010d %05d n \n", entry.pos, entry.gen).freeze
      elsif entry.free?
        @io << "0000000000 65535 f \n"
      else
        # Should never occur since we create the xref section!
        raise HexaPDF::Error, "Cannot use xref type #{entry.type} in cross-reference section"
      end
    end
  end
end

def xref_and_object_streams(rev)

ignored.
it contains multiple cross-reference streams only the first one is used, the rest are
An error is raised if the revision contains object streams and no cross-reference stream. If

Returns the cross-reference and object streams of the given revision.

writer.xref_and_object_streams -> [xref_stream, object_streams]
:call-seq:
def xref_and_object_streams(rev)
  xref_stream = nil
  object_streams = []
  rev.each do |obj|
    if obj.type == :ObjStm
      object_streams << obj
    elsif !xref_stream && obj.type == :XRef
      xref_stream = obj
    end
  end
  if (!object_streams.empty? || @use_xref_streams) && xref_stream.nil?
    xref_stream = @document.wrap({}, type: Type::XRefStream, oid: @document.revisions.next_oid)
    rev.add(xref_stream)
  end
  @use_xref_streams = true if xref_stream
  [xref_stream, object_streams]
end