pdf-toolkit 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,4 @@
1
+ If you have an archive other than a Ruby Gem, you can install with the usual
2
+ "ruby setup.rb". Use "rake rdoc" to generate documentation.
3
+
4
+ PDF::Toolkit is relased under the MIT license.
@@ -0,0 +1,105 @@
1
+ begin
2
+ require 'rubygems'
3
+ rescue LoadError
4
+ end
5
+ require 'rake'
6
+ require 'rake/testtask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/packagetask'
9
+ require 'rake/gempackagetask'
10
+ require 'rake/contrib/sshpublisher'
11
+ require 'rake/contrib/rubyforgepublisher'
12
+ require File.join(File.dirname(__FILE__), 'lib', 'pdf', 'toolkit')
13
+
14
+ PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
15
+ PKG_NAME = 'pdf-toolkit'
16
+ PKG_VERSION = PDF::Toolkit::PDF_TOOLKIT_VERSION
17
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
18
+ # PKG_DESTINATION = ENV["PKG_DESTINATION"] || "../#{PKG_NAME}"
19
+
20
+ # RELEASE_NAME = "REL #{PKG_VERSION}"
21
+
22
+ RUBY_FORGE_PROJECT = PKG_NAME
23
+ RUBY_FORGE_USER = "tpope"
24
+
25
+ desc "Default task: test"
26
+ task :default => [ :test ]
27
+
28
+
29
+ # Run the unit tests
30
+ Rake::TestTask.new { |t|
31
+ t.libs << "test"
32
+ t.test_files = Dir['test/*_test.rb'] + Dir['test/test_*.rb']
33
+ t.verbose = true
34
+ }
35
+
36
+
37
+ # Generate the RDoc documentation
38
+ Rake::RDocTask.new { |rdoc|
39
+ rdoc.rdoc_dir = 'doc'
40
+ rdoc.rdoc_files.add('lib')
41
+ rdoc.main = "PDF::Toolkit"
42
+ rdoc.title = rdoc.main
43
+ rdoc.options << '--inline-source'
44
+ }
45
+
46
+
47
+ # Create compressed packages
48
+ spec = Gem::Specification.new do |s|
49
+ s.platform = Gem::Platform::RUBY
50
+ s.name = PKG_NAME
51
+ s.summary = 'A wrapper around pdftk to allow PDF metadata manipulation'
52
+ s.description = 'PDF::Toolkit provides a simple interface for querying and unpdation PDF metadata like the document Author and Title.'
53
+ s.version = PKG_VERSION
54
+
55
+ s.author = 'Tim Pope'
56
+ s.email = 'ruby@tp0pe.inf0'.gsub(/0/,'o')
57
+ s.rubyforge_project = RUBY_FORGE_PROJECT
58
+ s.homepage = "http://#{PKG_NAME}.rubyforge.org"
59
+
60
+ s.has_rdoc = true
61
+ # s.requirements << 'none'
62
+ s.require_path = 'lib'
63
+
64
+ s.add_dependency('activesupport')
65
+
66
+ s.files = [ "Rakefile", "README", "setup.rb" ]
67
+ s.files = s.files + Dir.glob( "lib/**/*.rb" )
68
+ s.files = s.files + Dir.glob( "test/**/*" ).reject { |item| item.include?( "\.svn" ) }
69
+ end
70
+
71
+ Rake::GemPackageTask.new(spec) do |p|
72
+ p.gem_spec = spec
73
+ p.need_tar = true
74
+ p.need_zip = true
75
+ end
76
+
77
+ # Publish documentation
78
+ desc "Publish the API documentation"
79
+ task :pdoc => [:rerdoc] do
80
+ # Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT,RUBY_FORGE_USER).upload
81
+ Rake::SshDirPublisher.new("rubyforge.org", "/var/www/gforge-projects/#{PKG_NAME}", "doc").upload
82
+ end
83
+
84
+ desc "Publish the release files to RubyForge."
85
+ task :release => [ :package ] do
86
+ `rubyforge login`
87
+
88
+ for ext in %w( gem tgz zip )
89
+ release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}"
90
+ puts release_command
91
+ system(release_command)
92
+ end
93
+ end
94
+
95
+ begin
96
+ require 'rcov/rcovtask'
97
+ Rcov::RcovTask.new do |t|
98
+ t.test_files = Dir['test/*_test.rb'] + Dir['test/test_*.rb']
99
+ t.verbose = true
100
+ t.rcov_opts << "--text-report"
101
+ # t.rcov_opts << "--exclude \\\\A/var/lib/gems"
102
+ t.rcov_opts << "--exclude '/(active_record|active_support)\\b'"
103
+ end
104
+ rescue LoadError
105
+ end
@@ -0,0 +1,657 @@
1
+ # Copyright (c) 2006 Tim Pope
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'rubygems' rescue nil
23
+ require 'tempfile'
24
+ require 'forwardable'
25
+ require 'active_support'
26
+
27
+ # Certain existing libraries have a PDF class; no sense in being unnecessarily
28
+ # incompatible.
29
+ module PDF #:nodoc:
30
+ end unless defined? PDF
31
+
32
+ # PDF::Toolkit can be used as a simple class, or derived from and tweaked. The
33
+ # following two examples have identical results.
34
+ #
35
+ # my_pdf = PDF::Toolkit.open("somefile.pdf")
36
+ # my_pdf.updated_at = Time.now # ModDate
37
+ # my_pdf["SomeAttribute"] = "Some value"
38
+ # my_pdf.save!
39
+ #
40
+ # class MyDocument < PDF::Toolkit
41
+ # info_accessor :some_attribute
42
+ # def before_save
43
+ # self.updated_at = Time.now
44
+ # end
45
+ # end
46
+ # my_pdf = MyDocument.open("somefile.pdf")
47
+ # my_pdf.some_attribute = "Some value"
48
+ # my_pdf.save!
49
+ #
50
+ # Note the use of a +before_save+ callback in the second example. This is
51
+ # the only supported callback unless you use the experimental
52
+ # #loot_active_record class method.
53
+ #
54
+ # == Requirements
55
+ #
56
+ # PDF::Toolkit requires +pdftk+, which is available from
57
+ # http://www.accesspdf.com/pdftk. For full functionality, also install
58
+ # +xpdf+ from http://www.foolabs.com/xpdf. ActiveSupport (from Ruby on Rails)
59
+ # is also required but this dependency may be removed in the future.
60
+ #
61
+ # == Limitations
62
+ #
63
+ # Timestamps are written in UTF-16 by +pdftk+, which is not appropriately
64
+ # handled by +pdfinfo+.
65
+ #
66
+ # +pdftk+ requires the owner password, even for simply querying the document.
67
+ class PDF::Toolkit
68
+
69
+ PDF_TOOLKIT_VERSION = "0.5.0"
70
+ extend Forwardable
71
+ class Error < ::StandardError #:nodoc:
72
+ end
73
+ class ExecutionError < Error #:nodoc:
74
+ attr_reader :command, :exit_status
75
+ def initialize(msg = nil, cmd = nil, exit_status = nil)
76
+ super(msg)
77
+ @command = cmd
78
+ @exit_status = exit_status
79
+ end
80
+ end
81
+ class FileNotSaved < Error #:nodoc:
82
+ end
83
+
84
+ class <<self
85
+
86
+ # Add an accessor for a key. If the key is omitted, defaults to a
87
+ # camelized version of the accessor (+foo_bar+ becomes +FooBar+). The
88
+ # example below illustrates the defaults.
89
+ #
90
+ # class MyDocument < PDF::Toolkit
91
+ # info_accessor :created_at, "CreationDate"
92
+ # info_accessor :updated_at, "ModDate"
93
+ # info_accessor :author
94
+ # [:subject, :title, :keywords, :producer, :creator].each do |key|
95
+ # info_accessor key
96
+ # end
97
+ # end
98
+ #
99
+ # MyDocument.open("document.pdf").created_at
100
+ def info_accessor(accessor_name, info_key = nil)
101
+ info_key ||= camelize_key(accessor_name)
102
+ read_inheritable_attribute(:info_accessors)[accessor_name] = info_key
103
+ define_method accessor_name do
104
+ self[info_key]
105
+ end
106
+ define_method "#{accessor_name}=" do |value|
107
+ self[info_key] = value
108
+ end
109
+ end
110
+
111
+ # Invoke +pdftk+ with the given arguments, plus +dont_ask+. If :mode or
112
+ # a block is given, IO::popen is called. Otherwise, Kernel#system is
113
+ # used.
114
+ #
115
+ # result = PDF::Toolkit.pdftk(*%w(foo.pdf bar.pdf cat output baz.pdf))
116
+ # io = PDF::Toolkit.pdftk("foo.pdf","dump_data","output","-",:mode => 'r')
117
+ # PDF::Toolkit.pdftk("foo.pdf","dump_data","output","-") { |io| io.read }
118
+ def pdftk(*args,&block)
119
+ options = args.last.is_a?(Hash) ? args.pop : {}
120
+ args << "dont_ask"
121
+ args << options
122
+ result = call_program(executables[:pdftk],*args,&block)
123
+ return block_given? ? $?.success? : result
124
+ end
125
+
126
+ # Invoke +pdftotext+. If +outfile+ is omitted, returns an +IO+ object for
127
+ # the output.
128
+ def pdftotext(file,outfile = nil,&block)
129
+ call_program(executables[:pdftotext],file,
130
+ outfile||"-",:mode => (outfile ? nil : 'r'),&block)
131
+ end
132
+
133
+ # This method will +require+ and +include+ validations, callbacks, and
134
+ # timestamping from +ActiveRecord+. Use at your own risk.
135
+ def loot_active_record
136
+ require 'active_support'
137
+ require 'active_record'
138
+ # require 'active_record/validations'
139
+ # require 'active_record/callbacks'
140
+ # require 'active_record/timestamp'
141
+
142
+ unless defined? @@looted_active_record
143
+ @@looted_active_record = true
144
+ meta = (class <<self; self; end)
145
+ alias_method :initialize_ar_hack, :initialize
146
+ include ActiveRecord::Validations
147
+ include ActiveRecord::Callbacks
148
+ include ActiveRecord::Timestamp
149
+ alias_method :initialize, :initialize_ar_hack
150
+
151
+ cattr_accessor :record_timestamps # nil by default
152
+
153
+ meta.send(:define_method,:default_timezone) do
154
+ defined? ActiveRecord::Base ? ActiveRecord::Base.default_timezone : :local
155
+ end
156
+ end
157
+ self
158
+ end
159
+
160
+ def human_attribute_name(arg) #:nodoc:
161
+ defined? ActiveRecord::Base ? ActiveRecord::Base.human_attribute_name(arg) : arg.gsub(/_/,' ')
162
+ end
163
+
164
+ private
165
+
166
+ def instantiate(*args) #:nodoc:
167
+ raise NoMethodError, "stub method `instantiate' called for #{self}:#{self.class}"
168
+ end
169
+
170
+ def call_program(*args,&block)
171
+ old_stream = nil
172
+ options = args.last.is_a?(Hash) ? args.pop : {}
173
+ options[:mode] ||= 'r' if block_given?
174
+ unless options[:silence_stderr] == false
175
+ old_stream = STDERR.dup
176
+ STDERR.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')
177
+ STDERR.sync = true
178
+ end
179
+ if options[:mode]
180
+ command = (args.map {|arg| %{"#{arg.gsub('"','\\"')}"}}).join(" ")
181
+ retval = IO.popen(command,options[:mode],&block)
182
+ retval
183
+ else
184
+ system(*args)
185
+ end
186
+ ensure
187
+ STDERR.reopen(old_stream) if old_stream
188
+ end
189
+
190
+ def camelize_key(key)
191
+ if key.to_s.respond_to?(:camelize)
192
+ key.to_s.camelize
193
+ else
194
+ key.to_s.gsub(/_+([^_])/) {$1.upcase}.sub(/^./) {|l|l.upcase}
195
+ end
196
+ end
197
+
198
+ end
199
+
200
+ class_inheritable_accessor :executables, :default_permissions, :default_input_password
201
+ class_inheritable_accessor :default_owner_password, :default_user_password
202
+ protected :default_owner_password=, :default_user_password=
203
+ # self.pdftk = "pdftk"
204
+ self.executables = Hash.new {|h,k| k.to_s.dup}
205
+ write_inheritable_attribute :info_accessors, Hash.new { |h,k|
206
+ if h.has_key?(k.to_s.to_sym)
207
+ h[k.to_s.to_sym]
208
+ elsif k.kind_of?(Symbol)
209
+ camelize_key(k)
210
+ else
211
+ k.dup
212
+ end
213
+ }
214
+
215
+ info_accessor :created_at, "CreationDate"
216
+ info_accessor :updated_at, "ModDate"
217
+ [:author, :subject, :title, :keywords, :creator, :producer].each do |key|
218
+ info_accessor key
219
+ end
220
+
221
+ # Create a new object associated with +filename+ and read in the
222
+ # associated metadata.
223
+ #
224
+ # my_pdf = PDF::Toolkit.open("document.pdf")
225
+ def self.open(filename,input_password = nil)
226
+ object = new(filename,input_password)
227
+ object.reload
228
+ object
229
+ end
230
+
231
+ # Like +open+, only the attributes are lazily loaded. Under most
232
+ # circumstances, +open+ is preferred.
233
+ def initialize(filename,input_password = nil)
234
+ @filename = if filename.respond_to?(:to_str)
235
+ filename.to_str
236
+ elsif filename.kind_of?(self.class)
237
+ filename.instance_variable_get("@filename")
238
+ elsif filename.respond_to?(:path)
239
+ filename.path
240
+ else
241
+ filename
242
+ end
243
+ @input_password = input_password || default_input_password
244
+ @owner_password = default_owner_password
245
+ @user_password = default_user_password
246
+ @permissions = default_permissions || []
247
+ @new_info = {}
248
+ callback(:after_initialize) if respond_to?(:after_initialize) && respond_to?(:callback)
249
+ # reload
250
+ end
251
+
252
+ attr_reader :pdf_ids, :permissions
253
+ attr_writer :owner_password, :user_password
254
+
255
+ def page_count
256
+ read_data unless @pages
257
+ @pages
258
+ end
259
+
260
+ alias pages page_count
261
+
262
+ # Path to the file.
263
+ def path
264
+ @new_filename || @filename
265
+ end
266
+
267
+ # Retrieve the file's version as a symbol.
268
+ #
269
+ # my_pdf.version # => :"1.4"
270
+ def version
271
+ @version ||= File.open(@filename) do |io|
272
+ io.read(8)[5..-1].to_sym
273
+ end
274
+ end
275
+
276
+ # Reload (or load) the file's metadata.
277
+ def reload
278
+ @new_info = {}
279
+ read_data
280
+ # run_callbacks_for(:after_load)
281
+ self
282
+ end
283
+
284
+ # Commit changes to the PDF. The return value is a boolean reflecting the
285
+ # success of the operation (This should always be true unless you're
286
+ # utilizing #loot_active_record).
287
+ def save
288
+ create_or_update
289
+ end
290
+
291
+ # Like +save+, only raise an exception if the operation fails.
292
+ #
293
+ # TODO: ensure no ActiveRecord::RecordInvalid errors make it through.
294
+ def save!
295
+ if save
296
+ self
297
+ else
298
+ raise FileNotSaved
299
+ end
300
+ end
301
+
302
+ # Save to a different file. A new object is returned if the operation
303
+ # succeeded. Otherwise, +nil+ is returned.
304
+ def save_as(filename)
305
+ dup.save_as!(filename)
306
+ rescue FileNotSaved
307
+ nil
308
+ end
309
+
310
+ # Save to a different file. The existing object is modified. An exception
311
+ # is raised if the operation fails.
312
+ def save_as!(filename)
313
+ @new_filename = filename
314
+ save!
315
+ self
316
+ rescue ActiveRecord::RecordInvalid
317
+ raise FileNotSaved
318
+ end
319
+
320
+ # Invoke +pdftotext+ on the file and return an +IO+ object for reading the
321
+ # results.
322
+ #
323
+ # text = my_pdf.to_text.read
324
+ def to_text(filename = nil,&block)
325
+ self.class.send(:pdftotext,@filename,filename,&block)
326
+ end
327
+
328
+ def to_s #:nodoc:
329
+ "#<#{self.class}:#{path}>"
330
+ end
331
+
332
+ # Enumerable/Hash methods {{{1
333
+
334
+ # Create a hash from the file's metadata.
335
+ def to_hash
336
+ ensure_loaded
337
+ @info.merge(@new_info).reject {|key,value| value.nil?}
338
+ end
339
+
340
+ def_delegators :to_hash, :each, :keys, :values, :each_key, :each_value, :each_pair, :merge
341
+ include Enumerable
342
+
343
+ def new_record? #:nodoc:
344
+ !@new_filename.nil?
345
+ end
346
+
347
+ # Read a metadata attribute.
348
+ #
349
+ # author = my_pdf["Author"]
350
+ #
351
+ # See +info_accessor+ for an alternate syntax.
352
+ def [](key)
353
+ key = lookup_key(key)
354
+ return @new_info[key.to_s] if @new_info.has_key?(key.to_s)
355
+ ensure_loaded
356
+ @info[key.to_s]
357
+ end
358
+
359
+
360
+ # Write a metadata attribute.
361
+ #
362
+ # my_pdf["Author"] = author
363
+ #
364
+ # See +info_accessor+ for an alternate syntax.
365
+ def []=(key,value)
366
+ key = lookup_key(key)
367
+ @new_info[key.to_s] = value
368
+ end
369
+
370
+ def update_attribute(key,value)
371
+ self[key] = value
372
+ save
373
+ end
374
+
375
+ # True if the file has the given metadata attribute.
376
+ def has_key?(value)
377
+ ensure_loaded
378
+ value = lookup_key(value)
379
+ (@info.has_key?(value) || @new_info.has_key?(value)) && !!(self[value])
380
+ end
381
+ alias key? has_key?
382
+
383
+ # Remove the metadata attribute from the file.
384
+ def delete(key)
385
+ key = lookup_key(key)
386
+ if @info.has_key?(key) || !@pages
387
+ @new_info[key] = nil
388
+ else
389
+ @new_info.delete(key)
390
+ end
391
+ end
392
+
393
+ # Like +delete_if+, only nil is returned if no attributes were removed.
394
+ def reject!(&block)
395
+ ensure_loaded
396
+ ret = nil
397
+ each do |key,value|
398
+ if yield(key,value)
399
+ ret = self
400
+ delete(key)
401
+ end
402
+ end
403
+ ret
404
+ end
405
+
406
+ # Remove metadata if the given block returns false. The following would
407
+ # remove all timestamps.
408
+ #
409
+ # my_pdf.delete_if {|key,value| value.kind_of?(Time)}
410
+ def delete_if(&block)
411
+ reject!(&block)
412
+ self
413
+ end
414
+
415
+ # Add the specified attributes to the file. If symbols are given as keys,
416
+ # they are camelized.
417
+ #
418
+ # my_pdf.merge!("Author" => "Dave Thomas", :title => "Programming Ruby")
419
+ def merge!(hash)
420
+ hash.each do |k,v|
421
+ @new_info[lookup_key(k)] = v
422
+ end
423
+ self
424
+ end
425
+
426
+ # }}}1
427
+
428
+ protected
429
+
430
+ =begin
431
+ def method_missing(method,*args)
432
+ args_needed = method.to_s.last == "=" ? 1 : 0
433
+ if args.length != args_needed
434
+ raise ArgumentError,
435
+ "wrong number of arguments (#{args.length} for #{args_needed})"
436
+ end
437
+ ensure_loaded
438
+ attribute = lookup_key(method.to_s.chomp("=").to_sym)
439
+ if method.to_s.last == "="
440
+ self[attribute] = args.first
441
+ else
442
+ self[attribute]
443
+ end
444
+ end
445
+ =end
446
+
447
+ def read_attribute(key)
448
+ self[key]
449
+ end
450
+
451
+ def write_attribute(key,value)
452
+ self[key] = value
453
+ end
454
+
455
+ private
456
+
457
+ # The password that will be used to decrypt the file.
458
+ def input_password
459
+ @input_password || @owner_password || @user_password
460
+ end
461
+
462
+ def lookup_key(key)
463
+ return self.class.read_inheritable_attribute(:info_accessors)[key]
464
+ end
465
+
466
+ def call_pdftk_on_file(*args,&block)
467
+ options = args.last.is_a?(Hash) ? args.pop : {}
468
+ args.unshift("input_pw",input_password) if input_password
469
+ args.unshift(@filename)
470
+ args << options
471
+ self.class.send(:pdftk,*args,&block)
472
+ end
473
+
474
+ def cast_field(field)
475
+ case field
476
+ when /^D:(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)([-+].*)?$/
477
+ parse_time(field)
478
+ when /^\d+$/
479
+ field.to_i
480
+ else
481
+ field
482
+ end
483
+ end
484
+
485
+ def parse_time(string)
486
+ if string =~ /^D:(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)([-+].*)?$/
487
+ date = $~[1..6].map {|n|n.to_i}
488
+ tz = $7
489
+ time = Time.utc(*date)
490
+ tz_match = tz.match(/^([+-])(\d{1,2})(?:'(\d\d)')?$/) if tz
491
+ if tz_match
492
+ direction, hours, minutes = tz_match[1..3]
493
+ offset = (hours.to_i*60+minutes.to_i)*60
494
+ # Go the *opposite* direction
495
+ time += (offset == "+" ? -offset : offset)
496
+ end
497
+ time.getlocal
498
+ else
499
+ string
500
+ end
501
+ end
502
+
503
+ def format_field(field)
504
+ format_time(field)
505
+ end
506
+
507
+ def format_time(time)
508
+ if time.kind_of?(Time)
509
+ string = ("D:%04d"+"%02d"*5) % time.to_a[0..5].reverse
510
+ string += (time.utc_offset < 0 ? "-" : "+")
511
+ string += "%02d'%02d'" % [time.utc_offset.abs/3600,(time.utc_offset.abs/60)%60]
512
+ else
513
+ time
514
+ end
515
+ end
516
+
517
+ def read_data
518
+ last = nil
519
+ bookmark_title, bookmark_level = nil, nil
520
+ @info = {}
521
+ @unknown_data = {}
522
+ @pdf_ids = []
523
+ @bookmarks = []
524
+ unless File.readable?(@filename)
525
+ raise ExecutionError, "File not found - #{@filename}"
526
+ end
527
+ retval = call_pdftk_on_file("dump_data","output","-", :mode => "r") do |pipe|
528
+ pipe.each_line do |line|
529
+ match = line.chomp.match(/(.*?): (.*)/)
530
+ unless match
531
+ raise ExecutionError, "Error parsing PDFTK output"
532
+ end
533
+ key, value = match[1], match[2]
534
+ # key, value = line.chomp.split(/: /)
535
+ case key
536
+ when 'InfoKey'
537
+ last = value
538
+ when 'InfoValue'
539
+ @info[last] = cast_field(value)
540
+ last = nil
541
+ when /^PdfID(\d+)$/
542
+ @pdf_ids << value
543
+ when /^PageLabel/
544
+ # TODO
545
+ when 'NumberOfPages'
546
+ @pages = value.to_i
547
+ when 'BookmarkTitle'
548
+ bookmark_title = value
549
+ when 'BookmarkLevel'
550
+ bookmark_level = value.to_i
551
+ when 'BookmarkPageNumber'
552
+ unless bookmark_title && bookmark_level
553
+ raise ExecutionError, "Error parsing PDFTK output"
554
+ end
555
+ @bookmarks << [bookmark_title, bookmark_level, value.to_i]
556
+ bookmark_title, bookmark_level = nil, nil
557
+ else
558
+ @unknown_data[key] = value
559
+ end
560
+ end
561
+ end
562
+ if @info.empty? && !@pages || !retval
563
+ raise ExecutionError.new("Error invoking PDFTK",nil,$?)
564
+ end
565
+ self
566
+ end
567
+
568
+ def write_info_to_file(out)
569
+ # ensure_loaded
570
+ raise Error, "No data to update PDF with" unless @new_info
571
+ tmp = ( out == @filename ? "#{out}.#{$$}.new" : nil)
572
+ args = ["update_info","-","output",tmp || out]
573
+ args += [ "owner_pw", @owner_password ] if @owner_password
574
+ args += [ "user_pw" , @user_password ] if @user_password
575
+ args += (["allow"] + @permissions.uniq ) if @permissions && !@permissions.empty?
576
+ args << {:mode => "w"}
577
+ # If a value is omitted, the old value is used. If it is blank, it is
578
+ # removed from the file. Thus, it is not necessary to read the old
579
+ # metadata in order to modify the file.
580
+ retval = call_pdftk_on_file(*args) do |io|
581
+ (@info || {}).merge(@new_info).each do |key,value|
582
+ io.puts "InfoKey: #{key}"
583
+ io.puts "InfoValue: #{format_field(value)}"
584
+ end
585
+ end
586
+ if retval
587
+ if tmp
588
+ File.rename(tmp,out)
589
+ tmp = nil
590
+ end
591
+ else
592
+ raise ExecutionError.new("Error invoking PDFTK",nil,$?)
593
+ end
594
+ retval
595
+ ensure
596
+ File.unlink(tmp) if tmp && File.exists?(tmp)
597
+ end
598
+
599
+ def update
600
+ if write_info_to_file(@filename)
601
+ cleanup
602
+ true
603
+ end
604
+ end
605
+
606
+ def create
607
+ if write_info_to_file(@new_filename)
608
+ cleanup
609
+ @filename = @new_filename
610
+ @new_filename = nil
611
+ true
612
+ end
613
+ end
614
+
615
+ def create_or_update #:nodoc:
616
+ run_callbacks_for(:before_save)
617
+ result = new_record? ? create : update
618
+ if result
619
+ # run_callbacks_for(:after_save)
620
+ end
621
+ result
622
+ end
623
+
624
+ def respond_to_without_attributes?(method)
625
+ respond_to?(method)
626
+ end
627
+
628
+ def destroy
629
+ raise NoMethodError, "stub method `destroy' called for #{self}:#{self.class}"
630
+ # File.unlink(@filename); self.freeze!
631
+ end
632
+
633
+ def cleanup
634
+ if @info
635
+ # Create a new hash on purpose
636
+ @info = @info.merge(@new_info).reject {|key,value| value.nil?}
637
+ @new_info = {}
638
+ end
639
+ @version = nil
640
+ @input_password = nil
641
+ self
642
+ end
643
+
644
+ def ensure_loaded
645
+ unless @pages
646
+ read_data
647
+ end
648
+ self
649
+ end
650
+
651
+ def run_callbacks_for(event,*args)
652
+ send(event,*args) if respond_to?(event,true) && !respond_to?(:callback,true)
653
+ end
654
+ end
655
+
656
+ #--
657
+ # vim:set tw=79: