pdf-toolkit 0.5.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.
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: