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.
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
-
#snapshot ⇒ Object
A serialized copy of the pristine object graph; see Template.
Constructor Details
#initialize(path) ⇒ Document
Returns a new instance of Document.
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/acrofill/document.rb', line 16 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.
14 15 16 |
# File 'lib/acrofill/document.rb', line 14 def objects @objects end |
#trailer ⇒ Object (readonly)
Returns the value of attribute trailer.
14 15 16 |
# File 'lib/acrofill/document.rb', line 14 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).
43 44 45 46 47 48 49 50 |
# File 'lib/acrofill/document.rb', line 43 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.
88 89 90 91 92 |
# File 'lib/acrofill/document.rb', line 88 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.
77 78 79 80 81 82 83 84 85 |
# File 'lib/acrofill/document.rb', line 77 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).
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/acrofill/document.rb', line 107 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 dict[:Type] 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).
133 134 135 136 137 138 139 140 141 142 |
# File 'lib/acrofill/document.rb', line 133 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.
95 96 97 |
# File 'lib/acrofill/document.rb', line 95 def ref_for(obj) obj.is_a?(PDF::Reader::Reference) ? obj : add(obj) end |
#root ⇒ Object
99 100 101 |
# File 'lib/acrofill/document.rb', line 99 def root deref(@trailer[:Root]) end |
#snapshot ⇒ Object
A serialized copy of the pristine object graph; see Template.
35 36 37 |
# File 'lib/acrofill/document.rb', line 35 def snapshot Marshal.dump([@objects, @trailer, @max_oid]) end |