Class: Pdfrb::Writer

Inherits:
Object
  • Object
show all
Defined in:
lib/pdfrb/writer.rb

Overview

Writes a Document to an IO as a complete PDF file: header, indirect objects, xref section, trailer.

Two modes:

* +write+ (default) — full rewrite; assign oids to new objects,
emit every object the document references.
* +write_incremental+ — append a new revision to an existing
byte stream. Only valid when the document was opened from an
IO; the original bytes are preserved verbatim, and a new
xref + trailer are appended with /Prev pointing back.

Constant Summary collapse

DEFAULT_VERSION =
"1.4"
TRAILER_OVERRIDDEN_KEYS =

Trailer keys each writer recomputes itself; existing values for these are never carried through from the parsed trailer.

%i[Size Root Prev XRefStm].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document, io) ⇒ Writer

Returns a new instance of Writer.



26
27
28
29
30
31
# File 'lib/pdfrb/writer.rb', line 26

def initialize(document, io)
  @document = document
  @io = io
  @serializer = Writer.serializer_for(document)
  @xref_offsets = {}
end

Instance Attribute Details

#documentObject (readonly)

Returns the value of attribute document.



24
25
26
# File 'lib/pdfrb/writer.rb', line 24

def document
  @document
end

#ioObject (readonly)

Returns the value of attribute io.



24
25
26
# File 'lib/pdfrb/writer.rb', line 24

def io
  @io
end

#serializerObject (readonly)

Returns the value of attribute serializer.



24
25
26
# File 'lib/pdfrb/writer.rb', line 24

def serializer
  @serializer
end

Class Method Details

.serializer_encrypter_opts_for(document) ⇒ Object

Build the encrypter option for the Serializer from the document's /Encrypt dict. If the document isn't encrypted, returns empty hash. The SecurityHandler's encrypt_data method becomes the Serializer's encrypter, which encrypts string/stream payloads per-object.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/pdfrb/writer.rb', line 50

def self.serializer_encrypter_opts_for(document)
  # Use a pre-configured handler if the document provides one
  # (e.g., set by the encryption CLI).
  handler = document.config["encryption.handler"]
  return { encrypter: handler } if handler

  trailer = document.trailer
  return {} unless trailer

  encrypt_ref = trailer[:Encrypt]
  return {} unless encrypt_ref

  encrypt_dict = document.resolve(encrypt_ref)
  return {} unless encrypt_dict

  handler = Pdfrb::Encryption::StandardSecurityHandler.new(
    { Encrypt: encrypt_dict.value, ID: trailer[:ID] }
  )
  password = document.config["encryption.password"] || ""
  unless handler.verify_user_password?(password)
    raise Pdfrb::EncryptionError,
          "cannot write encrypted document: password does not verify"
  end

  { encrypter: handler }
end

.serializer_for(document) ⇒ Object

Build a Serializer configured for document: stream compression from config, and an encrypter derived from the trailer /Encrypt. Raises EncryptionError when the document is encrypted but the configured password does not open it — writing must never silently fall back to plaintext.



38
39
40
41
42
43
44
# File 'lib/pdfrb/writer.rb', line 38

def self.serializer_for(document)
  Serializer.new(
    compress_streams: document.config["writer.compress_streams"],
    compress_min_size: document.config["writer.compress_min_size"],
    **serializer_encrypter_opts_for(document)
  )
end

.write(document, io) ⇒ Object



83
84
85
# File 'lib/pdfrb/writer.rb', line 83

def self.write(document, io)
  new(document, io).write
end

.write_incremental(document, io) ⇒ Object



87
88
89
# File 'lib/pdfrb/writer.rb', line 87

def self.write_incremental(document, io)
  new(document, io).write_incremental
end

Instance Method Details

#encryption_dict?(obj) ⇒ Boolean

The /Encrypt dictionary's own strings (U, O, UE, OE, Perms) must stay in cleartext — they carry the password hashes.

Returns:

  • (Boolean)


79
80
81
# File 'lib/pdfrb/writer.rb', line 79

def encryption_dict?(obj)
  (document.trailer || {})[:Encrypt]&.oid == obj.oid
end

#writeObject



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/pdfrb/writer.rb', line 91

def write
  @io.binmode
  @io.truncate(0) if @io.is_a?(StringIO)
  write_header
  dispatch_before_write

  use_stream = document.config["writer.use_xref_stream"]
  pack_objstm = document.config["writer.pack_object_streams"]

  packed = pack_objstm ? pack_object_streams : {}

  each_indirect_object do |obj|
    next if packed.key?(obj.oid)

    @xref_offsets[obj.oid] = @io.pos
    @io << @serializer.serialize_indirect(obj,
                                          skip_string_encryption: encryption_dict?(obj))
  end

  xref_pos = if use_stream
               write_xref_stream(packed)
             else
               write_xref
             end
  if use_stream
    write_xref_stream_trailer(xref_pos)
  else
    write_trailer(xref_pos)
  end
  @io.flush
  self
end

#write_incrementalObject

Append a new revision to the original byte stream. Requires the document to have been opened from an IO (so we can copy its original bytes verbatim) and the IO the writer is given is the target output.

Raises:



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/pdfrb/writer.rb', line 128

def write_incremental
  raise Pdfrb::Error, "incremental write requires a source IO" unless document.io

  @io.binmode
  # Copy the original bytes verbatim.
  document.io.seek(0, IO::SEEK_SET)
  @io << document.io.read
  dispatch_before_write
  # Emit only the modified objects.
  document.each_indirect_object do |obj|
    next unless obj.indirect?

    @xref_offsets[obj.oid] = @io.pos
    @io << @serializer.serialize_indirect(obj,
                                          skip_string_encryption: encryption_dict?(obj))
  end
  xref_pos = write_xref(prev: previous_xref_offset)
  write_trailer(xref_pos, prev: previous_xref_offset)
  @io.flush
  self
end