Class: Pdfrb::Document

Inherits:
Object
  • Object
show all
Defined in:
lib/pdfrb/document.rb,
lib/pdfrb/document/form.rb,
lib/pdfrb/document/info.rb,
lib/pdfrb/document/files.rb,
lib/pdfrb/document/fonts.rb,
lib/pdfrb/document/pages.rb,
lib/pdfrb/document/colors.rb,
lib/pdfrb/document/images.rb,
lib/pdfrb/document/layers.rb,
lib/pdfrb/document/stamps.rb,
lib/pdfrb/document/display.rb,
lib/pdfrb/document/outline.rb,
lib/pdfrb/document/metadata.rb,
lib/pdfrb/document/shadings.rb,
lib/pdfrb/document/portfolio.rb,
lib/pdfrb/document/structure.rb,
lib/pdfrb/document/encryption.rb,
lib/pdfrb/document/annotations.rb,
lib/pdfrb/document/page_labels.rb,
lib/pdfrb/document/destinations.rb,
lib/pdfrb/document/form_xobject.rb,
lib/pdfrb/document/graphics_state.rb,
lib/pdfrb/document/output_intents.rb,
lib/pdfrb/document/associated_files.rb

Overview

Top-level PDF document facade. Owns:

* The IO it was read from (or nil for in-memory).
* An oid -> Object table for new/modified objects.
* A wrap() pipeline that upgrades raw Hashes to typed
Dictionary subclasses based on /Type or an explicit type.
* On read: an XrefSection + ObjectReader for lazy resolution.
* A revisions stack (TODO 30 — incremental updates).

Defined Under Namespace

Classes: Annotations, AssociatedFiles, Colors, Destinations, Display, Encryption, Files, Fonts, Form, FormXObject, GraphicsState, Images, Info, Layers, Metadata, Outline, OutlineEntry, OutputIntents, PageLabels, Pages, Portfolio, Shadings, Stamps, Structure

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io: nil, config: {}) ⇒ Document

Returns a new instance of Document.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/pdfrb/document.rb', line 36

def initialize(io: nil, config: {})
  Pdfrb::Model::Type.eager_load!
  @config = Configuration.new(config)
  @io = io
  @objects = {}        # oid -> Pdfrb::Model::Object (modified or new)
  @next_oid = 1
  @listeners = {}      # message_name -> [Proc]
  @xref = nil
  @object_reader = nil
  @version = "1.4"
  @empty_trailer = nil
  @revisions = []      # array of [xref, trailer] tuples, latest first

  if io
    read_from_io(io)
  else
    seed_empty_structure
  end
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



56
57
58
# File 'lib/pdfrb/document.rb', line 56

def config
  @config
end

#ioObject (readonly)

Returns the value of attribute io.



56
57
58
# File 'lib/pdfrb/document.rb', line 56

def io
  @io
end

#next_oidObject (readonly)

Returns the value of attribute next_oid.



56
57
58
# File 'lib/pdfrb/document.rb', line 56

def next_oid
  @next_oid
end

#versionObject

Returns the value of attribute version.



56
57
58
# File 'lib/pdfrb/document.rb', line 56

def version
  @version
end

#xrefObject (readonly)

Returns the value of attribute xref.



56
57
58
# File 'lib/pdfrb/document.rb', line 56

def xref
  @xref
end

Class Method Details

.open(path, **opts) ⇒ Object



58
59
60
61
62
63
64
65
66
# File 'lib/pdfrb/document.rb', line 58

def self.open(path, **opts)
  if block_given?
    File.open(path, "rb") { |f| yield new(io: f, **opts) }
  else
    bytes = File.binread(path)
    require "stringio"
    new(io: StringIO.new(bytes), **opts)
  end
end

Instance Method Details

#add(value, type: nil) ⇒ Object



78
79
80
81
82
83
# File 'lib/pdfrb/document.rb', line 78

def add(value, type: nil)
  oid = allocate_oid
  obj = wrap(value, type: type, oid: oid, gen: 0)
  register(obj)
  obj
end

#annotationsObject



137
138
139
# File 'lib/pdfrb/document.rb', line 137

def annotations
  @annotations ||= Document::Annotations.new(self)
end

#associated_filesObject



157
# File 'lib/pdfrb/document.rb', line 157

def associated_files; @associated_files ||= Document::AssociatedFiles.new(self); end

#catalogObject



231
232
233
234
235
236
237
238
# File 'lib/pdfrb/document.rb', line 231

def catalog
  return @catalog if defined?(@catalog) && @catalog

  ref = trailer ? trailer[:Root] : nil
  return nil unless ref

  @catalog = object(ref)
end

#colorsObject



151
# File 'lib/pdfrb/document.rb', line 151

def colors; @colors ||= Document::Colors.new(self); end

#create_form_xobject(name: nil, bbox: nil, matrix: nil) ⇒ Object



194
195
196
# File 'lib/pdfrb/document.rb', line 194

def create_form_xobject(name: nil, bbox: nil, matrix: nil)
  FormXObject.new(self, name: name, bbox: bbox, matrix: matrix)
end

#destinationsObject



133
134
135
# File 'lib/pdfrb/document.rb', line 133

def destinations
  @destinations ||= Document::Destinations.new(self)
end

#dispatch_message(message, *args) ⇒ Object



103
104
105
106
107
# File 'lib/pdfrb/document.rb', line 103

def dispatch_message(message, *args)
  return unless @listeners.key?(message)

  @listeners[message].each { |blk| blk.call(*args) }
end

#displayObject



171
# File 'lib/pdfrb/document.rb', line 171

def display; @display ||= Document::Display.new(self); end

#each_indirect_objectObject

Yield every indirect object in this document: modified/new ones first (they shadow loaded ones), then every loaded entry the xref knows about.



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/pdfrb/document.rb', line 243

def each_indirect_object
  return enum_for(:each_indirect_object) unless block_given?

  seen = Set.new
  @objects.each_value do |obj|
    next unless obj.indirect?

    seen << obj.oid
    yield obj
  end
  return if @xref.nil?

  @xref.entries.each_key do |oid|
    next if seen.include?(oid)
    next if oid.zero?

    obj = object(Pdfrb::Model::Reference.new(oid, 0))
    next if obj.nil? || !obj.indirect?

    yield obj
  end
end

#each_revisionObject

Walk every revision in the document's incremental-update chain, from the latest (most recent) revision backward via /Prev. Each revision yields a (revision_index, xref, trailer) tuple where revision_index 0 is the latest. Documents without incremental updates yield a single tuple.

Useful for forensic inspection, version-aware rendering, and debugging "ghost" objects that were modified in a later revision.



275
276
277
278
279
280
281
# File 'lib/pdfrb/document.rb', line 275

def each_revision
  return enum_for(:each_revision) unless block_given?

  return (yield 0, @xref, trailer) if @revisions.empty? && @xref

  @revisions.each_with_index { |(xref, tr), i| yield i, xref, tr }
end

#encryptionObject



167
# File 'lib/pdfrb/document.rb', line 167

def encryption; @encryption ||= Document::Encryption.new(self); end

#filesObject



125
126
127
# File 'lib/pdfrb/document.rb', line 125

def files
  @files ||= Document::Files.new(self)
end

#fontsObject



117
118
119
# File 'lib/pdfrb/document.rb', line 117

def fonts
  @fonts ||= Document::Fonts.new(self)
end

#forget(oid) ⇒ Object

Drop a modified/new object from the in-memory table so it's no longer emitted by the writer. Used by Task::Optimize when a stream has been deduplicated against a canonical sibling. Returns the dropped object (or nil if not present).



209
210
211
# File 'lib/pdfrb/document.rb', line 209

def forget(oid)
  @objects.delete(oid)
end

#formObject



147
# File 'lib/pdfrb/document.rb', line 147

def form; @form ||= Document::Form.new(self); end

#graphics_stateObject



153
# File 'lib/pdfrb/document.rb', line 153

def graphics_state; @graphics_state ||= Document::GraphicsState.new(self); end

#imagesObject



121
122
123
# File 'lib/pdfrb/document.rb', line 121

def images
  @images ||= Document::Images.new(self)
end

#infoObject



169
# File 'lib/pdfrb/document.rb', line 169

def info; @info ||= Document::Info.new(self); end

#layersObject



155
# File 'lib/pdfrb/document.rb', line 155

def layers; @layers ||= Document::Layers.new(self); end

#load_xmp_packetObject



186
187
188
189
190
# File 'lib/pdfrb/document.rb', line 186

def load_xmp_packet
  Pdfrb::XMP::Packet.new
rescue LoadError
  nil
end

#metadataObject



129
130
131
# File 'lib/pdfrb/document.rb', line 129

def 
  @metadata ||= Document::Metadata.new(self)
end

#object(reference) ⇒ Object Also known as: dereference

Resolve a Reference to its Object. New/modified objects take precedence over xref-loaded ones; otherwise consult the ObjectReader (which caches per oid).



88
89
90
91
92
93
94
95
96
# File 'lib/pdfrb/document.rb', line 88

def object(reference)
  return reference unless reference.is_a?(Pdfrb::Model::Reference)

  modified = @objects[reference.oid]
  return modified if modified
  return nil if @object_reader.nil?

  @object_reader.load_oid(reference.oid)
end

#outlineObject



141
142
143
# File 'lib/pdfrb/document.rb', line 141

def outline
  @outline ||= Document::Outline.new(self)
end

#output_intentsObject



161
# File 'lib/pdfrb/document.rb', line 161

def output_intents; @output_intents ||= Document::OutputIntents.new(self); end

#page_labelsObject



163
# File 'lib/pdfrb/document.rb', line 163

def page_labels; @page_labels ||= Document::PageLabels.new(self); end

#pagesObject

---- Facade accessors (Phase 13) ---- Each returns a memoised helper object that knows how to mutate this document. Defined under Pdfrb::Document::*.



113
114
115
# File 'lib/pdfrb/document.rb', line 113

def pages
  @pages ||= Document::Pages.new(self)
end

#portfolioObject



159
# File 'lib/pdfrb/document.rb', line 159

def portfolio; @portfolio ||= Document::Portfolio.new(self); end

#register_listener(message, &block) ⇒ Object



99
100
101
# File 'lib/pdfrb/document.rb', line 99

def register_listener(message, &block)
  (@listeners[message] ||= []) << block
end

#register_override(obj) ⇒ Object

Replace an indirect object in the @objects table. Used by the Importer when promoting a Dictionary stub to a Stream (so cycles resolve correctly). Idempotent.



201
202
203
# File 'lib/pdfrb/document.rb', line 201

def register_override(obj)
  @objects[obj.oid] = obj
end

#revision_countObject

Number of revisions in the document. 1 for a freshly written PDF; >1 for incrementally-updated PDFs.



285
286
287
# File 'lib/pdfrb/document.rb', line 285

def revision_count
  @revisions.empty? ? 1 : @revisions.length
end

#shadingsObject



149
# File 'lib/pdfrb/document.rb', line 149

def shadings; @shadings ||= Document::Shadings.new(self); end

#stampsObject



165
# File 'lib/pdfrb/document.rb', line 165

def stamps; @stamps ||= Document::Stamps.new(self); end

#structureObject



145
# File 'lib/pdfrb/document.rb', line 145

def structure; @structure ||= Document::Structure.new(self); end

#trailerObject

Convenience accessor for the document Catalog dict. Loads from trailer's /Root, which itself is loaded lazily from the xref.



225
226
227
228
229
# File 'lib/pdfrb/document.rb', line 225

def trailer
  return @trailer if defined?(@trailer) && @trailer

  @trailer = read_trailer
end

#wrap(data, type: nil, oid: 0, gen: 0) ⇒ Object



68
69
70
71
72
73
74
75
76
# File 'lib/pdfrb/document.rb', line 68

def wrap(data, type: nil, oid: 0, gen: 0)
  target_class = resolve_target_class(data, type)
  value = unwrap_value(data)
  if target_class <= Pdfrb::Model::Object
    target_class.new(value, oid: oid, gen: gen, document: self)
  else
    target_class.new(value)
  end
end

#write(path = nil, io: nil) ⇒ Object

Write this document to path (or any IO via +io:).

Raises:

  • (ArgumentError)


214
215
216
217
218
219
220
221
# File 'lib/pdfrb/document.rb', line 214

def write(path = nil, io: nil)
  target = io || (path && File.open(path, "wb"))
  raise ArgumentError, "write needs a path or io:" unless target

  Pdfrb::Writer.write(self, target)
  target.close if path && io.nil? && target.is_a?(IO)
  self
end

#xmpObject



173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/pdfrb/document.rb', line 173

def xmp
  return @xmp if @xmp

  packet = load_xmp_packet
  return nil unless packet

  title = [:Title]
  packet.title = title if title
  author = [:Author]
  packet.author = author if author
  @xmp = packet
end