Class: Acrofill::Document
- Inherits:
-
Object
- Object
- Acrofill::Document
- Defined in:
- lib/acrofill/document.rb
Overview
In-memory object store for a parsed PDF. All indirect objects are materialized once via pdf-reader (which transparently handles xref streams and object streams), then mutated in place before writing.
Constant Summary collapse
- PAGE_NODE_TYPES =
%i[Pages Page].freeze
Instance Attribute Summary collapse
-
#objects ⇒ Object
readonly
Returns the value of attribute objects.
-
#trailer ⇒ Object
readonly
Returns the value of attribute trailer.
Class Method Summary collapse
-
.restore(snapshot) ⇒ Object
Rebuilds a Document from a snapshot produced by #snapshot.
Instance Method Summary collapse
-
#add(obj) ⇒ Object
Adds a new indirect object and returns a reference to it.
-
#deref(obj) ⇒ Object
Follows reference chains (ref -> ref -> value is legal PDF), with a hop cap so a reference cycle cannot loop forever.
-
#each_page ⇒ Object
Depth-first, document-order walk over the /Pages tree.
-
#inherited_value(node, key) ⇒ Object
Resolves an attribute inheritable through the /Parent chain (field attributes like /FT, /DA, /Q or page attributes).
-
#initialize(path) ⇒ Document
constructor
A new instance of Document.
-
#ref_for(obj) ⇒ Object
Wraps a direct object into an indirect one; passes references through.
-
#root ⇒ Object
The document catalog.
-
#snapshot ⇒ Object
A serialized copy of the pristine object graph; see Template.
Constructor Details
#initialize(path) ⇒ Document
Returns a new instance of Document.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/acrofill/document.rb', line 18 def initialize(path) hash = parse_boundary { PDF::Reader::ObjectHash.new(path) } raise Error, 'encrypted PDFs are not supported' if hash.trailer[:Encrypt] @objects = {} @max_oid = 0 # ObjectHash parses object bodies lazily, so the iteration that # materializes them must also stay inside the parse boundary. parse_boundary do hash.each do |ref, obj| @objects[ref.id] = obj @max_oid = ref.id if ref.id > @max_oid end end @trailer = hash.trailer.slice(:Root, :Info, :ID) raise Error, 'PDF has no document catalog' unless @trailer[:Root] end |
Instance Attribute Details
#objects ⇒ Object (readonly)
Returns the value of attribute objects.
16 17 18 |
# File 'lib/acrofill/document.rb', line 16 def objects @objects end |
#trailer ⇒ Object (readonly)
Returns the value of attribute trailer.
16 17 18 |
# File 'lib/acrofill/document.rb', line 16 def trailer @trailer end |
Class Method Details
.restore(snapshot) ⇒ Object
Rebuilds a Document from a snapshot produced by #snapshot. Restoring deep-copies every object, so mutations never leak between fills. Snapshots are an internal format: only feed this data produced by #snapshot in the same process (Marshal is not safe on foreign input).
45 46 47 48 49 50 51 52 |
# File 'lib/acrofill/document.rb', line 45 def self.restore(snapshot) doc = allocate # Snapshots are produced by #snapshot in the same process and never # accepted from external input, so Marshal here is not a deserialization # boundary (see Template docs). doc.send(:restore_state, *Marshal.load(snapshot)) # rubocop:disable Security/MarshalLoad doc end |
Instance Method Details
#add(obj) ⇒ Object
Adds a new indirect object and returns a reference to it.
90 91 92 93 94 |
# File 'lib/acrofill/document.rb', line 90 def add(obj) @max_oid += 1 @objects[@max_oid] = obj PDF::Reader::Reference.new(@max_oid, 0) end |
#deref(obj) ⇒ Object
Follows reference chains (ref -> ref -> value is legal PDF), with a hop cap so a reference cycle cannot loop forever.
79 80 81 82 83 84 85 86 87 |
# File 'lib/acrofill/document.rb', line 79 def deref(obj) hops = 0 while obj.is_a?(PDF::Reader::Reference) return nil if (hops += 1) > 16 obj = @objects[obj.id] end obj end |
#each_page ⇒ Object
Depth-first, document-order walk over the /Pages tree. Iterative with an explicit stack (a deep linear chain must not overflow the Ruby stack) and a visited set (a cyclic or shared billion-laughs tree must not hang or amplify).
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/acrofill/document.rb', line 115 def each_page stack = [root[:Pages]] seen = {} until stack.empty? node = stack.pop dict = deref(node) next unless dict.is_a?(Hash) if node.is_a?(PDF::Reader::Reference) next if seen[node.id] seen[node.id] = true end case node_type(dict) when :Pages kids = deref(dict[:Kids]) stack.concat(kids.reverse) if kids.is_a?(Array) when :Page yield dict end end end |
#inherited_value(node, key) ⇒ Object
Resolves an attribute inheritable through the /Parent chain (field attributes like /FT, /DA, /Q or page attributes).
141 142 143 144 145 146 147 148 149 150 |
# File 'lib/acrofill/document.rb', line 141 def inherited_value(node, key) seen = 0 while node.is_a?(Hash) return node[key] if node.key?(key) return nil if (seen += 1) > 64 # cycle guard node = deref(node[:Parent]) end nil end |
#ref_for(obj) ⇒ Object
Wraps a direct object into an indirect one; passes references through.
97 98 99 |
# File 'lib/acrofill/document.rb', line 97 def ref_for(obj) obj.is_a?(PDF::Reader::Reference) ? obj : add(obj) end |
#root ⇒ Object
The document catalog. The trailer may point at a missing or mistyped object, so this is a parse-boundary check too: callers get an Acrofill::Error rather than a NoMethodError on nil.
104 105 106 107 108 109 |
# File 'lib/acrofill/document.rb', line 104 def root node = deref(@trailer[:Root]) raise Error, 'PDF has no document catalog' unless node.is_a?(Hash) node end |
#snapshot ⇒ Object
A serialized copy of the pristine object graph; see Template.
37 38 39 |
# File 'lib/acrofill/document.rb', line 37 def snapshot Marshal.dump([@objects, @trailer, @max_oid]) end |