pdfwalker 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/COPYING +674 -0
- data/bin/pdfwalker +30 -0
- data/lib/pdfwalker.rb +21 -0
- data/lib/pdfwalker/about.rb +37 -0
- data/lib/pdfwalker/config.rb +120 -0
- data/lib/pdfwalker/file.rb +320 -0
- data/lib/pdfwalker/hexview.rb +84 -0
- data/lib/pdfwalker/imgview.rb +69 -0
- data/lib/pdfwalker/menu.rb +382 -0
- data/lib/pdfwalker/properties.rb +126 -0
- data/lib/pdfwalker/signing.rb +558 -0
- data/lib/pdfwalker/textview.rb +88 -0
- data/lib/pdfwalker/treeview.rb +416 -0
- data/lib/pdfwalker/version.rb +23 -0
- data/lib/pdfwalker/walker.rb +300 -0
- data/lib/pdfwalker/xrefs.rb +75 -0
- metadata +103 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This file is part of PDF Walker, a graphical PDF file browser
|
4
|
+
Copyright (C) 2017 Guillaume Delugré.
|
5
|
+
|
6
|
+
PDF Walker is free software: you can redistribute it and/or modify
|
7
|
+
it under the terms of the GNU General Public License as published by
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
9
|
+
(at your option) any later version.
|
10
|
+
|
11
|
+
PDF Walker is distributed in the hope that it will be useful,
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
GNU General Public License for more details.
|
15
|
+
|
16
|
+
You should have received a copy of the GNU General Public License
|
17
|
+
along with PDF Walker. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
=end
|
20
|
+
|
21
|
+
module PDFWalker
|
22
|
+
|
23
|
+
class Walker < Window
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def create_objectview
|
28
|
+
@objectview = ObjectView.new(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
class ObjectView < Notebook
|
32
|
+
attr_reader :parent
|
33
|
+
attr_reader :pdfpanel, :valuepanel
|
34
|
+
|
35
|
+
def initialize(parent)
|
36
|
+
@parent = parent
|
37
|
+
super()
|
38
|
+
|
39
|
+
@pdfbuffer = TextBuffer.new
|
40
|
+
@pdfview = TextView.new(@pdfbuffer).set_editable(false).set_cursor_visible(false).set_left_margin(5)
|
41
|
+
|
42
|
+
@pdfpanel = ScrolledWindow.new.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
|
43
|
+
@pdfpanel.add_with_viewport @pdfview
|
44
|
+
append_page(@pdfpanel, Label.new("PDF Code"))
|
45
|
+
|
46
|
+
@pdfbuffer.create_tag("Default",
|
47
|
+
weight: Pango::Weight::BOLD,
|
48
|
+
family: "monospace",
|
49
|
+
scale: Pango::Scale::LARGE
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def load(object)
|
54
|
+
begin
|
55
|
+
self.clear
|
56
|
+
|
57
|
+
case object
|
58
|
+
when Origami::PDF::Header, Origami::FDF::Header, Origami::PPKLite::Header
|
59
|
+
text = object.to_s
|
60
|
+
@pdfbuffer.set_text(text)
|
61
|
+
@pdfbuffer.apply_tag("Default", @pdfbuffer.start_iter, @pdfbuffer.end_iter)
|
62
|
+
|
63
|
+
when Origami::Object
|
64
|
+
if object.is_a?(Origami::Stream)
|
65
|
+
text = [ "#{object.no} #{object.generation} obj", object.dictionary ].join($/)
|
66
|
+
else
|
67
|
+
text = object.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
text.encode!("UTF-8", replace: '.')
|
71
|
+
.tr!("\x00", '.')
|
72
|
+
|
73
|
+
@pdfbuffer.set_text(text)
|
74
|
+
@pdfbuffer.apply_tag("Default", @pdfbuffer.start_iter, @pdfbuffer.end_iter)
|
75
|
+
end
|
76
|
+
|
77
|
+
rescue
|
78
|
+
@parent.error("An error occured while loading this object.\n#{$!} (#{$!.class})")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def clear
|
83
|
+
@pdfbuffer.set_text("")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,416 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This file is part of PDF Walker, a graphical PDF file browser
|
4
|
+
Copyright (C) 2017 Guillaume Delugré.
|
5
|
+
|
6
|
+
PDF Walker is free software: you can redistribute it and/or modify
|
7
|
+
it under the terms of the GNU General Public License as published by
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
9
|
+
(at your option) any later version.
|
10
|
+
|
11
|
+
PDF Walker is distributed in the hope that it will be useful,
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
GNU General Public License for more details.
|
15
|
+
|
16
|
+
You should have received a copy of the GNU General Public License
|
17
|
+
along with PDF Walker. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
=end
|
20
|
+
|
21
|
+
module PDFWalker
|
22
|
+
|
23
|
+
class Walker < Window
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def create_treeview
|
28
|
+
@treeview = PDFTree.new(self).set_headers_visible(false)
|
29
|
+
|
30
|
+
colcontent = Gtk::TreeViewColumn.new("Names",
|
31
|
+
Gtk::CellRendererText.new.set_foreground_set(true).set_background_set(true),
|
32
|
+
text: PDFTree::TEXTCOL,
|
33
|
+
weight: PDFTree::WEIGHTCOL,
|
34
|
+
style: PDFTree::STYLECOL,
|
35
|
+
foreground: PDFTree::FGCOL,
|
36
|
+
background: PDFTree::BGCOL
|
37
|
+
)
|
38
|
+
|
39
|
+
@treeview.append_column(colcontent)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class PDFTree < TreeView
|
44
|
+
include PopupMenu
|
45
|
+
|
46
|
+
OBJCOL = 0
|
47
|
+
TEXTCOL = 1
|
48
|
+
WEIGHTCOL = 2
|
49
|
+
STYLECOL = 3
|
50
|
+
FGCOL = 4
|
51
|
+
BGCOL = 5
|
52
|
+
LOADCOL = 6
|
53
|
+
|
54
|
+
@@appearance = Hash.new(Weight: :normal, Style: :normal)
|
55
|
+
|
56
|
+
attr_reader :parent
|
57
|
+
|
58
|
+
def initialize(parent)
|
59
|
+
@parent = parent
|
60
|
+
|
61
|
+
reset_appearance
|
62
|
+
|
63
|
+
@treestore = TreeStore.new(Object::Object, String, Pango::Weight, Pango::Style, String, String, Integer)
|
64
|
+
super(@treestore)
|
65
|
+
|
66
|
+
signal_connect('cursor-changed') {
|
67
|
+
iter = selection.selected
|
68
|
+
if iter
|
69
|
+
obj = @treestore.get_value(iter, OBJCOL)
|
70
|
+
|
71
|
+
parent.hexview.load(obj)
|
72
|
+
parent.objectview.load(obj)
|
73
|
+
end
|
74
|
+
}
|
75
|
+
|
76
|
+
signal_connect('row-activated') { |_tree, path,_column|
|
77
|
+
if selection.selected
|
78
|
+
obj = @treestore.get_value(selection.selected, OBJCOL)
|
79
|
+
|
80
|
+
if row_expanded?(path)
|
81
|
+
collapse_row(path)
|
82
|
+
else
|
83
|
+
expand_row(path, false)
|
84
|
+
end
|
85
|
+
|
86
|
+
goto(obj) if obj.is_a?(Origami::Reference)
|
87
|
+
end
|
88
|
+
}
|
89
|
+
|
90
|
+
signal_connect('row-expanded') { |_tree, iter, _path|
|
91
|
+
obj = @treestore.get_value(iter, OBJCOL)
|
92
|
+
|
93
|
+
if obj.is_a?(Origami::Stream) and iter.n_children == 1
|
94
|
+
|
95
|
+
# Processing with an XRef or Object Stream
|
96
|
+
if obj.is_a?(Origami::ObjectStream)
|
97
|
+
obj.each { |embeddedobj|
|
98
|
+
load_object(iter, embeddedobj)
|
99
|
+
}
|
100
|
+
|
101
|
+
elsif obj.is_a?(Origami::XRefStream)
|
102
|
+
obj.each { |xref|
|
103
|
+
load_xrefstm(iter, xref)
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
for i in 0...iter.n_children
|
109
|
+
subiter = iter.nth_child(i)
|
110
|
+
subobj = @treestore.get_value(subiter, OBJCOL)
|
111
|
+
|
112
|
+
load_sub_objects(subiter, subobj)
|
113
|
+
end
|
114
|
+
}
|
115
|
+
|
116
|
+
add_events(Gdk::Event::BUTTON_PRESS_MASK)
|
117
|
+
signal_connect('button_press_event') { |_widget, event|
|
118
|
+
if event.button == 3 && parent.opened
|
119
|
+
path = get_path(event.x,event.y).first
|
120
|
+
set_cursor(path, nil, false)
|
121
|
+
|
122
|
+
obj = @treestore.get_value(@treestore.get_iter(path), OBJCOL)
|
123
|
+
popup_menu(obj, event, path)
|
124
|
+
end
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def clear
|
129
|
+
@treestore.clear
|
130
|
+
end
|
131
|
+
|
132
|
+
def goto(obj, follow_references: true)
|
133
|
+
if obj.is_a?(TreePath)
|
134
|
+
set_cursor(obj, nil, false)
|
135
|
+
else
|
136
|
+
if obj.is_a?(Origami::Name) and obj.parent.is_a?(Origami::Dictionary) and obj.parent.has_key?(obj)
|
137
|
+
obj = obj.parent[obj]
|
138
|
+
elsif obj.is_a?(Origami::Reference) and follow_references
|
139
|
+
obj =
|
140
|
+
begin
|
141
|
+
obj.solve
|
142
|
+
rescue Origami::InvalidReferenceError
|
143
|
+
@parent.error("Object not found : #{obj}")
|
144
|
+
return
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
_, path = object_to_tree_pos(obj)
|
149
|
+
if path.nil?
|
150
|
+
@parent.error("Object not found : #{obj.type}")
|
151
|
+
return
|
152
|
+
end
|
153
|
+
|
154
|
+
expand_to_path(path) unless row_expanded?(path)
|
155
|
+
@parent.explorer_history << cursor.first if cursor.first
|
156
|
+
set_cursor(path, nil, false)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def highlight(obj, color)
|
161
|
+
if obj.is_a?(Origami::Name) and obj.parent.is_a?(Origami::Dictionary) and obj.parent.has_key?(obj)
|
162
|
+
obj = obj.parent[obj]
|
163
|
+
end
|
164
|
+
|
165
|
+
iter, path = object_to_tree_pos(obj)
|
166
|
+
if iter.nil? or path.nil?
|
167
|
+
@parent.error("Object not found : #{obj.type}")
|
168
|
+
return
|
169
|
+
end
|
170
|
+
|
171
|
+
@treestore.set_value(iter, BGCOL, color)
|
172
|
+
expand_to_path(path) unless row_expanded?(path)
|
173
|
+
end
|
174
|
+
|
175
|
+
def load(pdf)
|
176
|
+
return unless pdf
|
177
|
+
|
178
|
+
self.clear
|
179
|
+
|
180
|
+
begin
|
181
|
+
#
|
182
|
+
# Create root entry
|
183
|
+
#
|
184
|
+
root = @treestore.append(nil)
|
185
|
+
@treestore.set_value(root, OBJCOL, pdf)
|
186
|
+
|
187
|
+
set_node(root, :Filename, @parent.filename)
|
188
|
+
|
189
|
+
#
|
190
|
+
# Create header entry
|
191
|
+
#
|
192
|
+
header = @treestore.append(root)
|
193
|
+
@treestore.set_value(header, OBJCOL, pdf.header)
|
194
|
+
|
195
|
+
set_node(header, :Header,
|
196
|
+
"Header (version #{pdf.header.major_version}.#{pdf.header.minor_version})")
|
197
|
+
|
198
|
+
no = 1
|
199
|
+
pdf.revisions.each { |revision|
|
200
|
+
load_revision(root, no, revision)
|
201
|
+
no = no + 1
|
202
|
+
}
|
203
|
+
|
204
|
+
set_model(@treestore)
|
205
|
+
|
206
|
+
ensure
|
207
|
+
expand(@treestore.iter_first, 3)
|
208
|
+
set_cursor(@treestore.iter_first.path, nil, false)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def object_by_path(path)
|
213
|
+
iter = @treestore.get_iter(path)
|
214
|
+
|
215
|
+
@treestore.get_value(iter, OBJCOL)
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def object_to_tree_pos(obj)
|
221
|
+
|
222
|
+
# Locate the indirect object.
|
223
|
+
root_obj = obj
|
224
|
+
object_path = [ root_obj ]
|
225
|
+
while root_obj.parent
|
226
|
+
root_obj = root_obj.parent
|
227
|
+
object_path.push(root_obj)
|
228
|
+
end
|
229
|
+
|
230
|
+
@treestore.each do |_model, path, iter|
|
231
|
+
current_obj = @treestore.get_value(iter, OBJCOL)
|
232
|
+
|
233
|
+
# Load the intermediate nodes if necessary.
|
234
|
+
if object_path.any?{|object| object.equal?(current_obj)}
|
235
|
+
load_sub_objects(iter, current_obj)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Unfold the object stream if it's in the object path.
|
239
|
+
if obj.is_a?(Origami::Object) and current_obj.is_a?(Origami::ObjectStream) and
|
240
|
+
root_obj.equal?(current_obj) and iter.n_children == 1
|
241
|
+
|
242
|
+
current_obj.each { |embeddedobj|
|
243
|
+
load_object(iter, embeddedobj)
|
244
|
+
}
|
245
|
+
end
|
246
|
+
|
247
|
+
return [ iter, path ] if obj.equal?(current_obj)
|
248
|
+
end
|
249
|
+
|
250
|
+
nil
|
251
|
+
end
|
252
|
+
|
253
|
+
def expand(row, depth)
|
254
|
+
if row and depth != 0
|
255
|
+
loop do
|
256
|
+
expand_row(row.path, false)
|
257
|
+
expand(row.first_child, depth - 1)
|
258
|
+
|
259
|
+
break if not row.next!
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def load_revision(root, no, revision)
|
265
|
+
revroot = @treestore.append(root)
|
266
|
+
@treestore.set_value(revroot, OBJCOL, revision)
|
267
|
+
|
268
|
+
set_node(revroot, :Revision, "Revision #{no}")
|
269
|
+
|
270
|
+
load_body(revroot, revision.body.values)
|
271
|
+
load_xrefs(revroot, revision.xreftable)
|
272
|
+
load_trailer(revroot, revision.trailer)
|
273
|
+
end
|
274
|
+
|
275
|
+
def load_body(rev, body)
|
276
|
+
bodyroot = @treestore.append(rev)
|
277
|
+
@treestore.set_value(bodyroot, OBJCOL, body)
|
278
|
+
|
279
|
+
set_node(bodyroot, :Body, "Body")
|
280
|
+
|
281
|
+
body.sort_by{|obj| obj.file_offset.to_i }.each { |object|
|
282
|
+
begin
|
283
|
+
load_object(bodyroot, object)
|
284
|
+
rescue
|
285
|
+
msg = "#{$!.class}: #{$!.message}\n#{$!.backtrace.join($/)}"
|
286
|
+
STDERR.puts(msg)
|
287
|
+
|
288
|
+
#@parent.error(msg)
|
289
|
+
next
|
290
|
+
end
|
291
|
+
}
|
292
|
+
end
|
293
|
+
|
294
|
+
def load_object(container, object, depth = 1, name = nil)
|
295
|
+
iter = @treestore.append(container)
|
296
|
+
@treestore.set_value(iter, OBJCOL, object)
|
297
|
+
|
298
|
+
type = object.native_type.to_s.split('::').last.to_sym
|
299
|
+
|
300
|
+
if name.nil?
|
301
|
+
name =
|
302
|
+
case object
|
303
|
+
when Origami::String
|
304
|
+
'"' + object.to_utf8.tr("\x00", ".") + '"'
|
305
|
+
when Origami::Number, Origami::Name
|
306
|
+
object.value.to_s
|
307
|
+
else
|
308
|
+
object.type.to_s
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
set_node(iter, type, name)
|
313
|
+
return unless depth > 0
|
314
|
+
|
315
|
+
load_sub_objects(iter, object, depth)
|
316
|
+
end
|
317
|
+
|
318
|
+
def load_sub_objects(container, object, depth = 1)
|
319
|
+
return unless depth > 0 and @treestore.get_value(container, LOADCOL) != 1
|
320
|
+
|
321
|
+
case object
|
322
|
+
when Origami::Array
|
323
|
+
object.each do |subobject|
|
324
|
+
load_object(container, subobject, depth - 1)
|
325
|
+
end
|
326
|
+
|
327
|
+
when Origami::Dictionary
|
328
|
+
object.each_key do |subkey|
|
329
|
+
load_object(container, object[subkey.value], depth - 1, subkey.value.to_s)
|
330
|
+
end
|
331
|
+
|
332
|
+
when Origami::Stream
|
333
|
+
load_object(container, object.dictionary, depth - 1, "Stream Dictionary")
|
334
|
+
end
|
335
|
+
|
336
|
+
@treestore.set_value(container, LOADCOL, 1)
|
337
|
+
end
|
338
|
+
|
339
|
+
def load_xrefstm(stm, embxref)
|
340
|
+
xref = @treestore.append(stm)
|
341
|
+
@treestore.set_value(xref, OBJCOL, embxref)
|
342
|
+
|
343
|
+
if embxref.is_a?(Origami::XRef)
|
344
|
+
set_node(xref, :XRef, embxref.to_s.chomp)
|
345
|
+
else
|
346
|
+
set_node(xref, :XRef, "xref to ObjectStream #{embxref.objstmno}, object index #{embxref.index}")
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def load_xrefs(rev, table)
|
351
|
+
return unless table
|
352
|
+
|
353
|
+
section = @treestore.append(rev)
|
354
|
+
@treestore.set_value(section, OBJCOL, table)
|
355
|
+
|
356
|
+
set_node(section, :XRefSection, "XRef section")
|
357
|
+
|
358
|
+
table.each_subsection { |subtable|
|
359
|
+
subsection = @treestore.append(section)
|
360
|
+
@treestore.set_value(subsection, OBJCOL, subtable)
|
361
|
+
|
362
|
+
set_node(subsection, :XRefSubSection, "#{subtable.range.begin} #{subtable.range.end - subtable.range.begin + 1}")
|
363
|
+
|
364
|
+
subtable.each { |entry|
|
365
|
+
xref = @treestore.append(subsection)
|
366
|
+
@treestore.set_value(xref, OBJCOL, entry)
|
367
|
+
|
368
|
+
set_node(xref, :XRef, entry.to_s.chomp)
|
369
|
+
}
|
370
|
+
}
|
371
|
+
end
|
372
|
+
|
373
|
+
def load_trailer(rev, trailer)
|
374
|
+
trailer_root = @treestore.append(rev)
|
375
|
+
@treestore.set_value(trailer_root, OBJCOL, trailer)
|
376
|
+
|
377
|
+
set_node(trailer_root, :Trailer, "Trailer")
|
378
|
+
load_object(trailer_root, trailer.dictionary) unless trailer.dictionary.nil?
|
379
|
+
end
|
380
|
+
|
381
|
+
def reset_appearance
|
382
|
+
@@appearance[:Filename] = {Weight: :bold, Style: :normal}
|
383
|
+
@@appearance[:Header] = {Color: "darkgreen", Weight: :bold, Style: :normal}
|
384
|
+
@@appearance[:Revision] = {Color: "blue", Weight: :bold, Style: :normal}
|
385
|
+
@@appearance[:Body] = {Color: "purple", Weight: :bold, Style: :normal}
|
386
|
+
@@appearance[:XRefSection] = {Color: "purple", Weight: :bold, Style: :normal}
|
387
|
+
@@appearance[:XRefSubSection] = {Color: "brown", Weight: :bold, Style: :normal}
|
388
|
+
@@appearance[:XRef] = {Weight: :bold, Style: :normal}
|
389
|
+
@@appearance[:Trailer] = {Color: "purple", Weight: :bold, Style: :normal}
|
390
|
+
@@appearance[:StartXref] = {Weight: :bold, Style: :normal}
|
391
|
+
@@appearance[:String] = {Color: "red", Weight: :normal, Style: :italic}
|
392
|
+
@@appearance[:Name] = {Color: "gray", Weight: :normal, Style: :italic}
|
393
|
+
@@appearance[:Number] = {Color: "orange", Weight: :normal, Style: :normal}
|
394
|
+
@@appearance[:Dictionary] = {Color: "brown", Weight: :bold, Style: :normal}
|
395
|
+
@@appearance[:Stream] = {Color: "darkcyan", Weight: :bold, Style: :normal}
|
396
|
+
@@appearance[:StreamData] = {Color: "darkcyan", Weight: :normal, Style: :oblique}
|
397
|
+
@@appearance[:Array] = {Color: "darkgreen", Weight: :bold, Style: :normal}
|
398
|
+
@@appearance[:Reference] = {Weight: :normal, Style: :oblique}
|
399
|
+
@@appearance[:Boolean] = {Color: "deeppink", Weight: :normal, Style: :normal}
|
400
|
+
end
|
401
|
+
|
402
|
+
def get_object_appearance(type)
|
403
|
+
@@appearance[type]
|
404
|
+
end
|
405
|
+
|
406
|
+
def set_node(node, type, text)
|
407
|
+
@treestore.set_value(node, TEXTCOL, text)
|
408
|
+
|
409
|
+
app = get_object_appearance(type)
|
410
|
+
@treestore.set_value(node, WEIGHTCOL, app[:Weight])
|
411
|
+
@treestore.set_value(node, STYLECOL, app[:Style])
|
412
|
+
@treestore.set_value(node, FGCOL, app[:Color])
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
end
|