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 +4 -0
- data/Rakefile +105 -0
- data/lib/pdf/toolkit.rb +657 -0
- data/setup.rb +1585 -0
- data/test/pdf_toolkit_active_record_test.rb +84 -0
- data/test/pdf_toolkit_active_record_test.rb~ +84 -0
- data/test/pdf_toolkit_test.rb +158 -0
- data/test/pdfs/blank.pdf +0 -0
- metadata +62 -0
data/README
ADDED
data/Rakefile
ADDED
@@ -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
|
data/lib/pdf/toolkit.rb
ADDED
@@ -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:
|