Class: Acrofill::Document

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ Document

Returns a new instance of Document.

Raises:



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

#objectsObject (readonly)

Returns the value of attribute objects.



14
15
16
# File 'lib/acrofill/document.rb', line 14

def objects
  @objects
end

#trailerObject (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_pageObject

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

#rootObject



99
100
101
# File 'lib/acrofill/document.rb', line 99

def root
  deref(@trailer[:Root])
end

#snapshotObject

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