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,
lib/pdfrb/document/fonts/subsetting.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



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

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

#associated_filesObject



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

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

#catalogObject



250
251
252
253
254
255
256
257
# File 'lib/pdfrb/document.rb', line 250

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

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

  @catalog = object(ref)
end

#colorsObject



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

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

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



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

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

#decrypt!Object



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

def decrypt!
  encryption.decrypt!
end

#destinationsObject



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

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

#dispatch_message(message, *args) ⇒ Object



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

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

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

#displayObject



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

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.



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/pdfrb/document.rb', line 262

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.



294
295
296
297
298
299
300
# File 'lib/pdfrb/document.rb', line 294

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



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

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

#encryptionObject



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

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

#filesObject



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

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

#fontsObject



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

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



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

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

#formObject



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

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

#graphics_stateObject



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

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

#imagesObject



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

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

#infoObject



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

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

#layersObject



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

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

#load_xmp_packetObject



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

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

#metadataObject



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

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

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



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

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



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

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

#output_intentsObject



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

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

#page_labelsObject



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

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



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

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

#portfolioObject



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

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

#register_listener(message, &block) ⇒ Object



109
110
111
# File 'lib/pdfrb/document.rb', line 109

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.



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

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

#resolve(value) ⇒ Object

Resolve a Reference to its Object. New/modified objects take precedence over xref-loaded ones; otherwise consult the ObjectReader (which caches per oid). Resolve a value to the object it denotes: References are dereferenced against this document; anything else is returned unchanged. The single seam for "give me the thing itself".



94
95
96
# File 'lib/pdfrb/document.rb', line 94

def resolve(value)
  value.is_a?(Pdfrb::Model::Reference) ? object(value) : value
end

#revision_countObject

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



304
305
306
# File 'lib/pdfrb/document.rb', line 304

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

#shadingsObject



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

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

#stampsObject



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

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

#structureObject



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

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.



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

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)


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

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



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/pdfrb/document.rb', line 191

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