pdfwalker 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'pdfwalker'
5
+
6
+ OptionParser.new do |opts|
7
+ opts.banner = <<-BANNER
8
+ Usage: #{$0} [options] [PDF FILE]
9
+ PDFWalker is a frontend for exploring PDF objects, based on Origami.
10
+
11
+ Options:
12
+ BANNER
13
+
14
+ opts.on("-v", "--version", "Print version number.") do
15
+ puts PDFWalker::VERSION
16
+ exit
17
+ end
18
+
19
+ opts.on_tail("-h", "--help", "Show this message.") do
20
+ puts opts
21
+ exit
22
+ end
23
+ end
24
+ .parse!(ARGV)
25
+
26
+ if ARGV.size > 1
27
+ abort "Error: too many arguments."
28
+ end
29
+
30
+ PDFWalker::Walker.start(ARGV[0])
@@ -0,0 +1,21 @@
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_relative 'pdfwalker/walker'
@@ -0,0 +1,37 @@
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
+ def about
26
+ AboutDialog.show(self,
27
+ name: "PDF Walker",
28
+ program_name: "PDF Walker",
29
+ version: "#{PDFWalker::VERSION}",
30
+ copyright: "Copyright © 2017\nGuillaume Delugré",
31
+ comments: "A PDF file explorer, based on Origami",
32
+ license: File.read(File.join(__dir__, "../..", "COPYING")),
33
+ wrap_license: true,
34
+ )
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,120 @@
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 'origami'
22
+ require 'yaml'
23
+
24
+ module PDFWalker
25
+
26
+ class Walker < Window
27
+
28
+ class Config
29
+ DEFAULT_CONFIG_FILE = "#{File.expand_path("~")}/.pdfwalker.conf.yml"
30
+ DEFAULT_CONFIG =
31
+ {
32
+ "Debug" =>
33
+ {
34
+ "Profiling" => false,
35
+ "ProfilingOutputDir" => "prof",
36
+ "Verbosity" => Origami::Parser::VERBOSE_TRACE,
37
+ "IgnoreFileHeader" => true
38
+ },
39
+
40
+ "UI" =>
41
+ {
42
+ "LastOpenedDocuments" => []
43
+ }
44
+ }
45
+ NLOG_RECENT_FILES = 5
46
+
47
+ def initialize(configfile = DEFAULT_CONFIG_FILE)
48
+ begin
49
+ @conf = YAML.load(File.open(configfile))
50
+ rescue
51
+ @conf = DEFAULT_CONFIG
52
+ ensure
53
+ @filename = configfile
54
+ set_missing_values
55
+ end
56
+ end
57
+
58
+ def last_opened_file(filepath)
59
+ @conf["UI"]['LastOpenedDocuments'].push(filepath).uniq!
60
+ @conf["UI"]['LastOpenedDocuments'].delete_at(0) while @conf["UI"]['LastOpenedDocuments'].size > NLOG_RECENT_FILES
61
+
62
+ save
63
+ end
64
+
65
+ def recent_files(n = NLOG_RECENT_FILES)
66
+ @conf["UI"]['LastOpenedDocuments'].last(n).reverse
67
+ end
68
+
69
+ def set_profiling(bool)
70
+ @conf["Debug"]['Profiling'] = bool
71
+ save
72
+ end
73
+
74
+ def profile?
75
+ @conf["Debug"]['Profiling']
76
+ end
77
+
78
+ def profile_output_dir
79
+ @conf["Debug"]['ProfilingOutputDir']
80
+ end
81
+
82
+ def set_ignore_header(bool)
83
+ @conf["Debug"]['IgnoreFileHeader'] = bool
84
+ save
85
+ end
86
+
87
+ def ignore_header?
88
+ @conf["Debug"]['IgnoreFileHeader']
89
+ end
90
+
91
+ def set_verbosity(level)
92
+ @conf["Debug"]['Verbosity'] = level
93
+ save
94
+ end
95
+
96
+ def verbosity
97
+ @conf["Debug"]['Verbosity']
98
+ end
99
+
100
+ def save
101
+ File.open(@filename, "w").write(@conf.to_yaml)
102
+ end
103
+
104
+ private
105
+
106
+ def set_missing_values
107
+ @conf ||= {}
108
+
109
+ DEFAULT_CONFIG.each_key do |cat|
110
+ @conf[cat] = {} unless @conf.include?(cat)
111
+
112
+ DEFAULT_CONFIG[cat].each_pair do |key, value|
113
+ @conf[cat][key] = value unless @conf[cat].include?(key)
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ end
120
+ end
@@ -0,0 +1,320 @@
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 'origami'
22
+
23
+ module PDFWalker
24
+
25
+ class Walker < Window
26
+ attr_reader :opened
27
+ attr_reader :explore_history
28
+
29
+ def close
30
+ @opened = nil
31
+ @filename = ''
32
+ @explorer_history.clear
33
+
34
+ @treeview.clear
35
+ @objectview.clear
36
+ @hexview.clear
37
+
38
+ # disable all menus.
39
+ [
40
+ @file_menu_close, @file_menu_saveas, @file_menu_refresh,
41
+ @document_menu_search,
42
+ @document_menu_gotocatalog, @document_menu_gotodocinfo, @document_menu_gotometadata,
43
+ @document_menu_gotopage, @document_menu_gotofield, @document_menu_gotorev, @document_menu_gotoobj,
44
+ @document_menu_properties, @document_menu_sign, @document_menu_ur
45
+ ].each do |menu|
46
+ menu.sensitive = false
47
+ end
48
+
49
+ @statusbar.pop(@main_context)
50
+
51
+ GC.start
52
+ end
53
+
54
+ def open(filename = nil)
55
+ dialog = Gtk::FileChooserDialog.new("Open PDF File",
56
+ self,
57
+ FileChooser::ACTION_OPEN,
58
+ nil,
59
+ [Stock::CANCEL, Dialog::RESPONSE_CANCEL],
60
+ [Stock::OPEN, Dialog::RESPONSE_ACCEPT])
61
+
62
+ last_file = @config.recent_files.first
63
+ unless last_file.nil?
64
+ last_folder = File.dirname(last_file)
65
+ dialog.set_current_folder(last_folder) if File.directory?(last_folder)
66
+ end
67
+
68
+ dialog.filter = FileFilter.new.add_pattern("*.acrodata").add_pattern("*.pdf").add_pattern("*.fdf")
69
+
70
+ if filename.nil? and dialog.run != Gtk::Dialog::RESPONSE_ACCEPT
71
+ dialog.destroy
72
+ return
73
+ end
74
+
75
+ create_progressbar
76
+
77
+ filename ||= dialog.filename
78
+ dialog.destroy
79
+
80
+ begin
81
+ document = start_profiling do
82
+ parse_file(filename)
83
+ end
84
+
85
+ set_active_document(filename, document)
86
+
87
+ rescue
88
+ error("Error while parsing file.\n#{$!} (#{$!.class})\n" + $!.backtrace.join("\n"))
89
+ ensure
90
+ close_progressbar
91
+ self.activate_focus
92
+ end
93
+ end
94
+
95
+ def save_data(caption, data, filename = "")
96
+ dialog = Gtk::FileChooserDialog.new(caption,
97
+ self,
98
+ Gtk::FileChooser::ACTION_SAVE,
99
+ nil,
100
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
101
+ [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT]
102
+ )
103
+
104
+ dialog.do_overwrite_confirmation = true
105
+ dialog.current_name = File.basename(filename)
106
+ dialog.filter = FileFilter.new.add_pattern("*.*")
107
+
108
+ if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT
109
+ begin
110
+ File.binwrite(dialog.filename, data)
111
+ rescue
112
+ error("Error: #{$!.message}")
113
+ end
114
+ end
115
+
116
+ dialog.destroy
117
+ end
118
+
119
+ def save
120
+ dialog = Gtk::FileChooserDialog.new("Save PDF file",
121
+ self,
122
+ Gtk::FileChooser::ACTION_SAVE,
123
+ nil,
124
+ [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
125
+ [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT]
126
+ )
127
+
128
+ dialog.filter = FileFilter.new.add_pattern("*.acrodata").add_pattern("*.pdf").add_pattern("*.fdf")
129
+
130
+ folder = File.dirname(@filename)
131
+ dialog.set_current_folder(folder)
132
+
133
+ if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT
134
+ begin
135
+ @opened.save(dialog.filename)
136
+ rescue
137
+ error("#{$!.class}: #{$!.message}\n#{$!.backtrace.join($/)}")
138
+ end
139
+ end
140
+
141
+ dialog.destroy
142
+ end
143
+
144
+ private
145
+
146
+ def set_active_document(filename, document)
147
+ close if @opened
148
+ @opened = document
149
+ @filename = filename
150
+
151
+ @config.last_opened_file(filename)
152
+ @config.save
153
+ update_recent_menu
154
+
155
+ @last_search_result = []
156
+ @last_search =
157
+ {
158
+ :expr => "",
159
+ :regexp => false,
160
+ :type => :body
161
+ }
162
+
163
+ self.reload
164
+
165
+ # Enable basic file menus.
166
+ [
167
+ @file_menu_close, @file_menu_refresh,
168
+ ].each do |menu|
169
+ menu.sensitive = true
170
+ end
171
+
172
+ @explorer_history.clear
173
+
174
+ @statusbar.push(@main_context, "Viewing #{filename}")
175
+
176
+ setup_pdf_interface if @opened.is_a?(Origami::PDF)
177
+ end
178
+
179
+ def setup_pdf_interface
180
+ # Enable save and document menu.
181
+ [
182
+ @file_menu_saveas,
183
+ @document_menu_search,
184
+ @document_menu_gotocatalog, @document_menu_gotopage, @document_menu_gotorev, @document_menu_gotoobj,
185
+ @document_menu_properties, @document_menu_sign, @document_menu_ur
186
+ ].each do |menu|
187
+ menu.sensitive = true
188
+ end
189
+
190
+ @document_menu_gotodocinfo.sensitive = true if @opened.document_info?
191
+ @document_menu_gotometadata.sensitive = true if @opened.metadata?
192
+ @document_menu_gotofield.sensitive = true if @opened.form?
193
+
194
+ setup_page_menu
195
+ setup_field_menu
196
+ setup_revision_menu
197
+
198
+ goto_catalog
199
+ end
200
+
201
+ def setup_page_menu
202
+ page_menu = Menu.new
203
+ @document_menu_gotopage.remove_submenu
204
+ @opened.each_page.with_index(1) do |page, index|
205
+ page_menu.append(item = MenuItem.new(index.to_s).show)
206
+ item.signal_connect("activate") { @treeview.goto(page) }
207
+ end
208
+ @document_menu_gotopage.set_submenu(page_menu)
209
+ end
210
+
211
+ def setup_field_menu
212
+ field_menu = Menu.new
213
+ @document_menu_gotofield.remove_submenu
214
+ @opened.each_field do |field|
215
+ field_name =
216
+ if field.T.is_a?(Origami::String)
217
+ field.T.to_utf8
218
+ else
219
+ "<unnamed field>"
220
+ end
221
+
222
+ field_menu.append(item = MenuItem.new(field_name).show)
223
+ item.signal_connect("activate") { @treeview.goto(field) }
224
+ end
225
+ @document_menu_gotofield.set_submenu(field_menu)
226
+ end
227
+
228
+ def setup_revision_menu
229
+ rev_menu = Menu.new
230
+ @document_menu_gotorev.remove_submenu
231
+ @opened.revisions.each.with_index(1) do |rev, index|
232
+ rev_menu.append(item = MenuItem.new(index.to_s).show)
233
+ item.signal_connect("activate") { @treeview.goto(rev) }
234
+ end
235
+ @document_menu_gotorev.set_submenu(rev_menu)
236
+ end
237
+
238
+ def parse_file(path)
239
+ #
240
+ # Try to detect the file type of the document.
241
+ # Fallback to PDF if none is found.
242
+ #
243
+ file_type = detect_file_type(path)
244
+ if file_type.nil?
245
+ file_type = Origami::PDF
246
+ force_mode = true
247
+ else
248
+ force_mode = false
249
+ end
250
+
251
+ file_type.read(path,
252
+ verbosity: Origami::Parser::VERBOSE_TRACE,
253
+ ignore_errors: false,
254
+ callback: method(:update_progressbar),
255
+ prompt_password: method(:prompt_password),
256
+ force: force_mode
257
+ )
258
+ end
259
+
260
+ def update_progressbar(_obj)
261
+ @progressbar.pulse if @progressbar
262
+ Gtk.main_iteration while Gtk.events_pending?
263
+ end
264
+
265
+ def prompt_password
266
+ passwd = ""
267
+
268
+ dialog = Gtk::Dialog.new(
269
+ "This document is encrypted",
270
+ nil,
271
+ Gtk::Dialog::MODAL,
272
+ [ Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK ],
273
+ [ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ]
274
+ )
275
+
276
+ dialog.set_default_response(Gtk::Dialog::RESPONSE_OK)
277
+
278
+ label = Gtk::Label.new("Please enter password:")
279
+ entry = Gtk::Entry.new
280
+ entry.signal_connect('activate') {
281
+ dialog.response(Gtk::Dialog::RESPONSE_OK)
282
+ }
283
+
284
+ dialog.vbox.add(label)
285
+ dialog.vbox.add(entry)
286
+ dialog.show_all
287
+
288
+ dialog.run do |response|
289
+ passwd = entry.text if response == Gtk::Dialog::RESPONSE_OK
290
+ end
291
+
292
+ dialog.destroy
293
+ passwd
294
+ end
295
+
296
+ def detect_file_type(path)
297
+ supported_types = [ Origami::PDF, Origami::FDF, Origami::PPKLite ]
298
+
299
+ File.open(path, 'rb') do |file|
300
+ data = file.read(128)
301
+
302
+ supported_types.each do |type|
303
+ return type if data.match(type::Header::MAGIC)
304
+ end
305
+ end
306
+
307
+ nil
308
+ end
309
+
310
+ def create_progressbar
311
+ @progresswin = Dialog.new("Parsing file...", self, Dialog::MODAL)
312
+ @progresswin.vbox.add(@progressbar = ProgressBar.new.set_pulse_step(0.05))
313
+ @progresswin.show_all
314
+ end
315
+
316
+ def close_progressbar
317
+ @progresswin.close
318
+ end
319
+ end
320
+ end