Class: Pdfrb::CLI

Inherits:
Thor
  • Object
show all
Defined in:
lib/pdfrb/cli.rb,
lib/pdfrb/cli/form.rb,
lib/pdfrb/cli/info.rb,
lib/pdfrb/cli/batch.rb,
lib/pdfrb/cli/files.rb,
lib/pdfrb/cli/fonts.rb,
lib/pdfrb/cli/merge.rb,
lib/pdfrb/cli/split.rb,
lib/pdfrb/cli/images.rb,
lib/pdfrb/cli/modify.rb,
lib/pdfrb/cli/inspect.rb,
lib/pdfrb/cli/optimize.rb,
lib/pdfrb/cli/image2pdf.rb,
lib/pdfrb/cli/watermark.rb,
lib/pdfrb/cli/debug_info.rb

Overview

pdfrb executable. Thor-based CLI mirroring pdftk/pdfinfo ergonomics. Each subcommand wraps a Task::* module or reads directly off the Document facade.

Defined Under Namespace

Modules: Files, Fonts, Images, Info, Split, Ubatch, UdebugUinfo, Uform, Uimage2pdf, Uinspect, Umerge, Umodify, Uoptimize, Uwatermark

Constant Summary collapse

PERMISSION_BITS =
{
  "print" => 4,
  "modify" => 8,
  "copy" => 16,
  "annotate" => 32,
  "fill" => 256,
  "extract" => 512,
  "assemble" => 1024,
  "print-hq" => 2048,
}.freeze

Instance Method Summary collapse

Instance Method Details

#batch(input, output, *args) ⇒ Object



268
269
270
271
272
273
274
275
276
277
# File 'lib/pdfrb/cli.rb', line 268

def batch(input, output, *args)
  doc = open_doc(input)
  commands = args.split("--").reject(&:empty?)
  commands.each do |cmd_args|
    cmd, *cmd_args = cmd_args
    apply_batch_command(doc, cmd, cmd_args)
  end
  doc.write(output)
  puts "Batch processed → #{output}"
end

#decrypt(input, output) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/pdfrb/cli.rb', line 123

def decrypt(input, output)
  doc = open_doc(input)

  # Strip /Encrypt from trailer and clear handler config so the
  # Writer doesn't re-encrypt on output.
  trailer = doc.trailer
  if trailer && trailer[:Encrypt]
    trailer.value.delete(:Encrypt)
    doc.config.delete("encryption.handler")
    doc.config.delete("encryption.password")
    puts "Stripped encryption"
  end

  doc.write(output)
  puts "Decrypted → #{output}"
end

#diff(left, right) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/pdfrb/cli.rb', line 71

def diff(left, right)
  report = Pdfrb::Compare.compare(
    File.binread(left),
    File.binread(right)
  )
  puts report.summary
  unless report.equivalent?
    puts
    report.per_page_text_diffs.first(5).each do |d|
      puts "  Page #{d[:page] + 1}: #{(d[:similarity] * 100).round(1)}% similar"
    end
    unless report.font_diff[:added].empty? && report.font_diff[:removed].empty?
      puts "  Fonts added: #{report.font_diff[:added].join(', ')}" unless report.font_diff[:added].empty?
      puts "  Fonts removed: #{report.font_diff[:removed].join(', ')}" unless report.font_diff[:removed].empty?
    end
    puts "  Page count delta: #{report.page_count_delta}" unless report.page_count_delta.zero?
    exit 1
  end
end

#encrypt(input, output) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/pdfrb/cli.rb', line 97

def encrypt(input, output)
  doc = open_doc(input)
  bits = options[:bits]
  password = options[:password]
  owner_pw = options[:owner_password] || password

  perms = parse_permissions(options[:permissions])

  # Build the /Encrypt dictionary.
  case bits
  when 256
    build_aes256_encrypt(doc, password, owner_pw, perms)
  when 128
    build_encrypt(doc, password, owner_pw, perms, v: 4, r: 4, length: 128)
  when 40
    build_encrypt(doc, password, owner_pw, perms, v: 2, r: 3, length: 40)
  else
    raise "unsupported key length: #{bits} (use 40, 128, or 256)"
  end

  doc.write(output)
  puts "Encrypted #{output} (#{bits}-bit)"
end

#extract_text(input, output = nil) ⇒ Object



42
43
44
45
46
47
48
49
50
51
# File 'lib/pdfrb/cli.rb', line 42

def extract_text(input, output = nil)
  doc = open_doc(input)
  texts = Pdfrb::Task::ExtractText.call(doc)
  if output
    File.write(output, texts.join("\n\n--- page break ---\n\n"))
    puts "Wrote #{output}"
  else
    texts.each_with_index { |t, i| puts "--- page #{i + 1} ---"; puts t }
  end
end

#files(input) ⇒ Object



244
245
246
247
248
249
250
251
252
253
254
# File 'lib/pdfrb/cli.rb', line 244

def files(input)
  doc = open_doc(input)
  if doc.files.empty?
    puts "No embedded files."
    return
  end
  doc.files.each do |name, spec|
    size = spec[:EF] ? "embedded" : "external"
    puts "#{name}  (#{size})"
  end
end

#files_add(input, output, file) ⇒ Object



258
259
260
261
262
263
264
# File 'lib/pdfrb/cli.rb', line 258

def files_add(input, output, file)
  doc = open_doc(input)
  doc.files.add(file, name: File.basename(file),
                      relationship: options[:relationship]&.to_sym)
  doc.write(output)
  puts "Embedded #{file}#{output}"
end

#form(input) ⇒ Object



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/pdfrb/cli.rb', line 150

def form(input)
  doc = open_doc(input)
  acro = doc.catalog[:AcroForm]
  if acro.nil?
    puts "No AcroForm in this document."
    return
  end
  fields = acro[:Fields]
  return unless fields

  fields.each do |ref|
    field = ref.is_a?(Pdfrb::Model::Reference) ? doc.object(ref) : ref
    next unless field

    name = field[:T] || "(unnamed)"
    type = field[:FT] || "?"
    value = field[:V] || "-"
    puts "/#{name}  type=#{type}  value=#{value}"
  end
end

#images(input) ⇒ Object



54
55
56
57
58
59
60
# File 'lib/pdfrb/cli.rb', line 54

def images(input)
  doc = open_doc(input)
  Pdfrb::Task::ExtractImages.call(doc) do |info|
    puts "page #{info.page_index + 1} /#{info.name} " \
         "#{info.width}x#{info.height} filter=#{info.filter.inspect}"
  end
end

#images_add(input, image, output) ⇒ Object



141
142
143
144
145
146
147
# File 'lib/pdfrb/cli.rb', line 141

def images_add(input, image, output)
  doc = open_doc(input)
  name = doc.images.add(image)
  doc.pages.add # just adds a blank page for now
  doc.write(output)
  puts "Added image as /#{name} and wrote #{output}"
end

#info(input) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
# File 'lib/pdfrb/cli.rb', line 19

def info(input)
  doc = open_doc(input)
  puts "File: #{input}"
  puts "PDF version: #{doc.version}"
  puts "Pages: #{doc.pages.count}"
  puts "Title: #{doc..title || '-'}"
  puts "Author: #{doc..author || '-'}"
  puts "Subject: #{doc..subject || '-'}"
  puts "Producer: #{doc..producer || '-'}"
  puts "Encrypted: #{encrypted?(doc)}"
end

#inspect(input) ⇒ Object



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/pdfrb/cli.rb', line 224

def inspect(input)
  doc = open_doc(input)
  if options[:object]
    ref = Pdfrb::Model::Reference.new(options[:object], 0)
    obj = doc.object(ref)
    puts obj&.value
    return
  end

  catalog = doc.catalog
  puts "Catalog: /Type=#{catalog[:Type]}"
  puts "  Pages: #{doc.pages.count}"
  puts "  Title: #{doc..title || '-'}"
  af = catalog[:AcroForm]
  puts "  AcroForm: #{af ? 'present' : 'absent'}"
  outlines = catalog[:Outlines]
  puts "  Outlines: #{outlines ? 'present' : 'absent'}"
end

#merge(output, *inputs) ⇒ Object

Raises:

  • (ArgumentError)


32
33
34
35
36
37
38
39
# File 'lib/pdfrb/cli.rb', line 32

def merge(output, *inputs)
  raise ArgumentError, "merge needs at least one INPUT" if inputs.empty?

  target = Pdfrb::Document.new
  inputs.each { |i| Pdfrb::Task::Merge.call(target, open_doc(i)) }
  target.write(output)
  puts "Wrote #{output} (#{target.pages.count} pages)"
end

#modify(input, output) ⇒ Object



214
215
216
217
218
219
220
# File 'lib/pdfrb/cli.rb', line 214

def modify(input, output)
  doc = open_doc(input)
  apply_delete(doc, options[:delete]) if options[:delete]
  apply_rotate(doc, options[:rotate]) if options[:rotate]
  doc.write(output)
  puts "Modified → #{output}"
end

#optimize(input, output) ⇒ Object



63
64
65
66
67
68
# File 'lib/pdfrb/cli.rb', line 63

def optimize(input, output)
  doc = open_doc(input)
  Pdfrb::Task::Optimize.call(doc)
  doc.write(output)
  puts "Wrote #{output}"
end

#split(input, output_pattern, from = nil, to = nil) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/pdfrb/cli.rb', line 172

def split(input, output_pattern, from = nil, to = nil)
  doc = open_doc(input)
  total = doc.pages.count
  from_idx = from ? from.to_i : 1
  to_idx = to ? to.to_i : total
  (from_idx..to_idx).each do |i|
    target = Pdfrb::Document.new
    Pdfrb::Importer.new(target).import(doc.pages[i - 1].value, doc)
    out_path = format(output_pattern, i)
    target.write(out_path)
  end
  puts "Split #{input} into #{to_idx - from_idx + 1} files"
end

#versionObject



14
15
16
# File 'lib/pdfrb/cli.rb', line 14

def version
  puts "pdfrb #{Pdfrb::VERSION}"
end

#watermark(input, output) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/pdfrb/cli.rb', line 193

def watermark(input, output)
  doc = open_doc(input)
  color = options[:color].split(",").map(&:to_f)
  doc.pages.each do |page|
    canvas = page.canvas
    doc.graphics_state.register_transparency(page, opacity: options[:opacity])
    canvas.save_graphics_state
    canvas.opacity = options[:opacity]
    canvas.fill_color(color)
    canvas.text(options[:text], at: [100, 400],
                                font: options[:font].to_sym,
                                size: options[:size])
    canvas.restore_graphics_state
  end
  doc.write(output)
  puts "Watermarked #{doc.pages.count} pages → #{output}"
end