class HexaPDF::Writer

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

Public Class Methods

new(document, io) click to toggle source

Creates a new writer object for the given HexaPDF document that gets written to the IO object.

# File lib/hexapdf/writer.rb, line 58
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
end
write(document, io, incremental: false) click to toggle source

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

# File lib/hexapdf/writer.rb, line 48
def self.write(document, io, incremental: false)
  if incremental && document.revisions.parser
    new(document, io).write_incremental
  else
    new(document, io).write
  end
end

Public Instance Methods

write() click to toggle source

Writes the document to the IO object.

# File lib/hexapdf/writer.rb, line 71
def write
  write_file_header

  pos = nil
  @document.trailer.info[:Producer] = "HexaPDF version #{HexaPDF::VERSION}"
  @document.revisions.each do |rev|
    pos = write_revision(rev, pos)
  end
end
write_incremental() click to toggle source

Writes the complete source document and one revision containing all changes to the IO.

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

# File lib/hexapdf/writer.rb, line 84
def write_incremental
  @document.revisions.parser.io.seek(0, IO::SEEK_SET)
  IO.copy_stream(@document.revisions.parser.io, @io)

  @rev_size = @document.revisions.current.next_free_oid

  revision = Revision.new(@document.revisions.current.trailer)
  @document.revisions.each do |rev|
    rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
  end
  write_revision(revision, @document.revisions.parser.startxref_offset)
end

Private Instance Methods

write_file_header() click to toggle source

Writes the PDF file header.

See: PDF1.7 s7.5.2

# File lib/hexapdf/writer.rb, line 102
def write_file_header
  @io << "%PDF-#{@document.version}\n%\xCF\xEC\xFF\xE8\xD7\xCB\xCD\n"
end
write_indirect_object(obj) click to toggle source

Writes the single indirect object which may be a stream object or another object.

# File lib/hexapdf/writer.rb, line 180
def write_indirect_object(obj)
  @io << "#{obj.oid} #{obj.gen} obj\n"
  @serializer.serialize_to_io(obj, @io)
  @io << "\nendobj\n"
end
write_revision(rev, previous_xref_pos = nil) click to toggle source

Writes the given revision.

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

# File lib/hexapdf/writer.rb, line 110
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
end
write_startxref(startxref) click to toggle source

Writes the startxref line needed for cross-reference sections and cross-reference streams.

See: PDF1.7 s7.5.5, s7.5.8

# File lib/hexapdf/writer.rb, line 216
def write_startxref(startxref)
  @io << "startxref\n#{startxref}\n%%EOF\n"
end
write_trailer(trailer) click to toggle source

Writes the trailer dictionary.

See: PDF1.7 s7.5.5

# File lib/hexapdf/writer.rb, line 209
def write_trailer(trailer)
  @io << "trailer\n#{@serializer.serialize(trailer)}\n"
end
write_xref_section(xref_section) click to toggle source

Writes the cross-reference section.

See: PDF1.7 s7.5.4

# File lib/hexapdf/writer.rb, line 189
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
xref_and_object_streams → [xref_stream, object_streams] click to toggle source

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

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

# File lib/hexapdf/writer.rb, line 160
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? && xref_stream.nil?
    raise HexaPDF::Error, "Cannot use object streams when there is no xref stream"
  end

  [xref_stream, object_streams]
end