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.
@@ -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