pdfwalker 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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