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/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, 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.



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

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.



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

def config
  @config
end

#ioObject (readonly)

Returns the value of attribute io.



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

def io
  @io
end

#next_oidObject (readonly)

Returns the value of attribute next_oid.



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

def next_oid
  @next_oid
end

#versionObject

Returns the value of attribute version.



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

def version
  @version
end

#xrefObject (readonly)

Returns the value of attribute xref.



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

def xref
  @xref
end

Class Method Details

.open(path, **opts) ⇒ Object



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

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



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

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

#annotationsObject



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

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

#associated_filesObject



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

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

#catalogObject



228
229
230
231
232
233
234
235
# File 'lib/pdfrb/document.rb', line 228

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

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

  @catalog = object(ref)
end

#colorsObject



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

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

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



191
192
193
# File 'lib/pdfrb/document.rb', line 191

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

#destinationsObject



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

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

#dispatch_message(message, *args) ⇒ Object



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

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

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

#displayObject



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

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.



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

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.



272
273
274
275
276
277
278
# File 'lib/pdfrb/document.rb', line 272

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

#filesObject



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

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

#fontsObject



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

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).



206
207
208
# File 'lib/pdfrb/document.rb', line 206

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

#formObject



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

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

#graphics_stateObject



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

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

#imagesObject



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

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

#infoObject



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

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

#layersObject



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

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

#load_xmp_packetObject



183
184
185
186
187
# File 'lib/pdfrb/document.rb', line 183

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

#metadataObject



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

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).



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

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



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

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

#output_intentsObject



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

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

#page_labelsObject



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

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::*.



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

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

#portfolioObject



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

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

#register_listener(message, &block) ⇒ Object



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

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.



198
199
200
# File 'lib/pdfrb/document.rb', line 198

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.



282
283
284
# File 'lib/pdfrb/document.rb', line 282

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

#shadingsObject



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

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

#stampsObject



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

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

#structureObject



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

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.



222
223
224
225
226
# File 'lib/pdfrb/document.rb', line 222

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

  @trailer = read_trailer
end

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



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

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)


211
212
213
214
215
216
217
218
# File 'lib/pdfrb/document.rb', line 211

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



170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/pdfrb/document.rb', line 170

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