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,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