Class: Pdfrb::Document

Inherits:
Object
  • Object
show all
Includes:
PdfA
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/pdf_a.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

Modules: PdfA 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

Constant Summary

Constants included from PdfA

PdfA::SRGB_IDENTIFIER, PdfA::SRGB_REGISTRY

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from PdfA

#enable_pdf_a!, #pdfa_conformance, #pdfa_part

Constructor Details

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

Returns a new instance of Document.



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

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.



59
60
61
# File 'lib/pdfrb/document.rb', line 59

def config
  @config
end

#ioObject (readonly)

Returns the value of attribute io.



59
60
61
# File 'lib/pdfrb/document.rb', line 59

def io
  @io
end

#next_oidObject (readonly)

Returns the value of attribute next_oid.



59
60
61
# File 'lib/pdfrb/document.rb', line 59

def next_oid
  @next_oid
end

#versionObject

Returns the value of attribute version.



59
60
61
# File 'lib/pdfrb/document.rb', line 59

def version
  @version
end

#xrefObject (readonly)

Returns the value of attribute xref.



59
60
61
# File 'lib/pdfrb/document.rb', line 59

def xref
  @xref
end

Class Method Details

.open(path, **opts) ⇒ Object



61
62
63
64
65
66
67
68
69
# File 'lib/pdfrb/document.rb', line 61

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



81
82
83
84
85
86
# File 'lib/pdfrb/document.rb', line 81

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

#annotationsObject



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

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

#associated_filesObject



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

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

#catalogObject



243
244
245
246
247
248
249
250
# File 'lib/pdfrb/document.rb', line 243

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

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

  @catalog = object(ref)
end

#colorsObject



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

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

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



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

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

#decrypt!Object



176
177
178
# File 'lib/pdfrb/document.rb', line 176

def decrypt!
  encryption.decrypt!
end

#destinationsObject



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

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

#dispatch_message(message, *args) ⇒ Object



106
107
108
109
110
# File 'lib/pdfrb/document.rb', line 106

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

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

#displayObject



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

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.



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/pdfrb/document.rb', line 255

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.



287
288
289
290
291
292
293
# File 'lib/pdfrb/document.rb', line 287

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

#encrypt!Object



172
173
174
# File 'lib/pdfrb/document.rb', line 172

def encrypt!(**)
  encryption.encrypt!(**)
end

#encryptionObject



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

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

#filesObject



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

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

#fontsObject



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

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



220
221
222
# File 'lib/pdfrb/document.rb', line 220

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

#formObject



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

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

#graphics_stateObject



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

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

#imagesObject



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

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

#infoObject



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

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

#layersObject



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

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

#load_xmp_packetObject



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

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

#metadataObject



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

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



91
92
93
94
95
96
97
98
99
# File 'lib/pdfrb/document.rb', line 91

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



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

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

#output_intentsObject



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

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

#page_labelsObject



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

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



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

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

#portfolioObject



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

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

#register_listener(message, &block) ⇒ Object



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

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.



212
213
214
# File 'lib/pdfrb/document.rb', line 212

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.



297
298
299
# File 'lib/pdfrb/document.rb', line 297

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

#shadingsObject



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

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

#stampsObject



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

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

#structureObject



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

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.



237
238
239
240
241
# File 'lib/pdfrb/document.rb', line 237

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

  @trailer = read_trailer
end

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



71
72
73
74
75
76
77
78
79
# File 'lib/pdfrb/document.rb', line 71

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)


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

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

  fonts.subset_fonts! if config["writer.subset_fonts"] != false
  Pdfrb::Writer.write(self, target)
  target.close if path && io.nil? && target.is_a?(IO)
  self
end

#xmpObject



184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/pdfrb/document.rb', line 184

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