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,84 @@
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
+ require 'gtkhex'
22
+
23
+ module PDFWalker
24
+
25
+ class Walker < Window
26
+ private
27
+
28
+ def create_hexview
29
+ @hexview = DumpView.new(self)
30
+ end
31
+
32
+ class DumpView < ScrolledWindow
33
+
34
+ def initialize(parent)
35
+ @parent = parent
36
+ super()
37
+
38
+ set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC)
39
+
40
+ @current_obj = nil
41
+
42
+ @view = HexEditor.new
43
+ @view.show_offsets(true)
44
+
45
+ add_with_viewport @view
46
+ end
47
+
48
+ def clear
49
+ @view.set_data ''
50
+ end
51
+
52
+ def load(object)
53
+ return if @current_obj.equal?(object)
54
+
55
+ self.clear
56
+
57
+ case object
58
+ when Origami::Stream
59
+ load_stream(object)
60
+ when Origami::String
61
+ load_string(object)
62
+ end
63
+
64
+ @current_obj = object
65
+ end
66
+
67
+ private
68
+
69
+ def load_stream(object)
70
+ begin
71
+ @view.set_data(object.data)
72
+ rescue Origami::Filter::Error
73
+ @view.set_data($!.input_data) if $!.input_data
74
+
75
+ @parent.error("#{$!.class}: #{$!.message}") unless object.filters == [ :DCTDecode ]
76
+ end
77
+ end
78
+
79
+ def load_string(object)
80
+ @view.set_data(object.value)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,69 @@
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 ImgViewer < Window
24
+ attr_reader :image
25
+
26
+ def initialize
27
+ super()
28
+
29
+ set_title "Image view"
30
+ set_decorated false
31
+ set_resizable false
32
+
33
+ add_events(Gdk::Event::KEY_RELEASE_MASK)
34
+ signal_connect('key_release_event') { |_, event|
35
+ destroy if event.keyval == Gdk::Keyval::GDK_Escape
36
+ }
37
+ end
38
+
39
+ def show_raw_img(data, w, h, bpc, bpr)
40
+ set_default_size w,h
41
+
42
+ pixbuf = GdkPixbuf::Pixbuf.new data: data,
43
+ colorspace: GdkPixbuf::Colorspace::RGB,
44
+ has_alpha: false,
45
+ bits_per_sample: bpc,
46
+ width: w, height: h,
47
+ row_stride: bpr
48
+
49
+ @image = Gtk::Image.new(pixbuf)
50
+ add @image
51
+
52
+ show_all
53
+ end
54
+
55
+ def show_compressed_img(data)
56
+ loader = GdkPixbuf::PixbufLoader.new
57
+ loader.last_write data
58
+
59
+ pixbuf = loader.pixbuf
60
+ set_default_size pixbuf.width, pixbuf.height
61
+
62
+ @image = Gtk::Image.new(pixbuf)
63
+ add @image
64
+
65
+ show_all
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,382 @@
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
+ module PopupMenu
24
+
25
+ @@menus = Hash.new([])
26
+ @@menus['PDF'] =
27
+ [
28
+ {
29
+ Name: Stock::SAVE_AS,
30
+ Sensitive: true,
31
+ Callback: lambda { |_widget, viewer, _path|
32
+ viewer.parent.save
33
+ }
34
+ },
35
+ {
36
+ Name: :"---"
37
+ },
38
+ {
39
+ Name: Stock::PROPERTIES,
40
+ Sensitive: true,
41
+ Callback: lambda { |_widget, viewer, _path|
42
+ viewer.parent.display_file_properties
43
+ }
44
+ },
45
+ {
46
+ Name: :"---"
47
+ },
48
+ {
49
+ Name: Stock::CLOSE,
50
+ Sensitive: true,
51
+ Callback: lambda { |_widget, viewer, _path|
52
+ viewer.parent.close
53
+ }
54
+ }
55
+ ]
56
+
57
+ @@menus['Reference'] =
58
+ [
59
+ {
60
+ Name: Stock::JUMP_TO,
61
+ Sensitive: true,
62
+ Callback: lambda { |_widget, viewer, path|
63
+ viewer.row_activated(path, viewer.get_column(viewer.class::TEXTCOL))
64
+ }
65
+ }
66
+ ]
67
+
68
+ @@menus['Revision'] =
69
+ [
70
+ {
71
+ Name: "Save to this revision",
72
+ Sensitive: true,
73
+ Callback: lambda { |_widget, viewer, path|
74
+ revstr = viewer.model.get_value(viewer.model.get_iter(path), viewer.class::TEXTCOL)
75
+ revstr.slice!(0, "Revision ".size)
76
+
77
+ revnum = revstr.to_i
78
+
79
+ dialog = Gtk::FileChooserDialog.new("Save PDF File",
80
+ viewer.parent,
81
+ Gtk::FileChooser::ACTION_SAVE,
82
+ nil,
83
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
84
+ [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT]
85
+ )
86
+
87
+ dialog.filter = FileFilter.new.add_pattern("*.pdf")
88
+
89
+ if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT
90
+ viewer.parent.opened.save_upto(revnum, dialog.filename)
91
+ end
92
+
93
+ dialog.destroy
94
+ }
95
+ }
96
+ ]
97
+
98
+ @@menus['Stream'] =
99
+ [
100
+ {
101
+ Name: "Dump encoded stream",
102
+ Sensitive: true,
103
+ Callback: lambda { |_widget, viewer, path|
104
+ stream = viewer.object_by_path(path)
105
+
106
+ viewer.parent.save_data("Save encoded stream to file", stream.encoded_data)
107
+ }
108
+ },
109
+ {
110
+ Name: "Dump decoded stream",
111
+ Sensitive: true,
112
+ Callback: lambda { |_widget, viewer, path|
113
+ stream = viewer.object_by_path(path)
114
+
115
+ viewer.parent.save_data("Save decoded stream to file", stream.data)
116
+ }
117
+ }
118
+ ]
119
+
120
+ @@menus['String'] =
121
+ [
122
+ {
123
+ Name: "Dump string",
124
+ Sensitive: true,
125
+ Callback: lambda { |_widget, viewer, path|
126
+ string = viewer.object_by_path(path)
127
+
128
+ viewer.parent.save_data("Save string to file", string.value)
129
+ }
130
+ }
131
+ ]
132
+
133
+ @@menus['Image'] = @@menus['Stream'] +
134
+ [
135
+ {
136
+ Name: :"---"
137
+ },
138
+ {
139
+ Name: "View image",
140
+ Sensitive: true,
141
+ Callback: lambda { |_widget, viewer, path|
142
+ stm = viewer.object_by_path(path)
143
+ w,h = stm.Width, stm.Height
144
+
145
+ if stm.ColorSpace.nil?
146
+ colors = 1
147
+ else
148
+ colors =
149
+ case stm.ColorSpace.value
150
+ when :DeviceGray then 1
151
+ when :DeviceRGB then 3
152
+ when :DeviceCMYK then 4
153
+ else
154
+ 1
155
+ end
156
+ end
157
+
158
+ bpc = stm.BitsPerComponent || 8
159
+ bpr = (w * colors * bpc + 7) >> 3
160
+ data = stm.data
161
+
162
+ begin
163
+ imgview = ImgViewer.new
164
+ if stm.Filter == :DCTDecode or (stm.Filter.is_a?(Array) and stm.Filter[0] == :DCTDecode)
165
+ imgview.show_compressed_img data
166
+ else
167
+ imgview.show_raw_img data, w, h, bpc, bpr
168
+ end
169
+ rescue
170
+ viewer.parent.error("#{$!.class}: #{$!.message}")
171
+ end
172
+ }
173
+ }
174
+ ]
175
+
176
+ def popup_menu(obj, event, path)
177
+ menu = Menu.new
178
+
179
+ type = popup_menu_key(obj)
180
+
181
+ # Create menu title.
182
+ title = obj.is_a?(Origami::Object) ? "Object : " : ""
183
+ title << type.to_s
184
+ menu.append(MenuItem.new(title).set_sensitive(false).modify_text(Gtk::STATE_INSENSITIVE, Gdk::Color.new(255,0,255)))
185
+
186
+ # Object information.
187
+ create_object_menu(menu, obj) if obj.is_a?(Origami::Object)
188
+
189
+ # Type-specific menu.
190
+ create_type_menu(menu, type, path)
191
+
192
+ menu.show_all
193
+ menu.popup(nil, nil, event.button, event.time)
194
+ end
195
+
196
+ private
197
+
198
+ def create_object_menu(menu, object)
199
+ if object.indirect?
200
+ menu.append(MenuItem.new("Number : #{object.no}; Generation : #{object.generation}").set_sensitive(false))
201
+ menu.append(MenuItem.new("File offset : #{object.file_offset}").set_sensitive(false))
202
+
203
+ getxrefs = MenuItem.new("Search references to this object").set_sensitive(true)
204
+ getxrefs.signal_connect("activate") do
205
+ self.parent.show_xrefs(object)
206
+ end
207
+ menu.append(getxrefs)
208
+
209
+ elsif not object.parent.nil?
210
+ gotoparent = MenuItem.new("Goto Parent Object").set_sensitive(true)
211
+ gotoparent.signal_connect("activate") do
212
+ self.goto(object.parent)
213
+ end
214
+ menu.append(gotoparent)
215
+ end
216
+ end
217
+
218
+ def create_type_menu(menu, type, path)
219
+ items = @@menus[type]
220
+ menu.append(SeparatorMenuItem.new) if not items.empty?
221
+
222
+ items.each do |item|
223
+ if item[:Name] == :"---"
224
+ entry = SeparatorMenuItem.new
225
+ else
226
+ if item[:Name].is_a?(String)
227
+ entry = MenuItem.new(item[:Name])
228
+ else
229
+ entry = ImageMenuItem.new(item[:Name])
230
+ end
231
+
232
+ entry.set_sensitive(item[:Sensitive])
233
+ entry.signal_connect("activate", self, path, &item[:Callback])
234
+ end
235
+
236
+ menu.append(entry)
237
+ end
238
+ end
239
+
240
+ def popup_menu_key(object)
241
+ if object.is_a?(Origami::Object)
242
+ popup_menu_object_key(object)
243
+ else
244
+ popup_menu_struct_key(object)
245
+ end
246
+ end
247
+
248
+ def popup_menu_object_key(object)
249
+ if object.is_a?(Origami::Graphics::ImageXObject)
250
+ 'Image'
251
+ else
252
+ object.native_type.to_s.split("::").last
253
+ end
254
+ end
255
+
256
+ def popup_menu_struct_key(struct)
257
+ case struct
258
+ when ::Array
259
+ 'Body'
260
+ when Origami::XRef, Origami::XRefToCompressedObject
261
+ 'XRef'
262
+ else
263
+ struct.class.name.split('::').last
264
+ end
265
+ end
266
+ end
267
+
268
+ class Walker < Window
269
+
270
+ private
271
+
272
+ def create_menus
273
+ AccelMap.add_entry("<PDF Walker>/File/Open", Gdk::Keyval::GDK_O, Gdk::Window::CONTROL_MASK)
274
+ AccelMap.add_entry("<PDF Walker>/File/Refresh", Gdk::Keyval::GDK_R, Gdk::Window::CONTROL_MASK)
275
+ AccelMap.add_entry("<PDF Walker>/File/Close", Gdk::Keyval::GDK_W, Gdk::Window::CONTROL_MASK)
276
+ AccelMap.add_entry("<PDF Walker>/File/Save", Gdk::Keyval::GDK_S, Gdk::Window::CONTROL_MASK)
277
+ AccelMap.add_entry("<PDF Walker>/File/Quit", Gdk::Keyval::GDK_Q, Gdk::Window::CONTROL_MASK)
278
+ AccelMap.add_entry("<PDF Walker>/Document/Search", Gdk::Keyval::GDK_F, Gdk::Window::CONTROL_MASK)
279
+
280
+ @menu = MenuBar.new
281
+
282
+ create_file_menu
283
+ create_document_menu
284
+ create_help_menu
285
+ end
286
+
287
+ def create_file_menu
288
+ file_ag = Gtk::AccelGroup.new
289
+ @file_menu = Menu.new.set_accel_group(file_ag).set_accel_path("<PDF Walker>/File")
290
+ add_accel_group(file_ag)
291
+
292
+ entries = [
293
+ @file_menu_open = ImageMenuItem.new(Stock::OPEN).set_accel_path("<PDF Walker>/File/Open"),
294
+ @file_menu_recent = MenuItem.new("Last opened"),
295
+ @file_menu_refresh = ImageMenuItem.new(Stock::REFRESH).set_sensitive(false).set_accel_path("<PDF Walker>/File/Refresh"),
296
+ @file_menu_close = ImageMenuItem.new(Stock::CLOSE).set_sensitive(false).set_accel_path("<PDF Walker>/File/Close"),
297
+ @file_menu_saveas = ImageMenuItem.new(Stock::SAVE_AS).set_sensitive(false).set_accel_path("<PDF Walker>/File/Save"),
298
+ @file_menu_exit = ImageMenuItem.new(Stock::QUIT).set_accel_path("<PDF Walker>/File/Quit"),
299
+ ]
300
+
301
+ @file_menu_open.signal_connect('activate') { open }
302
+ @file_menu_refresh.signal_connect('activate') { open(@filename) }
303
+ @file_menu_close.signal_connect('activate') { close }
304
+ @file_menu_saveas.signal_connect('activate') { save }
305
+ @file_menu_exit.signal_connect('activate') { self.destroy }
306
+
307
+ update_recent_menu
308
+
309
+ entries.each do |entry|
310
+ @file_menu.append(entry)
311
+ end
312
+
313
+ @menu.append(MenuItem.new('_File').set_submenu(@file_menu))
314
+ end
315
+
316
+ def create_document_menu
317
+ doc_ag = Gtk::AccelGroup.new
318
+ @document_menu = Menu.new.set_accel_group(doc_ag)
319
+ add_accel_group(doc_ag)
320
+
321
+ entries = [
322
+ @document_menu_search = ImageMenuItem.new(Stock::FIND).set_sensitive(false).set_accel_path("<PDF Walker>/Document/Search"),
323
+ MenuItem.new,
324
+ @document_menu_gotocatalog = MenuItem.new("Jump to Catalog").set_sensitive(false),
325
+ @document_menu_gotodocinfo = MenuItem.new("Jump to Document Info").set_sensitive(false),
326
+ @document_menu_gotometadata = MenuItem.new("Jump to Metadata").set_sensitive(false),
327
+ @document_menu_gotorev = MenuItem.new("Jump to Revision...").set_sensitive(false),
328
+ @document_menu_gotopage = MenuItem.new("Jump to Page...").set_sensitive(false),
329
+ @document_menu_gotofield = MenuItem.new("Jump to Field...").set_sensitive(false),
330
+ @document_menu_gotoobj = MenuItem.new("Jump to Object...").set_sensitive(false),
331
+ MenuItem.new,
332
+ @document_menu_sign = MenuItem.new("Sign the document").set_sensitive(false),
333
+ @document_menu_ur = MenuItem.new("Enable Usage Rights").set_sensitive(false),
334
+ @document_menu_properties = ImageMenuItem.new(Stock::PROPERTIES).set_sensitive(false),
335
+ ]
336
+
337
+ @document_menu_search.signal_connect('activate') { search }
338
+ @document_menu_gotocatalog.signal_connect('activate') { goto_catalog }
339
+ @document_menu_gotodocinfo.signal_connect('activate') { goto_docinfo }
340
+ @document_menu_gotometadata.signal_connect('activate') { goto_metadata }
341
+ @document_menu_gotoobj.signal_connect('activate') { goto_object }
342
+ @document_menu_properties.signal_connect('activate') { display_file_properties }
343
+ @document_menu_sign.signal_connect('activate') { display_signing_wizard }
344
+ @document_menu_ur.signal_connect('activate') { display_usage_rights_wizard }
345
+
346
+ entries.each do |entry|
347
+ @document_menu.append(entry)
348
+ end
349
+
350
+ @menu.append(MenuItem.new('_Document').set_submenu(@document_menu))
351
+ end
352
+
353
+ def create_help_menu
354
+ @help_menu = Menu.new
355
+
356
+ @help_menu_profile = CheckMenuItem.new("Profiling (Debug purposes only)").set_active(@config.profile?)
357
+ @help_menu_about = ImageMenuItem.new(Stock::ABOUT)
358
+
359
+ @help_menu_profile.signal_connect('toggled') do @config.set_profiling(@help_menu_profile.active?) end
360
+ @help_menu_about.signal_connect('activate') { about }
361
+
362
+ @help_menu.append(@help_menu_profile)
363
+ @help_menu.append(@help_menu_about)
364
+
365
+ @menu.append(MenuItem.new('_Help').set_submenu(@help_menu))
366
+ end
367
+
368
+ def update_recent_menu
369
+ @recent_menu = Menu.new
370
+ @config.recent_files.each { |file|
371
+ menu = MenuItem.new(file)
372
+ menu.signal_connect('activate') do open(file) end
373
+
374
+ @recent_menu.append(menu)
375
+ }
376
+
377
+ @file_menu_recent.set_submenu(@recent_menu)
378
+ @file_menu_recent.show_all
379
+ end
380
+ end
381
+
382
+ end