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.
-
#normalized_box(raw) ⇒ Object
A /Rect or /BBox as [llx, lly, urx, ury]: the value and each corner may be indirect, and the corners may be given in either order.
-
#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).
162 163 164 165 166 167 168 169 170 171 |
# File 'lib/acrofill/document.rb', line 162 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 |
#normalized_box(raw) ⇒ Object
A /Rect or /BBox as [llx, lly, urx, ury]: the value and each corner may be indirect, and the corners may be given in either order. Returns nil when it is not four finite numbers — a non-finite corner would subtract into a NaN width or height, which every later comparison silently passes and every later clamp raises on.
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/acrofill/document.rb', line 144 def normalized_box(raw) box = deref(raw) return nil unless box.is_a?(Array) && box.size == 4 corners = box.map { |value| deref(value) } return nil unless corners.all? { |corner| corner.is_a?(Numeric) && corner.to_f.finite? } xs = [corners[0].to_f, corners[2].to_f].sort ys = [corners[1].to_f, corners[3].to_f].sort # Corners far enough apart still overflow when subtracted, and the # width and height every caller derives have to be usable too. return nil unless (xs[1] - xs[0]).finite? && (ys[1] - ys[0]).finite? [xs[0], ys[0], xs[1], ys[1]] 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 |