├── .gitignore
├── Gemfile
├── test
├── dataset
│ ├── crypto.pdf
│ ├── empty.pdf
│ └── calc.pdf
├── test_pdf_create.rb
├── test_pdf.rb
├── test_actions.rb
├── test_forms.rb
├── test_pdf_attachment.rb
├── test_object_tree.rb
├── test_pages.rb
├── test_pdf_parse_lazy.rb
├── test_xrefs.rb
├── test_annotations.rb
├── test_pdf_encrypt.rb
├── test_native_types.rb
├── test_pdf_sign.rb
├── test_pdf_parse.rb
└── test_streams.rb
├── examples
├── flash
│ ├── helloworld.swf
│ └── flash.rb
├── uri
│ ├── open-uri.rb
│ ├── submitform.rb
│ └── javascript.rb
├── javascript
│ ├── hello_world.rb
│ └── js_emulation.rb
├── README.md
├── encryption
│ └── encryption.rb
├── loop
│ ├── goto.rb
│ └── named.rb
├── attachments
│ ├── attachment.rb
│ └── nested_document.rb
├── forms
│ ├── javascript.rb
│ └── xfa.rb
├── signature
│ └── signature.rb
└── events
│ └── events.rb
├── bin
├── pdfsh
├── shell
│ ├── hexdump.rb
│ ├── irbrc
│ └── console.rb
├── pdf2pdfa
├── pdfdecrypt
├── pdfdecompress
├── pdfencrypt
└── pdfmetadata
├── .travis.yml
├── Rakefile
├── lib
├── origami
│ ├── version.rb
│ ├── filters
│ │ ├── crypt.rb
│ │ ├── jpx.rb
│ │ ├── dct.rb
│ │ ├── jbig2.rb
│ │ ├── flate.rb
│ │ ├── runlength.rb
│ │ └── lzw.rb
│ ├── graphics.rb
│ ├── xfa
│ │ ├── signature.rb
│ │ ├── xdc.rb
│ │ ├── localeset.rb
│ │ ├── xmpmeta.rb
│ │ ├── sourceset.rb
│ │ ├── xfdf.rb
│ │ ├── stylesheet.rb
│ │ ├── datasets.rb
│ │ ├── package.rb
│ │ ├── pdf.rb
│ │ ├── xfa.rb
│ │ └── connectionset.rb
│ ├── graphics
│ │ ├── render.rb
│ │ ├── instruction.rb
│ │ └── path.rb
│ ├── null.rb
│ ├── parsers
│ │ ├── fdf.rb
│ │ ├── ppklite.rb
│ │ ├── pdf.rb
│ │ └── pdf
│ │ │ └── linear.rb
│ ├── template
│ │ └── patterns.rb
│ ├── tree.rb
│ ├── outline.rb
│ ├── xfa.rb
│ ├── header.rb
│ ├── boolean.rb
│ ├── outputintents.rb
│ ├── functions.rb
│ ├── webcapture.rb
│ ├── reference.rb
│ ├── name.rb
│ ├── compound.rb
│ ├── numeric.rb
│ └── collections.rb
└── origami.rb
├── origami.gemspec
├── CHANGELOG.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
--------------------------------------------------------------------------------
/test/dataset/crypto.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdelugre/origami/HEAD/test/dataset/crypto.pdf
--------------------------------------------------------------------------------
/examples/flash/helloworld.swf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gdelugre/origami/HEAD/examples/flash/helloworld.swf
--------------------------------------------------------------------------------
/bin/pdfsh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | begin
4 | require 'irb'
5 | rescue LoadError
6 | abort "Error: you need to install irb to run this application."
7 | end
8 |
9 | $:.unshift File.join(__dir__, 'shell')
10 | ENV["IRBRC"] = File.join(__dir__, "shell", "irbrc")
11 |
12 | IRB.start
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | cache: bundler
3 | language: ruby
4 | rvm:
5 | - 2.2
6 | - 2.3
7 | - 2.4
8 | - 2.5
9 | - 2.6
10 | - ruby-head
11 | - jruby-head
12 | matrix:
13 | fast_finish: true
14 | allow_failures:
15 | - rvm: ruby-head
16 | - rvm: jruby-head
17 | install:
18 | - bundle install --jobs=4 --retry=3
19 |
--------------------------------------------------------------------------------
/examples/uri/open-uri.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | OUTPUT_FILE = "#{File.basename(__FILE__, '.rb')}.pdf"
12 | URL = "http://google.fr"
13 |
14 | pdf = PDF.new
15 |
16 | # Trigger an URI action when the document is opened.
17 | pdf.onDocumentOpen Action::URI[URL]
18 |
19 | pdf.save(OUTPUT_FILE)
20 |
21 | puts "PDF file saved as #{OUTPUT_FILE}."
22 |
--------------------------------------------------------------------------------
/examples/javascript/hello_world.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "/../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | #
12 | # Displays a message box when the document is opened.
13 | #
14 |
15 | OUTPUT_FILE = "#{File.basename(__FILE__, ".rb")}.pdf"
16 |
17 | # Creating a new file
18 | PDF.new
19 | .onDocumentOpen(Action::JavaScript 'app.alert("Hello world");')
20 | .save(OUTPUT_FILE)
21 |
22 | puts "PDF file saved as #{OUTPUT_FILE}."
23 |
--------------------------------------------------------------------------------
/test/test_pdf_create.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'stringio'
3 |
4 | class TestPDFCreate < Minitest::Test
5 |
6 | def setup
7 | @output = StringIO.new
8 | end
9 |
10 | def test_pdf_create
11 | pdf = PDF.new
12 |
13 | null = Null.new
14 | pdf << null
15 |
16 | pdf.save(@output)
17 |
18 | assert null.indirect?
19 | assert_equal null.reference.solve, null
20 | assert pdf.root_objects.include?(null)
21 | assert_equal pdf.revisions.first.body[null.reference], null
22 | assert_equal null.reference.solve, null
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/test_pdf.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 |
3 | $:.unshift File.join(__dir__, "..", "lib")
4 | require 'origami'
5 | include Origami
6 |
7 | require_relative 'test_native_types'
8 | require_relative 'test_pdf_parse'
9 | require_relative 'test_pdf_parse_lazy'
10 | require_relative 'test_pdf_create'
11 | require_relative 'test_streams'
12 | require_relative 'test_pdf_encrypt'
13 | require_relative 'test_pdf_sign'
14 | require_relative 'test_pdf_attachment'
15 | require_relative 'test_pages'
16 | require_relative 'test_actions'
17 | require_relative 'test_annotations'
18 | require_relative 'test_forms'
19 | require_relative 'test_xrefs'
20 | require_relative 'test_object_tree'
21 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Origami samples
2 |
3 | ## ``attachments``
4 |
5 | Adding a file attachment to a PDF document.
6 |
7 | ## ``javascript``
8 |
9 | Adding JavaScript to a document and emulating it.
10 |
11 | ## ``encryption``
12 |
13 | PDF encryption (supports RC4 40-128 bits, and AES128).
14 | Create a new encrypted document.
15 |
16 | ## ``signature``
17 |
18 | PDF digital signatures. Create a new document and signs it.
19 |
20 | ## ``flash``
21 |
22 | Create a document with an embedded SWF file.
23 |
24 | ## ``loop``
25 |
26 | Create a looping document using ``GoTo`` and ``Named`` actions.
27 |
28 | ## ``events``
29 |
30 | Create a document running JavaScript on various events.
31 |
32 | ## ``uri``
33 |
34 | Various methods for connecting to a remote URL.
35 |
--------------------------------------------------------------------------------
/examples/encryption/encryption.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | #
12 | # Encrypts a document with an empty password.
13 | #
14 |
15 | OUTPUT_FILE = "#{File.basename(__FILE__, ".rb")}.pdf"
16 |
17 | # Creates an encrypted document with AES256 and a null password.
18 | pdf = PDF.new.encrypt(cipher: 'aes', key_size: 256)
19 |
20 | contents = ContentStream.new
21 | contents.write "Encrypted document sample",
22 | x: 100, y: 750, rendering: Text::Rendering::STROKE, size: 30
23 |
24 | pdf.append_page Page.new.setContents(contents)
25 |
26 | pdf.save(OUTPUT_FILE)
27 |
28 | puts "PDF file saved as #{OUTPUT_FILE}."
29 |
--------------------------------------------------------------------------------
/test/dataset/empty.pdf:
--------------------------------------------------------------------------------
1 | %PDF-1.0
2 | 1 0 obj
3 | <<
4 | /Pages 2 0 R
5 | /Type /Catalog
6 | >>
7 | endobj
8 | 2 0 obj
9 | <<
10 | /Kids [ 3 0 R ]
11 | /Count 1
12 | /Type /Pages
13 | >>
14 | endobj
15 | 3 0 obj
16 | <<
17 | /Type /Page
18 | /Parent 2 0 R
19 | /MediaBox [ 0 0 795 842 ]
20 | /Resources <<
21 | /Font <<
22 | /F1 4 0 R
23 | >>
24 | >>
25 | >>
26 | endobj
27 | 4 0 obj
28 | <<
29 | /Name /F1
30 | /Subtype /Type1
31 | /Type /Font
32 | /BaseFont /Helvetica
33 | >>
34 | endobj
35 | xref
36 | 0 5
37 | 0000000000 65535 f
38 | 0000000010 00000 n
39 | 0000000067 00000 n
40 | 0000000136 00000 n
41 | 0000000272 00000 n
42 | trailer
43 | <<
44 | /Root 1 0 R
45 | /Size 5
46 | >>
47 | startxref
48 | 364
49 | %%EOF
50 |
--------------------------------------------------------------------------------
/test/test_actions.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 |
3 | class TestActions < MiniTest::Test
4 | def setup
5 | @target = PDF.new
6 | @page = Page.new
7 | @action = Action::JavaScript "app.alert(null);"
8 | end
9 |
10 | def test_pdf_actions
11 | @target.onDocumentOpen @action
12 | @target.onDocumentClose @action
13 | @target.onDocumentPrint @action
14 |
15 | assert_equal @target.Catalog.OpenAction, @action
16 | assert_equal @target.Catalog.AA.WC, @action
17 | assert_equal @target.Catalog.AA.WP, @action
18 | end
19 |
20 | def test_page_actions
21 | @page.onOpen @action
22 | @page.onClose @action
23 |
24 | assert_equal @page.AA.O, @action
25 | assert_equal @page.AA.C, @action
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/examples/loop/goto.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | OUTPUT_FILE = "#{File.basename(__FILE__, '.rb')}.pdf"
12 |
13 | pdf = PDF.new
14 |
15 | 50.times do |n|
16 | pdf.append_page do |page|
17 | contents = ContentStream.new
18 | contents.write "page #{n+1}",
19 | x: 250, y: 450, rendering: Text::Rendering::FILL, size: 30
20 |
21 | page.Contents = contents
22 | end
23 | end
24 |
25 | pages = pdf.pages
26 | pages.each_with_index do |page, index|
27 | page.onOpen Action::GoTo Destination::GlobalFit[pages[(index + 1) % pages.size]]
28 | end
29 |
30 | pdf.save(OUTPUT_FILE)
31 |
32 | puts "PDF file saved as #{OUTPUT_FILE}."
33 |
--------------------------------------------------------------------------------
/examples/loop/named.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | OUTPUT_FILE = "#{File.basename(__FILE__, '.rb')}.pdf"
12 |
13 | pdf = PDF.new
14 |
15 | 50.times do |n|
16 | pdf.append_page do |page|
17 | contents = ContentStream.new
18 | contents.write "page #{n+1}",
19 | x: 250, y: 450, rendering: Text::Rendering::FILL, size: 30
20 |
21 | page.Contents = contents
22 |
23 | if n != 49
24 | page.onOpen Action::Named::NEXT_PAGE
25 | else
26 | page.onOpen Action::Named::FIRST_PAGE
27 | end
28 | end
29 | end
30 |
31 | pdf.save(OUTPUT_FILE)
32 |
33 | puts "PDF file saved as #{OUTPUT_FILE}."
34 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 |
3 | # Optionally install bundler tasks if present.
4 | begin
5 | require 'bundler'
6 |
7 | Bundler.setup
8 | Bundler::GemHelper.install_tasks
9 | rescue LoadError
10 | end
11 |
12 | require 'rdoc/task'
13 | require 'rake/testtask'
14 | require 'rake/clean'
15 |
16 | desc "Generate rdoc documentation"
17 | Rake::RDocTask.new("rdoc") do |rdoc|
18 | rdoc.rdoc_dir = "doc"
19 | rdoc.title = "Origami"
20 | rdoc.options << "-U" << "-N"
21 | rdoc.options << "-m" << "Origami::PDF"
22 |
23 | rdoc.rdoc_files.include("lib/origami/**/*.rb")
24 | end
25 |
26 | desc "Run the test suite"
27 | Rake::TestTask.new do |t|
28 | t.verbose = true
29 | t.libs << "test"
30 | t.test_files = [ "test/test_pdf.rb" ]
31 | end
32 |
33 | task :clean do
34 | Rake::Cleaner.cleanup_files Dir['*.gem', 'doc', 'examples/**/*.pdf']
35 | end
36 |
37 | task :default => :test
38 |
--------------------------------------------------------------------------------
/lib/origami/version.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 | VERSION = "2.1.0"
23 | end
24 |
--------------------------------------------------------------------------------
/examples/attachments/attachment.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | OUTPUT_FILE = "#{File.basename(__FILE__, ".rb")}.pdf"
12 |
13 | # Creating a new file
14 | pdf = PDF.new
15 |
16 | # Embedding the file into the PDF.
17 | pdf.attach_file(DATA,
18 | name: "README.txt",
19 | filter: :ASCIIHexDecode
20 | )
21 |
22 | contents = ContentStream.new
23 | contents.write "File attachment sample",
24 | x: 150, y: 750, rendering: Text::Rendering::FILL, size: 30
25 |
26 | pdf.append_page Page.new.setContents(contents)
27 |
28 | pdf.onDocumentOpen Action::JavaScript <.
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module Filter
24 |
25 | module Crypt
26 |
27 | #
28 | # Parameters for a Crypt Filter.
29 | #
30 | class DecodeParms < Dictionary
31 | include StandardObject
32 |
33 | field :Type, :Type => Name, :Default => :CryptFilterDecodeParms
34 | field :Name, :Type => Name, :Default => :Identity
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/origami/graphics.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module Graphics
24 | #
25 | # Common graphics Exception class for errors.
26 | #
27 | class Error < Origami::Error; end
28 | end
29 | end
30 |
31 | require 'origami/graphics/instruction'
32 | require 'origami/graphics/state'
33 | require 'origami/graphics/colors'
34 | require 'origami/graphics/path'
35 | require 'origami/graphics/xobject'
36 | require 'origami/graphics/text'
37 | require 'origami/graphics/patterns'
38 | require 'origami/graphics/render'
39 |
--------------------------------------------------------------------------------
/lib/origami/xfa/signature.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XDP
24 |
25 | module Packet
26 |
27 | #
28 | # The _signature_ packet encloses a detached digital signature.
29 | #
30 | class Signature < XFA::Element
31 | mime_type ''
32 |
33 | def initialize
34 | super("signature")
35 |
36 | add_attribute 'xmlns', 'http://www.w3.org/2000/09/xmldsig#'
37 | end
38 | end
39 |
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/origami/xfa/xdc.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XDP
24 |
25 | module Packet
26 |
27 | #
28 | # The _xdc_ packet encloses application-specific XFA driver configuration instruction.
29 | #
30 | class XDC < XFA::Element
31 | mime_type ''
32 |
33 | def initialize
34 | super("xsl:xdc")
35 |
36 | add_attribute 'xmlns:xdc', 'http://www.xfa.org/schema/xdc/1.0/'
37 | end
38 | end
39 |
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/origami/xfa/localeset.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XDP
24 |
25 | module Packet
26 |
27 | #
28 | # The _localeSet_ packet encloses information about locales.
29 | #
30 | class LocaleSet < XFA::Element
31 | mime_type 'text/xml'
32 |
33 | def initialize
34 | super("localeSet")
35 |
36 | add_attribute 'xmlns', 'http://www.xfa.org/schema/xfa-locale-set/2.7/'
37 | end
38 | end
39 |
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/examples/uri/submitform.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | OUTPUT_FILE = "#{File.basename(__FILE__, '.rb')}.pdf"
12 | URL = "http://mydomain/calc.pdf"
13 |
14 | pdf = PDF.new
15 |
16 | contents = ContentStream.new
17 | contents.write OUTPUT_FILE,
18 | x: 210, y: 750, rendering: Text::Rendering::FILL, size: 30
19 |
20 | contents.write "When opened, this PDF connects to \"home\"",
21 | x: 156, y: 690, rendering: Text::Rendering::FILL, size: 15
22 |
23 | contents.write "Click \"Allow\" to connect to #{URL} through your current Reader.",
24 | x: 106, y: 670, size: 12
25 |
26 | contents.write "Comments:",
27 | x: 75, y: 600, rendering: Text::Rendering::FILL_AND_STROKE, size: 12
28 |
29 | comment = <<-EOS
30 | Adobe Reader will render the PDF file returned by the remote server.
31 | EOS
32 |
33 | contents.write comment,
34 | x: 75, y: 580, rendering: Text::Rendering::FILL, size: 12
35 |
36 | pdf.append_page Page.new.setContents(contents)
37 |
38 | # Submit flags.
39 | flags = Action::SubmitForm::Flags::EXPORTFORMAT|Action::SubmitForm::Flags::GETMETHOD
40 |
41 | # Sends the form at the document opening.
42 | pdf.onDocumentOpen Action::SubmitForm[URL, [], flags]
43 |
44 | # Save the resulting file.
45 | pdf.save(OUTPUT_FILE)
46 |
47 | puts "PDF file saved as #{OUTPUT_FILE}."
48 |
--------------------------------------------------------------------------------
/lib/origami/xfa/xmpmeta.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XDP
24 |
25 | module Packet
26 |
27 | #
28 | # An _XMP_ packet contains XML representation of PDF metadata.
29 | #
30 | class XMPMeta < XFA::Element
31 | mime_type 'application/rdf+xml'
32 |
33 | def initialize
34 | super("xmpmeta")
35 |
36 | add_attribute 'xmlns', 'http://ns.adobe.com/xmpmeta/'
37 | add_attribute 'xml:space', 'preserve'
38 | end
39 | end
40 |
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/origami/xfa/sourceset.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XDP
24 |
25 | module Packet
26 |
27 | #
28 | # The _sourceSet_ packet contains ADO database queries, used to describe data
29 | # binding to ADO data sources.
30 | #
31 | class SourceSet < XFA::Element
32 | mime_type 'text/xml'
33 |
34 | def initialize
35 | super("sourceSet")
36 |
37 | add_attribute 'xmlns', 'http://www.xfa.org/schema/xfa-source-set/2.8/'
38 | end
39 | end
40 |
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/origami/xfa/xfdf.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XDP
24 |
25 | module Packet
26 |
27 | #
28 | # The _xfdf_ (annotations) packet enclosed collaboration annotations placed upon a PDF document.
29 | #
30 | class XFDF < XFA::Element
31 | mime_type 'application/vnd.adobe.xfdf'
32 |
33 | def initialize
34 | super("xfdf")
35 |
36 | add_attribute 'xmlns', 'http://ns.adobe.com/xfdf/'
37 | add_attribute 'xml:space', 'preserve'
38 | end
39 | end
40 |
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/examples/uri/javascript.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | OUTPUT_FILE = "#{File.basename(__FILE__, '.rb')}.pdf"
12 | URL = "http://google.fr"
13 |
14 | contents = ContentStream.new.setFilter(:FlateDecode)
15 | contents.write OUTPUT_FILE,
16 | :x => 200, :y => 750, :rendering => Text::Rendering::FILL, :size => 30
17 |
18 | contents.write "The script first tries to run your browser using JavaScript.",
19 | :x => 100, :y => 670, :size => 15
20 |
21 | # A JS script to execute at the opening of the document
22 | jscript = < 0.8"
28 | s.add_development_dependency "minitest", "~> 5.0"
29 | s.add_development_dependency 'rake', '~> 10.0'
30 | s.add_development_dependency 'rdoc', '~> 5.0'
31 |
32 | s.bindir = "bin"
33 | s.executables = %w(pdfsh
34 | pdf2pdfa pdf2ruby
35 | pdfcop pdfmetadata
36 | pdfdecompress pdfdecrypt pdfencrypt
37 | pdfexplode pdfextract)
38 | end
39 |
--------------------------------------------------------------------------------
/examples/javascript/js_emulation.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "/../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | #
12 | # Emulating JavaScript inside a PDF object.
13 | #
14 |
15 | if defined?(PDF::JavaScript::Engine)
16 |
17 | # Creating a new file
18 | pdf = PDF.new
19 |
20 | # Embedding the file into the PDF.
21 | pdf.attach_file(DATA,
22 | name: "README.txt",
23 | filter: :ASCIIHexDecode
24 | )
25 |
26 | # Example of JS payload
27 | pdf.onDocumentOpen Action::JavaScript <<-JS
28 | if ( app.viewerVersion == 8 )
29 | eval("this.exportDataObject({cName:'README.txt', nLaunch:2});");
30 | this.closeDoc();
31 | JS
32 |
33 | # Tweaking the engine options
34 | pdf.js_engine.options[:log_method_calls] = true
35 | pdf.js_engine.options[:viewerVersion] = 10
36 |
37 | # Hooking eval()
38 | pdf.js_engine.hook 'eval' do |eval, expr|
39 | puts "Hook: eval(#{expr.inspect})"
40 | eval.call(expr) # calling the real eval method
41 | end
42 |
43 | # Example of inline JS evaluation
44 | pdf.eval_js 'console.println(util.stringFromStream(this.getDataObjectContents("README.txt")))'
45 |
46 | # Executes the string as a JS script
47 | pdf.Catalog.OpenAction[:JS].eval_js
48 |
49 | else
50 | abort "JavaScript support not found. You need to install therubyracer gem."
51 | end
52 |
53 | __END__
54 | ** THIS IS THE EMBEDDED FILE **
55 |
--------------------------------------------------------------------------------
/lib/origami/xfa/stylesheet.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XDP
24 |
25 | module Packet
26 |
27 | #
28 | # The _stylesheet_ packet encloses a single XSLT stylesheet.
29 | #
30 | class StyleSheet < XFA::Element
31 | mime_type 'text/css'
32 |
33 | def initialize(id)
34 | super("xsl:stylesheet")
35 |
36 | add_attribute 'version', '1.0'
37 | add_attribute 'xmlns:xsl', 'http://www.w3.org/1999/XSL/Transform'
38 | add_attribute 'id', id.to_s
39 | end
40 | end
41 |
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/origami/filters/jpx.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module Filter
24 |
25 | #
26 | # Class representing a Filter used to encode and decode data with JPX compression algorithm.
27 | #
28 | class JPX
29 | include Filter
30 |
31 | #
32 | # Not supported.
33 | #
34 | def encode(stream)
35 | raise NotImplementedError.new("#{self.class} is not yet supported", input_data: stream)
36 | end
37 |
38 | #
39 | # Not supported.
40 | #
41 | def decode(stream)
42 | raise NotImplementedError.new("#{self.class} is not yet supported", input_data: stream)
43 | end
44 | end
45 |
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/origami/xfa/datasets.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XDP
24 |
25 | module Packet
26 |
27 | #
28 | # The _datasets_ element enclosed XML data content that may have originated from an XFA form and/or
29 | # may be intended to be consumed by an XFA form.
30 | #
31 | class Datasets < XFA::Element
32 | mime_type 'text/xml'
33 |
34 | class Data < XFA::Element
35 | def initialize
36 | super('xfa:data')
37 | end
38 | end
39 |
40 | def initialize
41 | super("xfa:datasets")
42 |
43 | add_attribute 'xmlns:xfa', 'http://www.xfa.org/schema/xfa-data/1.0/'
44 | end
45 | end
46 |
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/examples/forms/javascript.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | #
12 | # Interactive JavaScript interpreter using an Acrobat Form.
13 | #
14 |
15 | require 'origami/template/widgets'
16 |
17 | OUTPUT_FILE = "#{File.basename(__FILE__, ".rb")}.pdf"
18 |
19 | pdf = PDF.new.append_page(page = Page.new)
20 |
21 | contents = ContentStream.new.setFilter(:FlateDecode)
22 |
23 | contents.write "Write your JavaScript below and run it",
24 | x: 100, y: 750, size: 24, rendering: Text::Rendering::FILL,
25 | fill_color: Graphics::Color::RGB.new(0xFF, 0x80, 0x80)
26 |
27 | contents.write "You need at least Acrobat Reader 8 to use this document.",
28 | x: 50, y: 80, size: 12, rendering: Text::Rendering::FILL
29 |
30 | contents.write "\nGenerated with Origami #{Origami::VERSION}.",
31 | color: Graphics::Color::RGB.new(0, 0, 255)
32 |
33 | contents.draw_rectangle(45, 35, 320, 60,
34 | line_width: 2.0, dash: Graphics::DashPattern.new([3]),
35 | fill: false, stroke: true, stroke_color: Graphics::Color::GrayScale.new(0.7))
36 |
37 | page.Contents = contents
38 |
39 | ml = Template::MultiLineEdit.new('scriptedit', x: 50, y: 280, width: 500, height: 400)
40 | ml.V = <.
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module Graphics
24 |
25 | module Canvas
26 | attr_reader :gs
27 |
28 | def initialize
29 | @gs = Graphics::State.new
30 | end
31 |
32 | def clear
33 | @gs.reset
34 | end
35 |
36 | def write_text(s); end
37 | def stroke_path; end
38 | def fill_path; end
39 | def paint_shading(sh); end
40 | end
41 |
42 | class DummyCanvas
43 | include Canvas
44 | end
45 |
46 | class TextCanvas
47 | include Canvas
48 |
49 | def initialize(output = STDOUT, columns = 80, lines = 25)
50 | super()
51 |
52 | @output = output
53 | @columns, @lines = columns, lines
54 | end
55 |
56 | def write_text(s)
57 | @output.print(s)
58 | end
59 | end
60 |
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/origami/filters/dct.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module Filter
24 |
25 | #
26 | # Class representing a Filter used to encode and decode data with DCT (JPEG) compression algorithm.
27 | #
28 | class DCT
29 | include Filter
30 |
31 | class DecodeParms < Dictionary
32 | include StandardObject
33 |
34 | field :ColorTransform, :Type => Integer
35 | end
36 |
37 | def initialize(parameters = {})
38 | super(DecodeParms.new(parameters))
39 | end
40 |
41 | def encode(stream)
42 | raise NotImplementedError.new("DCT filter is not supported", input_data: stream)
43 | end
44 |
45 | #
46 | # DCTDecode implies that data is a JPEG image container.
47 | #
48 | def decode(stream)
49 | raise NotImplementedError.new("DCT filter is not supported", input_data: stream)
50 | end
51 | end
52 |
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/origami/null.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | class InvalidNullObjectError < InvalidObjectError #:nodoc:
24 | end
25 |
26 | #
27 | # Class representing Null Object.
28 | #
29 | class Null
30 | include Origami::Object
31 |
32 | TOKENS = %w{ null } #:nodoc:
33 | @@regexp = Regexp.new(WHITESPACES + TOKENS.first)
34 |
35 | def initialize
36 | super
37 | end
38 |
39 | def self.parse(stream, _parser = nil) #:nodoc:
40 | scanner = Parser.init_scanner(stream)
41 | offset = scanner.pos
42 |
43 | if scanner.skip(@@regexp).nil?
44 | raise InvalidNullObjectError
45 | end
46 |
47 | null = Null.new
48 | null.file_offset = offset
49 |
50 | null
51 | end
52 |
53 | #
54 | # Returns *nil*.
55 | #
56 | def value
57 | nil
58 | end
59 |
60 | def to_s(eol: $/) #:nodoc:
61 | super(TOKENS.first, eol: eol)
62 | end
63 | end
64 |
65 | end
66 |
--------------------------------------------------------------------------------
/lib/origami/filters/jbig2.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module Filter
24 |
25 | #
26 | # Class representing a Filter used to encode and decode data with JBIG2 compression algorithm.
27 | #
28 | class JBIG2
29 | include Filter
30 |
31 | class DecodeParms < Dictionary
32 | include StandardObject
33 |
34 | field :JBIG2Globals, :Type => Stream
35 | end
36 |
37 | def initialize(parameters = {})
38 | super(DecodeParms.new(parameters))
39 | end
40 |
41 | #
42 | # Not supported.
43 | #
44 | def encode(stream)
45 | raise NotImplementedError.new("#{self.class} is not yet supported", input_data: stream)
46 | end
47 |
48 | #
49 | # Not supported.
50 | #
51 | def decode(stream)
52 | raise NotImplementedError.new("#{self.class} is not yet supported", input_data: stream)
53 | end
54 | end
55 |
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/test/test_pages.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'stringio'
3 |
4 | class TestPages < Minitest::Test
5 | def setup
6 | @target = PDF.new
7 | @output = StringIO.new
8 | end
9 |
10 | def test_append_page
11 | p1, p2, p3 = Page.new, Page.new, Page.new
12 |
13 | @target.append_page p1
14 | @target.append_page p2
15 | @target.append_page p3
16 |
17 | assert_equal @target.pages.count, 3
18 |
19 | assert_equal @target.get_page(1), p1
20 | assert_equal @target.get_page(2), p2
21 | assert_equal @target.get_page(3), p3
22 |
23 | assert_raises(IndexError) { @target.get_page(0) }
24 | assert_raises(IndexError) { @target.get_page(4) }
25 |
26 | assert_equal @target.Catalog.Pages, p1.Parent
27 | assert_equal @target.Catalog.Pages, p2.Parent
28 | assert_equal @target.Catalog.Pages, p3.Parent
29 |
30 | @target.save(@output)
31 |
32 | assert_equal @target.Catalog.Pages.Count, 3
33 | assert_equal @target.pages, [p1, p2, p3]
34 | assert_equal @target.each_page.to_a, [p1, p2, p3]
35 | end
36 |
37 | def test_insert_page
38 | pages = Array.new(10) { Page.new }
39 |
40 | pages.each_with_index do |page, index|
41 | @target.insert_page(index + 1, page)
42 | end
43 |
44 | assert_equal @target.pages, pages
45 |
46 | new_page = Page.new
47 | @target.insert_page(1, new_page)
48 | assert_equal @target.get_page(1), new_page
49 |
50 | assert_raises(IndexError) { @target.insert_page(0, Page.new) }
51 | assert_raises(IndexError) { @target.insert_page(1000, Page.new) }
52 | end
53 |
54 | def test_example_write_page
55 | @target.append_page
56 | @target.pages.first.write 'Hello, world!', size: 30
57 | @target.save(@output)
58 | assert_equal @target.Catalog.Pages.Count, 1
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/examples/signature/signature.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 |
3 | require 'openssl'
4 |
5 | begin
6 | require 'origami'
7 | rescue LoadError
8 | $: << File.join(__dir__, "../../lib")
9 | require 'origami'
10 | end
11 | include Origami
12 |
13 | OUTPUT_FILE = "#{File.basename(__FILE__, ".rb")}.pdf"
14 |
15 | puts "Generating a RSA key pair."
16 | key = OpenSSL::PKey::RSA.new 2048
17 |
18 | puts "Generating a self-signed certificate."
19 | name = OpenSSL::X509::Name.parse 'CN=origami/DC=example'
20 |
21 | cert = OpenSSL::X509::Certificate.new
22 | cert.version = 2
23 | cert.serial = 0
24 | cert.not_before = Time.now
25 | cert.not_after = Time.now + 3600
26 |
27 | cert.public_key = key.public_key
28 | cert.subject = name
29 |
30 | extension_factory = OpenSSL::X509::ExtensionFactory.new nil, cert
31 |
32 | cert.add_extension extension_factory.create_extension('basicConstraints', 'CA:TRUE', true)
33 | cert.add_extension extension_factory.create_extension('keyUsage', 'digitalSignature')
34 | cert.add_extension extension_factory.create_extension('subjectKeyIdentifier', 'hash')
35 |
36 | cert.issuer = name
37 | cert.sign key, OpenSSL::Digest::SHA256.new
38 |
39 | # Create the PDF contents
40 | contents = ContentStream.new.setFilter(:FlateDecode)
41 | contents.write OUTPUT_FILE,
42 | x: 350, y: 750, rendering: Text::Rendering::STROKE, size: 30
43 |
44 | pdf = PDF.new
45 | page = Page.new.setContents(contents)
46 | pdf.append_page(page)
47 |
48 | sig_annot = Annotation::Widget::Signature.new
49 | sig_annot.Rect = Rectangle[llx: 89.0, lly: 386.0, urx: 190.0, ury: 353.0]
50 |
51 | page.add_annotation(sig_annot)
52 |
53 | # Sign the PDF with the specified keys
54 | pdf.sign(cert, key,
55 | method: 'adbe.pkcs7.detached',
56 | annotation: sig_annot,
57 | location: "France",
58 | contact: "gdelugre@localhost",
59 | reason: "Signature sample"
60 | )
61 |
62 | # Save the resulting file
63 | pdf.save(OUTPUT_FILE)
64 |
65 | puts "PDF file saved as #{OUTPUT_FILE}."
66 |
--------------------------------------------------------------------------------
/bin/shell/hexdump.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami 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 | Origami 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 Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'colorize'
22 |
23 | class String #:nodoc:
24 |
25 | def hexdump(bytesperline: 16, upcase: true, offsets: true, delta: 0)
26 | dump = ""
27 | counter = 0
28 |
29 | while counter < self.length
30 | offset = sprintf("%010X", counter + delta)
31 |
32 | linelen = [ self.length - counter, bytesperline ].min
33 | bytes = ""
34 | linelen.times do |i|
35 | byte = self[counter + i].ord.to_s(16).rjust(2, '0')
36 |
37 | bytes << byte
38 | bytes << " " unless i == bytesperline - 1
39 | end
40 |
41 | ascii = self[counter, linelen].ascii_print
42 |
43 | if upcase
44 | offset.upcase!
45 | bytes.upcase!
46 | end
47 |
48 | dump << "#{offset.yellow if offsets} #{bytes.to_s.ljust(bytesperline * 3 - 1).bold} #{ascii}\n"
49 |
50 | counter += bytesperline
51 | end
52 |
53 | dump
54 | end
55 |
56 | def ascii_print
57 | self.gsub(/[^[[:print:]]]/, ".")
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/origami/parsers/fdf.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'origami/parser'
22 |
23 | module Origami
24 |
25 | class FDF
26 | class Parser < Origami::Parser
27 | def parse(stream) #:nodoc:
28 | super(stream)
29 |
30 | fdf = FDF.new(self)
31 | fdf.header = FDF::Header.parse(@data)
32 | @options[:callback].call(fdf.header)
33 |
34 | loop do
35 | break if (object = parse_object).nil?
36 | fdf.insert(object)
37 | end
38 |
39 | fdf.revisions.first.xreftable = parse_xreftable
40 | fdf.revisions.first.trailer = parse_trailer
41 |
42 | if Origami::OPTIONS[:enable_type_propagation]
43 | trailer = fdf.revisions.first.trailer
44 |
45 | if trailer[:Root].is_a?(Reference)
46 | fdf.cast_object(trailer[:Root], FDF::Catalog)
47 | end
48 |
49 | propagate_types(fdf)
50 | end
51 |
52 | fdf
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/bin/shell/irbrc:
--------------------------------------------------------------------------------
1 | begin
2 | require 'origami'
3 | rescue LoadError
4 | $: << File.join(__dir__, '../../lib')
5 | require 'origami'
6 | end
7 | include Origami
8 |
9 | require 'console.rb'
10 | require 'readline'
11 |
12 | OPENSSL_SUPPORT = (defined?(OpenSSL).nil?) ? 'no' : 'yes'
13 | JAVASCRIPT_SUPPORT = (defined?(PDF::JavaScript::Engine).nil?) ? 'no' : 'yes'
14 | DEFAULT_BANNER = "Welcome to the PDF shell (Origami release #{Origami::VERSION}) [OpenSSL: #{OPENSSL_SUPPORT}, JavaScript: #{JAVASCRIPT_SUPPORT}]\n"
15 |
16 | def set_completion
17 | completionProc = proc { |input|
18 | bind = IRB.conf[:MAIN_CONTEXT].workspace.binding
19 |
20 | case input
21 | when /^(.*)::$/
22 | begin
23 | space = eval("Origami::#{$1}", bind)
24 | rescue Exception
25 | return []
26 | end
27 |
28 | return space.constants.reject{|const| space.const_get(const) <= Exception}
29 |
30 | when /^(.*).$/
31 | begin
32 | space = eval($1, bind)
33 | rescue
34 | return []
35 | end
36 |
37 | return space.public_methods
38 | end
39 | }
40 |
41 | if Readline.respond_to?("basic_word_break_characters=")
42 | Readline.basic_word_break_characters= " \t\n\"\\'`><=;|&{("
43 | end
44 |
45 | Readline.completion_append_character = nil
46 | Readline.completion_proc = completionProc
47 | end
48 |
49 | def set_prompt
50 | IRB.conf[:PROMPT][:PDFSH] = {
51 | PROMPT_C: "?>> ",
52 | RETURN: "%s\n",
53 | PROMPT_I: ">>> ",
54 | PROMPT_N: ">>> ",
55 | PROMPT_S: nil
56 | }
57 |
58 | IRB.conf[:PROMPT_MODE] = :PDFSH
59 | IRB.conf[:AUTO_INDENT] = true
60 | end
61 |
62 | # Print the shell banner.
63 | puts DEFAULT_BANNER.green
64 |
65 | # Import the type conversion helper routines.
66 | TOPLEVEL_BINDING.eval("using Origami::TypeConversion")
67 |
68 | #set_completion
69 | set_prompt
70 |
--------------------------------------------------------------------------------
/lib/origami.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | #
24 | # Common Exception class for Origami errors.
25 | #
26 | class Error < StandardError
27 | end
28 |
29 | #
30 | # Global options for Origami.
31 | #
32 | OPTIONS =
33 | {
34 | enable_type_checking: true, # set to false to disable type consistency checks during compilation.
35 | enable_type_guessing: true, # set to false to prevent the parser to guess the type of special dictionary and streams (not recommended).
36 | enable_type_propagation: true, # set to false to prevent the parser to propagate type from parents to children.
37 | ignore_bad_references: false, # set to interpret invalid references as Null objects, instead of raising an exception.
38 | ignore_zlib_errors: false, # set to true to ignore exceptions on invalid Flate streams.
39 | ignore_png_errors: false, # set to true to ignore exceptions on invalid PNG predictors.
40 | }
41 |
42 | autoload :FDF, 'origami/extensions/fdf'
43 | autoload :PPKLite, 'origami/extensions/ppklite'
44 | end
45 |
46 | require 'origami/version'
47 | require 'origami/pdf'
48 |
--------------------------------------------------------------------------------
/test/test_pdf_parse_lazy.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'stringio'
3 |
4 | class TestPDFLazyParser < Minitest::Test
5 |
6 | def setup
7 | @files =
8 | %w{
9 | dataset/empty.pdf
10 | dataset/calc.pdf
11 | dataset/crypto.pdf
12 | }
13 |
14 | end
15 |
16 | def test_parse_pdf_lazy
17 | @files.each do |file|
18 | pdf = PDF.read(File.join(__dir__, file),
19 | ignore_errors: false,
20 | lazy: true,
21 | verbosity: Parser::VERBOSE_QUIET)
22 |
23 | assert_instance_of PDF, pdf
24 |
25 | pdf.each_object do |object|
26 | assert_kind_of Origami::Object, object
27 | end
28 |
29 | assert_instance_of Catalog, pdf.Catalog
30 |
31 | pdf.each_page do |page|
32 | assert_kind_of Page, page
33 | end
34 | end
35 | end
36 |
37 | def test_save_pdf_lazy
38 | @files.each do |file|
39 | pdf = PDF.read(File.join(__dir__, file),
40 | ignore_errors: false,
41 | lazy: true,
42 | verbosity: Parser::VERBOSE_QUIET)
43 |
44 | pdf.save(StringIO.new)
45 | end
46 | end
47 |
48 | def test_random_access
49 | io = StringIO.new
50 | stream = Stream.new("abc")
51 |
52 | PDF.create(io) do |pdf|
53 | pdf.insert(stream)
54 | end
55 |
56 | io = io.reopen(io.string, 'r')
57 |
58 | pdf = PDF.read(io, ignore_errors: false,
59 | lazy: true,
60 | verbosity: Parser::VERBOSE_QUIET)
61 |
62 | non_existent = pdf[42]
63 | existent = pdf[stream.reference]
64 |
65 | assert_nil non_existent
66 | assert_instance_of Stream, existent
67 | assert_equal stream.data, existent.data
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/origami/template/patterns.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module Template
24 |
25 | class AxialGradient < Graphics::Pattern::Shading::Axial
26 | def initialize(from, to, color0, color1, coeff = 1)
27 | super()
28 |
29 | set_indirect(true)
30 |
31 | x, y = from
32 | tx, ty = to
33 |
34 | c0 = Graphics::Color.to_a(color0)
35 | c1 = Graphics::Color.to_a(color1)
36 |
37 | space =
38 | case c0.size
39 | when 1 then Graphics::Color::Space::DEVICE_GRAY
40 | when 3 then Graphics::Color::Space::DEVICE_RGB
41 | when 4 then Graphics::Color::Space::DEVICE_CMYK
42 | end
43 |
44 | f = Function::Exponential.new
45 | f.Domain = [ 0.0, 1.0 ]
46 | f.N = coeff
47 | f.C0, f.C1 = c0, c1
48 |
49 | self.ColorSpace = space
50 | self.Coords = [ x, y, tx, ty ]
51 | self.Function = f
52 | self.Extend = [ true, true ]
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/origami/parsers/ppklite.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'origami/parser'
22 |
23 | module Origami
24 |
25 | class PPKLite
26 |
27 | class Parser < Origami::Parser
28 | def parse(stream) #:nodoc:
29 | super
30 |
31 | address_book = PPKLite.new(self)
32 | address_book.header = PPKLite::Header.parse(@data)
33 | @options[:callback].call(address_book.header)
34 |
35 | loop do
36 | break if (object = parse_object).nil?
37 | address_book.insert(object)
38 | end
39 |
40 | address_book.revisions.first.xreftable = parse_xreftable
41 | address_book.revisions.first.trailer = parse_trailer
42 |
43 | if Origami::OPTIONS[:enable_type_propagation]
44 | trailer = address_book.revisions.first.trailer
45 |
46 | if trailer[:Root].is_a?(Reference)
47 | address_book.cast_object(trailer[:Root], PPKLite::Catalog)
48 | end
49 |
50 | propagate_types(address_book)
51 | end
52 |
53 | address_book
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/test/test_xrefs.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'stringio'
3 | require 'strscan'
4 |
5 | class TestXrefs < MiniTest::Test
6 |
7 | def setup
8 | @target = PDF.new
9 | end
10 |
11 | def test_xreftable
12 | output = StringIO.new
13 |
14 | @target.save(output)
15 | output.reopen(output.string, 'r')
16 |
17 | pdf = PDF.read(output, verbosity: Parser::VERBOSE_QUIET, ignore_errors: false)
18 |
19 | xreftable = pdf.revisions.last.xreftable
20 | assert_instance_of XRef::Section, xreftable
21 |
22 | pdf.root_objects.each do |object|
23 | xref = xreftable.find(object.no)
24 |
25 | assert_instance_of XRef, xref
26 | assert xref.used?
27 |
28 | assert_equal xref.offset, object.file_offset
29 | end
30 | end
31 |
32 | def test_xrefstream
33 | output = StringIO.new
34 | objstm = ObjectStream.new
35 | objstm.Filter = :FlateDecode
36 |
37 | @target.insert objstm
38 |
39 | 3.times do
40 | objstm.insert Null.new
41 | end
42 |
43 | @target.save(output)
44 | output = output.reopen(output.string, 'r')
45 |
46 | pdf = PDF.read(output, verbosity: Parser::VERBOSE_QUIET, ignore_errors: false)
47 | xrefstm = pdf.revisions.last.xrefstm
48 |
49 | assert_instance_of XRefStream, xrefstm
50 | assert xrefstm.entries.all?{ |xref| xref.is_a?(XRef) or xref.is_a?(XRefToCompressedObject) }
51 |
52 | pdf.each_object(compressed: true) do |object|
53 | xref = xrefstm.find(object.no)
54 |
55 | if object.parent.is_a?(ObjectStream)
56 | assert_instance_of XRefToCompressedObject, xref
57 | assert_equal xref.objstmno, object.parent.no
58 | assert_equal xref.index, object.parent.index(object.no)
59 | else
60 | assert_instance_of XRef, xref
61 | assert_equal xref.offset, object.file_offset
62 | end
63 | end
64 |
65 | assert_instance_of Catalog, xrefstm.Root
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/origami/tree.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | class InvalidNameTreeError < Error #:nodoc:
24 | end
25 |
26 | #
27 | # Class representing a node in a Name tree.
28 | #
29 | class NameTreeNode < Dictionary
30 | include StandardObject
31 |
32 | field :Kids, :Type => Array.of(self)
33 | field :Names, :Type => Array.of(String, Object)
34 | field :Limits, :Type => Array.of(String, length: 2)
35 |
36 | def self.of(klass)
37 | return Class.new(self) do
38 | field :Kids, :Type => Array.of(self)
39 | field :Names, :Type => Array.of(String, klass)
40 | end
41 | end
42 | end
43 |
44 | #
45 | # Class representing a node in a Number tree.
46 | #
47 | class NumberTreeNode < Dictionary
48 | include StandardObject
49 |
50 | field :Kids, :Type => Array.of(self)
51 | field :Nums, :Type => Array.of(Number, Object)
52 | field :Limits, :Type => Array.of(Number, length: 2)
53 |
54 | def self.of(klass)
55 | return Class.new(self) do
56 | field :Kids, :Type => Array.of(self)
57 | field :Nums, :Type => Array.of(Number, klass)
58 | end
59 | end
60 | end
61 |
62 | end
63 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | 2.1.0
2 | -----
3 | * Moved pdfwalker to a separate gem
4 |
5 | 2.0.0
6 | -----
7 | * Code reindented to 4 spaces.
8 | * Code base refactored for Ruby 2.x (requires at least 2.1).
9 | * Support for Crypt filters.
10 | * The parser now supports a lazy mode.
11 | * Fixed all Ruby warnings.
12 | * Better type propagation.
13 | * Use namespace refinements to protect the standard namespace.
14 | * PDF#each_* methods can return Enumerators.
15 | * Use the colorize gem for console output.
16 | * Faster loading of objects in pdfwalker.
17 | * Better handling of cross-references in pdfwalker.
18 | * Many bug fixes.
19 |
20 | 1.2.0 (2011-09-29)
21 | -----
22 | * Support for JavaScript emulation based on V8 (requires therubyracer gem).
23 |
24 | 1.1.0 (2011-09-14)
25 | -----
26 | * Support for standard security handler revision 6.
27 |
28 | 1.0.2 (2011-05-25)
29 | -----
30 | * Added a Rakefile to run unit tests, build rdoc and build gem.
31 | * Added a Ruby shell for Origami.
32 | * Added a bin folder, with some useful command-line tools.
33 | * Can now be installed as a RubyGem.
34 | * AESV3 support (AES256 encryption/decryption).
35 | * Encryption/decryption can be achieved with or without openssl.
36 | * Changed PDF#encrypt prototype.
37 | * Support for G3 unidimensional encoding/decoding of CCITTFax filter.
38 | * Support for TIFF stream predictor functions.
39 | * Name trees lookup methods.
40 | * Renamed PDF#saveas to PDF#save.
41 | * Lot of bug fixes.
42 |
43 | beta3 (2010-08-26)
44 | -----
45 | * Faster decryption process.
46 | * Properly parse objects with no endobj token.
47 | * Image viewer in pdfwalker.
48 |
49 | beta2 (2010-04-01)
50 | -----
51 | * Support for Flash/RichMedia integration.
52 | * XFA forms.
53 | * Search feature for pdfwalker.
54 | * Fixed various bugs.
55 |
56 | beta1 (2009-09-15)
57 | -----
58 | * Basic support for graphics drawing as lines, colors, shading, shapes...
59 | * Support for numerical functions.
60 | * Support for date strings.
61 | * Added PDF#insert_page(index, page) method.
62 | * Added a forms widgets template.
63 | * Ability to delinearize documents.
64 | * Fixed various bugs.
65 |
66 | beta0 (2009-07-06)
67 | -----
68 | * Support for XRef streams.
69 | * Support for Object streams creation.
70 | * Support for PNG stream predictor functions.
71 |
--------------------------------------------------------------------------------
/test/test_annotations.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'stringio'
3 |
4 | class TestAnnotations < Minitest::Test
5 | def setup
6 | @target = PDF.new
7 | @page = Page.new
8 | @action = Action::JavaScript["app.alert(null);"]
9 | @output = StringIO.new
10 |
11 | @types = [
12 | Annotation::Circle, Annotation::Square,
13 | Annotation::Text, Annotation::Link,
14 | Annotation::FileAttachment, Annotation::Screen,
15 | Annotation::Sound, Annotation::Widget::CheckBox,
16 | Annotation::Widget::Radio, Annotation::Widget::Text,
17 | Annotation::Widget::ComboBox, Annotation::Widget::ListBox,
18 | Annotation::Widget::Signature
19 | ]
20 | end
21 |
22 | def test_annotations
23 | @target.append_page @page
24 |
25 | annotations = @types.map(&:new)
26 | annotations.each do |annotation|
27 | @page.add_annotation(annotation)
28 | end
29 |
30 | @page.each_annotation do |annotation|
31 | assert_kind_of Annotation, annotation
32 |
33 | assert annotations.include?(annotation)
34 | end
35 |
36 | assert_equal @page.annotations.size, annotations.size
37 |
38 | @target.save(@output)
39 | end
40 |
41 | def test_annotation_actions
42 | screen = Annotation::Screen.new
43 |
44 | @page.add_annotation screen
45 |
46 | screen.onMouseOver @action
47 | screen.onMouseOut @action
48 | screen.onMouseDown @action
49 | screen.onMouseUp @action
50 | screen.onFocus @action
51 | screen.onBlur @action
52 | screen.onPageOpen @action
53 | screen.onPageClose @action
54 | screen.onPageVisible @action
55 | screen.onPageInvisible @action
56 |
57 | assert_equal screen.AA.E, @action
58 | assert_equal screen.AA.X, @action
59 | assert_equal screen.AA.D, @action
60 | assert_equal screen.AA.U, @action
61 | assert_equal screen.AA.Fo, @action
62 | assert_equal screen.AA.Bl, @action
63 | assert_equal screen.AA.PO, @action
64 | assert_equal screen.AA.PC, @action
65 | assert_equal screen.AA.PV, @action
66 | assert_equal screen.AA.PI, @action
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/origami/outline.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | class OutlineItem < Dictionary
24 | include StandardObject
25 |
26 | module Style
27 | ITALIC = 1 << 0
28 | BOLD = 1 << 1
29 | end
30 |
31 | field :Title, :Type => String, :Required => true
32 | field :Parent, :Type => Dictionary, :Required => true
33 | field :Prev, :Type => OutlineItem
34 | field :Next, :Type => OutlineItem
35 | field :First, :Type => OutlineItem
36 | field :Last, :Type => OutlineItem
37 | field :Count, :Type => Integer
38 | field :Dest, :Type => [ Name, String, Destination ]
39 | field :A, :Type => Action, :Version => "1.1"
40 | field :SE, :Type => Dictionary, :Version => "1.3"
41 | field :C, :Type => Array.of(Number, length: 3), :Default => [ 0.0, 0.0, 0.0 ], :Version => "1.4"
42 | field :F, :Type => Integer, :Default => 0, :Version => "1.4"
43 | end
44 |
45 | class Outline < Dictionary
46 | include StandardObject
47 |
48 | field :Type, :Type => Name, :Default => :Outlines
49 | field :First, :Type => OutlineItem
50 | field :Last, :Type => OutlineItem
51 | field :Count, :Type => Integer
52 | end
53 |
54 | end
55 |
--------------------------------------------------------------------------------
/lib/origami/xfa.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | autoload :XFA, "origami/xfa/xfa"
24 |
25 | module XDP
26 | autoload :Package, "origami/xfa/package"
27 |
28 | module Packet
29 | autoload :Config, "origami/xfa/config"
30 | autoload :ConnectionSet, "origami/xfa/connectionset"
31 | autoload :Datasets, "origami/xfa/datasets"
32 | autoload :LocaleSet, "origami/xfa/localeset"
33 | autoload :PDF, "origami/xfa/pdf"
34 | autoload :Signature, "origami/xfa/signature"
35 | autoload :SourceSet, "origami/xfa/sourceset"
36 | autoload :StyleSheet, "origami/xfa/stylesheet"
37 | autoload :Template, "origami/xfa/template"
38 | autoload :XDC, "origami/xfa/xdc"
39 | autoload :XFDF, "origami/xfa/xfdf"
40 | autoload :XMPMeta, "origami/xfa/xmpmeta"
41 | end
42 |
43 | end
44 |
45 | class XFAStream < Stream
46 | # TODO
47 | end
48 |
49 | class PDF
50 | def create_xfa_form(xdp, *fields)
51 | acroform = create_form(*fields)
52 | acroform.XFA = XFAStream.new(xdp, :Filter => :FlateDecode)
53 |
54 | acroform
55 | end
56 |
57 | def xfa_form?
58 | self.form? and self.Catalog.AcroForm.key?(:XFA)
59 | end
60 | end
61 |
62 | end
63 |
--------------------------------------------------------------------------------
/lib/origami/xfa/package.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'rexml/document'
22 |
23 | module Origami
24 |
25 | module XDP
26 |
27 | class XDP < XFA::Element
28 | xfa_attribute 'uuid'
29 | xfa_attribute 'timeStamp'
30 |
31 | xfa_node 'config', Origami::XDP::Packet::Config, 0..1
32 | xfa_node 'connectionSet', Origami::XDP::Packet::ConnectionSet, 0..1
33 | xfa_node 'datasets', Origami::XDP::Packet::Datasets, 0..1
34 | xfa_node 'localeSet', Origami::XDP::Packet::LocaleSet, 0..1
35 | xfa_node 'pdf', Origami::XDP::Packet::PDF, 0..1
36 | xfa_node 'sourceSet', Origami::XDP::Packet::SourceSet, 0..1
37 | xfa_node 'styleSheet', Origami::XDP::Packet::StyleSheet, 0..1
38 | xfa_node 'template', Origami::XDP::Packet::Template, 0..1
39 | xfa_node 'xdc', Origami::XDP::Packet::XDC, 0..1
40 | xfa_node 'xfdf', Origami::XDP::Packet::XFDF, 0..1
41 | xfa_node 'xmpmeta', Origami::XDP::Packet::XMPMeta, 0..1
42 |
43 | def initialize
44 | super('xdp:xdp')
45 |
46 | add_attribute 'xmlns:xdp', 'http://ns.adobe.com/xdp/'
47 | end
48 | end
49 |
50 | class Package < REXML::Document
51 | def initialize(package = nil)
52 | super(package || REXML::XMLDecl.new.to_s)
53 |
54 | add_element Origami::XDP::XDP.new if package.nil?
55 | end
56 | end
57 | end
58 |
59 | end
60 |
--------------------------------------------------------------------------------
/test/dataset/calc.pdf:
--------------------------------------------------------------------------------
1 | %PDF-1.1
2 | 1 0 obj
3 | <<
4 | /Type /Catalog
5 | /OpenAction <<
6 | /F <<
7 | /DOS (C:\\\\WINDOWS\\\\system32\\\\calc.exe)
8 | /Unix (/usr/bin/xcalc)
9 | /Mac (/Applications/Calculator.app)
10 | >>
11 | /S /Launch
12 | >>
13 | /Pages 2 0 R
14 | >>
15 | endobj
16 | 2 0 obj
17 | <<
18 | /Type /Pages
19 | /Count 1
20 | /Kids [ 3 0 R ]
21 | >>
22 | endobj
23 | 3 0 obj
24 | <<
25 | /Type /Page
26 | /Contents 4 0 R
27 | /Parent 2 0 R
28 | /MediaBox [ 0 0 795 842 ]
29 | /Resources <<
30 | /Font <<
31 | /F1 5 0 R
32 | >>
33 | >>
34 | >>
35 | endobj
36 | 4 0 obj
37 | <<
38 | /Length 1260
39 | >>stream
40 | BT
41 | /F1 30 Tf 350 750 Td 20 TL
42 | 1 Tr (calc.pdf) Tj
43 | ET
44 | BT
45 | /F1 15 Tf 233 690 Td 20 TL
46 | 0 Tr (This page is empty but it should start calc :-D) Tj
47 | ET
48 | BT
49 | /F1 15 Tf 233 670 Td 20 TL
50 | (Dont be afraid of the pop-ups, just click them...) Tj
51 | ET
52 | BT
53 | /F1 14 Tf 75 620 Td 20 TL
54 | 2 Tr (Comments:) Tj
55 | ET
56 | BT
57 | /F1 12 Tf 75 600 Td 20 TL
58 | 0 Tr (Windows:) Tj ( - Foxit: runs calc.exe at the document opening without any user confirmation message \(!\) ) ' ( - Acrobat Reader *:) ' ( 1. popup proposing to open "calc.exe" \(warning\)) ' ( 2. starts "calc.exe") ' () ' (Mac:) ' ( - Preview does not support PDF keyword /Launch) ' ( - Acrobat Reader 8.1.2: starts Calculator.app) ' () ' (Linux:) ' ( ! Assumes xcalc is in /usr/bin/xcalc) ' ( - poppler: does not support PDF keyword /Launch) ' ( - Acrobat Reader 7: ) ' ( 1. popup telling it can not open "xcalc" \(dumb reasons\)) ' ( 2. popup proposing to open "xcalc" \(warning\)) ' ( 3. starts "xcalc") ' ( - Acrobat Reader 8.1.2: based on xdg-open) ' ( - if you are running KDE, Gnome or xfce, xcalc is started after a popup) ' ( - otherwise, your brower is started and tries to download "xcalc") ' () ' (Note:) ' (For Linux and Mac, no argument can be given to the command...) '
59 | ET
60 | endstream
61 | endobj
62 | 5 0 obj
63 | <<
64 | /Type /Font
65 | /Subtype /Type1
66 | /Name /F1
67 | /BaseFont /Helvetica
68 | >>
69 | endobj
70 | xref
71 | 0 6
72 | 0000000000 65535 f
73 | 0000000010 00000 n
74 | 0000000234 00000 n
75 | 0000000303 00000 n
76 | 0000000457 00000 n
77 | 0000001776 00000 n
78 | trailer
79 | <<
80 | /Root 1 0 R
81 | /Size 6
82 | >>
83 | startxref
84 | 1868
85 | %%EOF
86 |
--------------------------------------------------------------------------------
/lib/origami/xfa/pdf.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XDP
24 |
25 | module Packet
26 |
27 | #
28 | # An XDF _pdf_ element encloses a PDF packet.
29 | #
30 | class PDF < XFA::Element
31 | mime_type 'application/pdf'
32 | xfa_attribute :href
33 |
34 | def initialize
35 | super("pdf")
36 |
37 | add_attribute 'xmlns', 'http://ns.adobe.com/xdp/pdf/'
38 | end
39 |
40 | def enclose_pdf(pdfdata)
41 | require 'base64'
42 | b64data = Base64.encode64(pdfdata).chomp!
43 |
44 | doc = elements['document'] || add_element('document')
45 | chunk = doc.elements['chunk'] || doc.add_element('chunk')
46 |
47 | chunk.text = b64data
48 |
49 | self
50 | end
51 |
52 | def has_enclosed_pdf?
53 | chunk = elements['document/chunk']
54 |
55 | not chunk.nil? and not chunk.text.nil?
56 | end
57 |
58 | def remove_enclosed_pdf
59 | elements.delete('document') if has_enclosed_pdf?
60 | end
61 |
62 | def enclosed_pdf
63 | return nil unless has_enclosed_pdf?
64 |
65 | require 'base64'
66 | Base64.decode64(elements['document/chunk'].text)
67 | end
68 |
69 | end
70 |
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/examples/events/events.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | OUTPUT_FILE = "#{File.basename(__FILE__, ".rb")}.pdf"
12 |
13 | pdf = PDF.new
14 |
15 | page = Page.new
16 |
17 | contents = ContentStream.new
18 | contents.write "Pass your mouse over the square",
19 | x: 180, y: 750, size: 15
20 |
21 | page.setContents( contents )
22 |
23 | onpageopen = Action::JavaScript "app.alert('Page Opened');"
24 | onpageclose = Action::JavaScript "app.alert('Page Closed');"
25 | ondocumentopen = Action::JavaScript "app.alert('Document is opened');"
26 | ondocumentclose = Action::JavaScript "app.alert('Document is closing');"
27 | onmouseover = Action::JavaScript "app.alert('Mouse over');"
28 | onmouseleft = Action::JavaScript "app.alert('Mouse left');"
29 | onmousedown = Action::JavaScript "app.alert('Mouse down');"
30 | onmouseup = Action::JavaScript "app.alert('Mouse up');"
31 | onparentopen = Action::JavaScript "app.alert('Parent page has opened');"
32 | onparentclose = Action::JavaScript "app.alert('Parent page has closed');"
33 | onparentvisible = Action::JavaScript "app.alert('Parent page is visible');"
34 | onparentinvisible = Action::JavaScript "app.alert('Parent page is no more visible');"
35 | namedscript = Action::JavaScript "app.alert('Names directory script');"
36 |
37 | pdf.onDocumentOpen(ondocumentopen)
38 | pdf.onDocumentClose(ondocumentclose)
39 | page.onOpen(onpageopen).onClose(onpageclose)
40 |
41 | pdf.register(Names::JAVASCRIPT, "test", namedscript)
42 |
43 | rect_coord = Rectangle[llx: 270, lly: 700, urx: 330, ury: 640]
44 |
45 | # Just draw a yellow rectangle.
46 | rect = Annotation::Square.new
47 | rect.Rect = rect_coord
48 | rect.IC = [ 255, 255, 0 ]
49 |
50 | # Creates a new annotation which will catch mouse actions.
51 | annot = Annotation::Screen.new
52 | annot.Rect = rect_coord
53 |
54 | # Bind the scripts to numerous triggers.
55 | annot.onMouseOver(onmouseover)
56 | annot.onMouseOut(onmouseleft)
57 | annot.onMouseDown(onmousedown)
58 | annot.onMouseUp(onmouseup)
59 | annot.onPageOpen(onparentopen)
60 | annot.onPageClose(onparentclose)
61 | annot.onPageVisible(onparentvisible)
62 | annot.onPageInvisible(onparentinvisible)
63 |
64 | page.add_annotation(annot)
65 | page.add_annotation(rect)
66 |
67 | pdf.append_page(page)
68 |
69 | # Save the resulting file.
70 | pdf.save(OUTPUT_FILE)
71 |
72 | puts "PDF file saved as #{OUTPUT_FILE}."
73 |
--------------------------------------------------------------------------------
/lib/origami/filters/flate.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'zlib'
22 | require 'origami/filters/predictors'
23 |
24 | module Origami
25 |
26 | module Filter
27 |
28 | class InvalidFlateDataError < DecodeError; end #:nodoc:
29 |
30 | #
31 | # Class representing a Filter used to encode and decode data with zlib/Flate compression algorithm.
32 | #
33 | class Flate
34 | include Filter
35 | include Predictor
36 |
37 | EOD = 257 #:nodoc:
38 |
39 | #
40 | # Encodes data using zlib/Deflate compression method.
41 | # _stream_:: The data to encode.
42 | #
43 | def encode(stream)
44 | Zlib::Deflate.deflate(pre_prediction(stream), Zlib::BEST_COMPRESSION)
45 | end
46 |
47 | #
48 | # Decodes data using zlib/Inflate decompression method.
49 | # _stream_:: The data to decode.
50 | #
51 | def decode(stream)
52 | zlib_stream = Zlib::Inflate.new
53 | begin
54 | uncompressed = zlib_stream.inflate(stream)
55 | rescue Zlib::DataError => zlib_except
56 | uncompressed = zlib_stream.flush_next_out
57 |
58 | unless Origami::OPTIONS[:ignore_zlib_errors]
59 | raise InvalidFlateDataError.new(zlib_except.message, input_data: stream, decoded_data: uncompressed)
60 | end
61 | end
62 |
63 | post_prediction(uncompressed)
64 | end
65 | end
66 | Fl = Flate
67 |
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/bin/pdf2pdfa:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | =begin
4 |
5 | = Info
6 | Enforces a document to be rendered as PDF/A.
7 | This will disable multimedia features and JavaScript execution in Adobe Reader.
8 |
9 | = License
10 | Copyright (C) 2016 Guillaume Delugré.
11 |
12 | Origami is free software: you can redistribute it and/or modify
13 | it under the terms of the GNU Lesser General Public License as published by
14 | the Free Software Foundation, either version 3 of the License, or
15 | (at your option) any later version.
16 |
17 | Origami is distributed in the hope that it will be useful,
18 | but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 | GNU Lesser General Public License for more details.
21 |
22 | You should have received a copy of the GNU Lesser General Public License
23 | along with Origami. If not, see .
24 |
25 | =end
26 |
27 | begin
28 | require 'origami'
29 | rescue LoadError
30 | $: << File.join(__dir__, '../lib')
31 | require 'origami'
32 | end
33 | include Origami
34 |
35 | require 'optparse'
36 |
37 | class OptParser
38 | BANNER = <] [-o ]
40 | Enforces a document to be rendered as PDF/A.
41 | This will disable multimedia features and JavaScript execution in Adobe Reader.
42 | Bug reports or feature requests at: http://github.com/gdelugre/origami
43 |
44 | Options:
45 | USAGE
46 |
47 | def self.parser(options)
48 | OptionParser.new do |opts|
49 | opts.banner = BANNER
50 |
51 | opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o|
52 | options[:output] = o
53 | end
54 |
55 | opts.on_tail("-h", "--help", "Show this message") do
56 | puts opts
57 | exit
58 | end
59 | end
60 | end
61 |
62 | def self.parse(args)
63 | options =
64 | {
65 | output: STDOUT,
66 | }
67 |
68 | self.parser(options).parse!(args)
69 |
70 | options
71 | end
72 | end
73 |
74 | begin
75 | @options = OptParser.parse(ARGV)
76 |
77 | target = (ARGV.empty?) ? STDIN : ARGV.shift
78 | params =
79 | {
80 | verbosity: Parser::VERBOSE_QUIET,
81 | }
82 |
83 | PDF.read(target, params).save(@options[:output], intent: 'PDF/A', noindent: true)
84 |
85 | rescue
86 | abort "#{$!.class}: #{$!.message}"
87 | end
88 |
--------------------------------------------------------------------------------
/bin/pdfdecrypt:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | =begin
4 |
5 | = Info
6 | Decrypts a PDF document.
7 |
8 | = License
9 | Copyright (C) 2016 Guillaume Delugré.
10 |
11 | Origami is free software: you can redistribute it and/or modify
12 | it under the terms of the GNU Lesser General Public License as published by
13 | the Free Software Foundation, either version 3 of the License, or
14 | (at your option) any later version.
15 |
16 | Origami is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU Lesser General Public License for more details.
20 |
21 | You should have received a copy of the GNU Lesser General Public License
22 | along with Origami. If not, see .
23 |
24 | =end
25 |
26 | begin
27 | require 'origami'
28 | rescue LoadError
29 | $: << File.join(__dir__, '../lib')
30 | require 'origami'
31 | end
32 | include Origami
33 |
34 | require 'optparse'
35 |
36 | class OptParser
37 | BANNER = <] [-p ] [-o ]
39 | Decrypts a PDF document. Supports RC4 40 to 128 bits, AES128, AES256.
40 | Bug reports or feature requests at: http://github.com/gdelugre/origami
41 |
42 | Options:
43 | USAGE
44 |
45 | def self.parser(options)
46 | OptionParser.new do |opts|
47 | opts.banner = BANNER
48 |
49 | opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o|
50 | options[:output] = o
51 | end
52 |
53 | opts.on("-p", "--password PASSWORD", "Password of the document") do |p|
54 | options[:password] = p
55 | end
56 |
57 | opts.on_tail("-h", "--help", "Show this message") do
58 | puts opts
59 | exit
60 | end
61 | end
62 | end
63 |
64 | def self.parse(args)
65 | options =
66 | {
67 | output: STDOUT,
68 | password: ''
69 | }
70 |
71 | self.parser(options).parse!(args)
72 |
73 | options
74 | end
75 | end
76 |
77 | begin
78 | @options = OptParser.parse(ARGV)
79 |
80 | target = (ARGV.empty?) ? STDIN : ARGV.shift
81 | params =
82 | {
83 | verbosity: Parser::VERBOSE_QUIET,
84 | password: @options[:password]
85 | }
86 |
87 | PDF.read(target, params).save(@options[:output], decrypt: true, noindent: true)
88 |
89 | rescue
90 | abort "#{$!.class}: #{$!.message}"
91 | end
92 |
--------------------------------------------------------------------------------
/lib/origami/header.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | class PDF
24 |
25 | class InvalidHeaderError < Error #:nodoc:
26 | end
27 |
28 | #
29 | # Class representing a PDF Header.
30 | #
31 | class Header
32 | MAGIC = /%PDF-(?\d+)\.(?\d+)/
33 |
34 | attr_accessor :major_version, :minor_version
35 |
36 | #
37 | # Creates a file header, with the given major and minor versions.
38 | # _major_version_:: Major PDF version, must be 1.
39 | # _minor_version_:: Minor PDF version, must be between 0 and 7.
40 | #
41 | def initialize(major_version = 1, minor_version = 4)
42 | @major_version, @minor_version = major_version, minor_version
43 | end
44 |
45 | def self.parse(stream) #:nodoc:
46 | scanner = Parser.init_scanner(stream)
47 |
48 | unless scanner.scan(MAGIC).nil?
49 | maj = scanner['major'].to_i
50 | min = scanner['minor'].to_i
51 | else
52 | raise InvalidHeaderError, "Invalid header format : #{scanner.peek(15).inspect}"
53 | end
54 |
55 | scanner.skip(REGEXP_WHITESPACES)
56 |
57 | PDF::Header.new(maj, min)
58 | end
59 |
60 | #
61 | # Returns the Header version as a String.
62 | #
63 | def version
64 | "#{@major_version}.#{@minor_version}"
65 | end
66 |
67 | #
68 | # Outputs self into PDF code.
69 | #
70 | def to_s(eol: $/)
71 | "%PDF-#{self.version}".b + eol
72 | end
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/lib/origami/boolean.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | class InvalidBooleanObjectError < InvalidObjectError #:nodoc:
24 | end
25 |
26 | #
27 | # Class representing a Boolean Object.
28 | # A Boolean Object can be *true* or *false*.
29 | #
30 | class Boolean
31 | include Origami::Object
32 |
33 | TOKENS = %w{ true false } #:nodoc:
34 | @@regexp = Regexp.new(WHITESPACES + "(?#{Regexp.union(TOKENS)})")
35 |
36 | #
37 | # Creates a new Boolean value.
38 | # _value_:: *true* or *false*.
39 | #
40 | def initialize(value)
41 | unless value.is_a?(TrueClass) or value.is_a?(FalseClass)
42 | raise TypeError, "Expected type TrueClass or FalseClass, received #{value.class}."
43 | end
44 |
45 | super()
46 |
47 | @value = (value == true)
48 | end
49 |
50 | def to_s(eol: $/) #:nodoc:
51 | super(@value.to_s, eol: eol)
52 | end
53 |
54 | def self.parse(stream, _parser = nil) #:nodoc:
55 | scanner = Parser.init_scanner(stream)
56 | offset = scanner.pos
57 |
58 | if scanner.scan(@@regexp).nil?
59 | raise InvalidBooleanObjectError
60 | end
61 |
62 | value = (scanner['value'] == "true")
63 |
64 | bool = Boolean.new(value)
65 | bool.file_offset = offset
66 |
67 | bool
68 | end
69 |
70 | #
71 | # Converts self into a Ruby boolean, that is TrueClass or FalseClass instance.
72 | #
73 | def value
74 | @value
75 | end
76 |
77 | def false?
78 | @value == false
79 | end
80 |
81 | def true?
82 | @value == true
83 | end
84 |
85 | def ==(bool)
86 | @value == bool
87 | end
88 | end
89 |
90 | end
91 |
--------------------------------------------------------------------------------
/bin/pdfdecompress:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | =begin
4 |
5 | = Info
6 | Uncompresses all binary streams of a PDF document.
7 |
8 | = License
9 | Copyright (C) 2016 Guillaume Delugré.
10 |
11 | Origami is free software: you can redistribute it and/or modify
12 | it under the terms of the GNU Lesser General Public License as published by
13 | the Free Software Foundation, either version 3 of the License, or
14 | (at your option) any later version.
15 |
16 | Origami is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU Lesser General Public License for more details.
20 |
21 | You should have received a copy of the GNU Lesser General Public License
22 | along with Origami. If not, see .
23 |
24 | =end
25 |
26 | begin
27 | require 'origami'
28 | rescue LoadError
29 | $: << File.join(__dir__, '../lib')
30 | require 'origami'
31 | end
32 | include Origami
33 |
34 | require 'optparse'
35 |
36 | class OptParser
37 | BANNER = <] [-p ] [-o ]
39 | Uncompresses all binary streams of a PDF document.
40 | Bug reports or feature requests at: http://github.com/gdelugre/origami
41 |
42 | Options:
43 | USAGE
44 |
45 | def self.parser(options)
46 | OptionParser.new do |opts|
47 | opts.banner = BANNER
48 |
49 | opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o|
50 | options[:output] = o
51 | end
52 |
53 | opts.on_tail("-h", "--help", "Show this message") do
54 | puts opts
55 | exit
56 | end
57 | end
58 | end
59 |
60 | def self.parse(args)
61 | options =
62 | {
63 | output: STDOUT,
64 | }
65 |
66 | self.parser(options).parse!(args)
67 |
68 | options
69 | end
70 | end
71 |
72 | begin
73 | @options = OptParser.parse(ARGV)
74 |
75 | target = (ARGV.empty?) ? STDIN : ARGV.shift
76 | params =
77 | {
78 | verbosity: Parser::VERBOSE_QUIET,
79 | }
80 |
81 | pdf = PDF.read(target, params)
82 |
83 | pdf.each_object
84 | .select { |obj| obj.is_a?(Stream) }
85 | .each { |stream|
86 | unless stream.filters.any?{|filter| %i[JPXDecode DCTDecode JBIG2Decode].include?(filter.value) }
87 | stream.encoded_data = stream.data
88 | stream.dictionary.delete(:Filter)
89 | end
90 | }
91 |
92 | pdf.save(@options[:output], noindent: true)
93 |
94 | rescue
95 | STDERR.puts $!.backtrace.join($/)
96 | abort "#{$!.class}: #{$!.message}"
97 | end
98 |
--------------------------------------------------------------------------------
/test/test_pdf_encrypt.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'stringio'
3 |
4 | class TestEncryption < Minitest::Test
5 | def setup
6 | @target = PDF.read(File.join(__dir__, "dataset/calc.pdf"),
7 | ignore_errors: false, verbosity: Parser::VERBOSE_QUIET)
8 | @output = StringIO.new
9 | end
10 |
11 | def test_encrypt_rc4_40b
12 | @output.string = ""
13 | @target.encrypt(cipher: 'rc4', key_size: 40).save(@output)
14 | end
15 |
16 | def test_encrypt_rc4_128b
17 | @output.string = ""
18 | @target.encrypt(cipher: 'rc4').save(@output)
19 | end
20 |
21 | def test_encrypt_aes_128b
22 | @output.string = ""
23 | @target.encrypt(cipher: 'aes').save(@output)
24 | end
25 |
26 | def test_decrypt_rc4_40b
27 | @output.string = ""
28 |
29 | pdf = PDF.new.encrypt(cipher: 'rc4', key_size: 40)
30 | pdf.Catalog[:Test] = "test"
31 | pdf.save(@output)
32 |
33 | refute_equal pdf.Catalog[:Test], "test"
34 |
35 | @output = @output.reopen(@output.string, "r")
36 | pdf = PDF.read(@output, ignore_errors: false, verbosity: Parser::VERBOSE_QUIET)
37 |
38 | assert_equal pdf.Catalog[:Test], "test"
39 | end
40 |
41 | def test_decrypt_rc4_128b
42 | @output.string = ""
43 | pdf = PDF.new.encrypt(cipher: 'rc4')
44 | pdf.Catalog[:Test] = "test"
45 | pdf.save(@output)
46 |
47 | refute_equal pdf.Catalog[:Test], "test"
48 |
49 | @output.reopen(@output.string, "r")
50 | pdf = PDF.read(@output, ignore_errors: false, verbosity: Parser::VERBOSE_QUIET)
51 |
52 | assert_equal pdf.Catalog[:Test], "test"
53 | end
54 |
55 | def test_decrypt_aes_128b
56 | @output.string = ""
57 | pdf = PDF.new.encrypt(cipher: 'aes')
58 | pdf.Catalog[:Test] = "test"
59 | pdf.save(@output)
60 |
61 | refute_equal pdf.Catalog[:Test], "test"
62 |
63 | @output = @output.reopen(@output.string, "r")
64 | pdf = PDF.read(@output, ignore_errors: false, verbosity: Parser::VERBOSE_QUIET)
65 |
66 | assert_equal pdf.Catalog[:Test], "test"
67 | end
68 |
69 | def test_decrypt_aes_256b
70 | @output.string = ""
71 | pdf = PDF.new.encrypt(cipher: 'aes', key_size: 256)
72 | pdf.Catalog[:Test] = "test"
73 | pdf.save(@output)
74 |
75 | refute_equal pdf.Catalog[:Test], "test"
76 |
77 | @output = @output.reopen(@output.string, "r")
78 | pdf = PDF.read(@output, ignore_errors: false, verbosity: Parser::VERBOSE_QUIET)
79 |
80 | assert_equal pdf.Catalog[:Test], "test"
81 | end
82 |
83 | def test_crypt_filter
84 | @output.string = ""
85 | pdf = PDF.new.encrypt(cipher: 'aes', key_size: 128)
86 |
87 | pdf.Catalog[:S1] = Stream.new("test", :Filter => :Crypt)
88 | pdf.Catalog[:S2] = Stream.new("test")
89 |
90 | pdf.save(@output)
91 |
92 | assert_equal pdf.Catalog.S1.encoded_data, "test"
93 | refute_equal pdf.Catalog.S2.encoded_data, "test"
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/test/test_native_types.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'strscan'
3 |
4 | class TestPDFCreate < Minitest::Test
5 | using Origami::TypeConversion
6 |
7 | def test_type_string
8 | assert_kind_of Origami::String, "".to_o
9 | assert_kind_of ::String, "".to_o.value
10 |
11 | assert_equal "<616263>", HexaString.new("abc").to_s
12 | assert_equal "(test)", LiteralString.new("test").to_s
13 | assert_equal '(\(\(\(\)\)\)\))', LiteralString.new("((())))").to_s
14 | assert_equal "abc", "abc".to_s.to_o
15 | end
16 |
17 | def test_string_encoding
18 | str = LiteralString.new("test")
19 |
20 | assert_equal Origami::String::Encoding::PDFDocEncoding, str.encoding
21 | assert_equal "UTF-8", str.to_utf8.encoding.to_s
22 |
23 | assert_equal "\xFE\xFF\x00t\x00e\x00s\x00t".b, str.to_utf16be
24 | assert_equal Origami::String::Encoding::UTF16BE, str.to_utf16be.to_o.encoding
25 | assert_equal str, str.to_utf16be.to_o.to_pdfdoc
26 | end
27 |
28 | def test_type_null
29 | assert_instance_of Null, nil.to_o
30 | assert_nil Null.new.value
31 | assert_equal Null.new.to_s, "null"
32 | end
33 |
34 | def test_type_name
35 | assert_instance_of Name, :test.to_o
36 | assert_instance_of Symbol, :test.to_o.value
37 |
38 | assert_equal "/test", Name.new(:test).to_s
39 | assert_equal "/#20#23#09#0d#0a#00#5b#5d#3c#3e#28#29#25#2f", Name.new(" #\t\r\n\0[]<>()%/").to_s
40 | assert_equal " #\t\r\n\0[]<>()%/", Name.new(" #\t\r\n\0[]<>()%/").value.to_s
41 | end
42 |
43 | def test_type_boolean
44 | assert_instance_of Boolean, true.to_o
45 | assert_instance_of Boolean, false.to_o
46 | assert Boolean.new(true).value
47 | refute Boolean.new(false).value
48 | assert_equal "true", true.to_o.to_s
49 | assert_equal "false", false.to_o.to_s
50 | end
51 |
52 | def test_type_numeric
53 | assert_instance_of Origami::Real, Math::PI.to_o
54 | assert_instance_of Origami::Integer, 1.to_o
55 |
56 | assert_equal "1.8", Origami::Real.new(1.8).to_s
57 | assert_equal "100", Origami::Integer.new(100).to_s
58 | assert_equal 1.8, 1.8.to_o.value
59 | assert_equal 100, 100.to_o.value
60 | end
61 |
62 | def test_type_array
63 | array = [1, "a", [], {}, :test, nil, true, 3.14]
64 |
65 | assert_instance_of Origami::Array, [].to_o
66 | assert_instance_of ::Array, [].to_o.value
67 |
68 | assert_equal array, array.to_o.value
69 | assert_equal "[1 (a) [] <<>> /test null true 3.14]", array.to_o.to_s
70 | assert array.to_o.all? {|o| o.is_a? Origami::Object}
71 | end
72 |
73 | def test_type_dictionary
74 | assert_instance_of Origami::Dictionary, {}.to_o
75 | assert_instance_of Hash, {}.to_o.value
76 |
77 | dict = {a: 1, b: false, c: nil, d: "abc", e: :abc, f: []}
78 |
79 | assert_equal "<>", dict.to_o.to_s(indent: 0)
80 | assert_equal dict, dict.to_o.value
81 | assert dict.to_o.all?{|k,v| k.is_a?(Name) and v.is_a?(Origami::Object)}
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/origami/outputintents.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | class OutputIntent < Dictionary
24 | include StandardObject
25 |
26 | module Intent
27 | PDFX = :GTS_PDFX
28 | PDFA1 = :GTS_PDFA1
29 | PDFE1 = :GTS_PDFE1
30 | end
31 |
32 | field :Type, :Type => Name, :Default => :OutputIntent
33 | field :S, :Type => Name, :Version => '1.4', :Required => true
34 | field :OutputCondition, :Type => String
35 | field :OutputConditionIdentifier, :Type => String
36 | field :RegistryName, :Type => String
37 | field :Info, :Type => String
38 | field :DestOutputProfile, :Type => Stream
39 | end
40 |
41 | class PDF
42 | def pdfa1?
43 | self.Catalog.OutputIntents.is_a?(Array) and
44 | self.Catalog.OutputIntents.any?{|intent|
45 | intent.solve.S == OutputIntent::Intent::PDFA1
46 | } and
47 | self.metadata? and (
48 | doc = REXML::Document.new self.Catalog.Metadata.data;
49 | REXML::XPath.match(doc, "*/*/rdf:Description[@xmlns:pdfaid]").any? {|desc|
50 | desc.elements["pdfaid:conformance"].text == "A" and
51 | desc.elements["pdfaid:part"].text == "1"
52 | }
53 | )
54 | end
55 |
56 | private
57 |
58 | def intents_as_pdfa1
59 | return if self.pdfa1?
60 |
61 | self.Catalog.OutputIntents ||= []
62 | self.Catalog.OutputIntents << self.insert(
63 | OutputIntent.new(
64 | :Type => :OutputIntent,
65 | :S => OutputIntent::Intent::PDFA1,
66 | :OutputConditionIdentifier => "RGB"
67 | )
68 | )
69 |
70 | metadata = self.create_metadata
71 | doc = REXML::Document.new(metadata.data)
72 |
73 | desc = REXML::Element.new 'rdf:Description'
74 | desc.add_attribute 'rdf:about', ''
75 | desc.add_attribute 'xmlns:pdfaid', 'http://www.aiim.org/pdfa/ns/id/'
76 | desc.add REXML::Element.new('pdfaid:conformance').add_text('A')
77 | desc.add REXML::Element.new('pdfaid:part').add_text('1')
78 | doc.elements["*/rdf:RDF"].add desc
79 |
80 | xml = ""; doc.write(xml, 3)
81 | metadata.data = xml
82 | end
83 | end
84 |
85 | end
86 |
--------------------------------------------------------------------------------
/lib/origami/functions.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 |
22 | module Origami
23 |
24 | module Function
25 |
26 | module Type
27 | SAMPLED = 0
28 | EXPONENTIAL = 2
29 | STITCHING = 3
30 | POSTSCRIPT = 4
31 | end
32 |
33 | def self.included(receiver)
34 | receiver.field :FunctionType, :Type => Integer, :Required => true
35 | receiver.field :Domain, :Type => Array.of(Number), :Required => true
36 | receiver.field :Range, :Type => Array.of(Number)
37 | end
38 |
39 | class Sampled < Stream
40 | include Function
41 |
42 | field :FunctionType, :Type => Integer, :Default => Type::SAMPLED, :Version => "1.3", :Required => true
43 | field :Range, :Type => Array.of(Number), :Required => true
44 | field :Size, :Type => Array.of(Integer), :Required => true
45 | field :BitsPerSample, :Type => Integer, :Required => true
46 | field :Order, :Type => Integer, :Default => 1
47 | field :Encode, :Type => Array.of(Number)
48 | field :Decode, :Type => Array.of(Number)
49 | end
50 |
51 | class Exponential < Dictionary
52 | include StandardObject
53 | include Function
54 |
55 | field :FunctionType, :Type => Integer, :Default => Type::EXPONENTIAL, :Version => "1.3", :Required => true
56 | field :C0, :Type => Array.of(Number), :Default => [ 0.0 ]
57 | field :C1, :Type => Array.of(Number), :Default => [ 1.0 ]
58 | field :N, :Type => Number, :Required => true
59 | end
60 |
61 | class Stitching < Dictionary
62 | include StandardObject
63 | include Function
64 |
65 | field :FunctionType, :Type => Integer, :Default => Type::STITCHING, :Version => "1.3", :Required => true
66 | field :Functions, :Type => Array, :Required => true
67 | field :Bounds, :Type => Array.of(Number), :Required => true
68 | field :Encode, :Type => Array.of(Number), :Required => true
69 | end
70 |
71 | class PostScript < Stream
72 | include Function
73 |
74 | field :FunctionType, :Type => Integer, :Default => Type::POSTSCRIPT, :Version => "1.3", :Required => true
75 | field :Range, :Type => Array.of(Number), :Required => true
76 | end
77 | end
78 |
79 | end
80 |
--------------------------------------------------------------------------------
/test/test_pdf_sign.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'stringio'
3 | require 'openssl'
4 |
5 | class TestSign < Minitest::Test
6 |
7 | def create_self_signed_ca_certificate(key_size, expires)
8 | key = OpenSSL::PKey::RSA.new key_size
9 |
10 | name = OpenSSL::X509::Name.parse 'CN=origami/DC=example'
11 |
12 | cert = OpenSSL::X509::Certificate.new
13 | cert.version = 2
14 | cert.serial = 0
15 | cert.not_before = Time.now
16 | cert.not_after = Time.now + expires
17 |
18 | cert.public_key = key.public_key
19 | cert.subject = name
20 |
21 | extension_factory = OpenSSL::X509::ExtensionFactory.new
22 | extension_factory.issuer_certificate = cert
23 | extension_factory.subject_certificate = cert
24 |
25 | cert.add_extension extension_factory.create_extension('basicConstraints', 'CA:TRUE', true)
26 | cert.add_extension extension_factory.create_extension('keyUsage', 'digitalSignature,keyCertSign')
27 | cert.add_extension extension_factory.create_extension('subjectKeyIdentifier', 'hash')
28 |
29 | cert.issuer = name
30 | cert.sign key, OpenSSL::Digest::SHA256.new
31 |
32 | [ cert, key ]
33 | end
34 |
35 | def setup
36 | @cert, @key = create_self_signed_ca_certificate(1024, 3600)
37 | @other_cert, @other_key = create_self_signed_ca_certificate(1024, 3600)
38 | end
39 |
40 | def setup_document_with_annotation
41 | document = PDF.read(File.join(__dir__, "dataset/calc.pdf"),
42 | ignore_errors: false, verbosity: Parser::VERBOSE_QUIET)
43 |
44 | annotation = Annotation::Widget::Signature.new.set_indirect(true)
45 | annotation.Rect = Rectangle[llx: 89.0, lly: 386.0, urx: 190.0, ury: 353.0]
46 |
47 | document.append_page do |page|
48 | page.add_annotation(annotation)
49 | end
50 |
51 | [ document, annotation ]
52 | end
53 |
54 | def sign_document_with_method(method)
55 | document, annotation = setup_document_with_annotation
56 |
57 | document.sign(@cert, @key,
58 | method: method,
59 | annotation: annotation,
60 | issuer: "Guillaume Delugré",
61 | location: "France",
62 | contact: "origami@localhost",
63 | reason: "Example"
64 | )
65 |
66 | assert document.frozen?
67 | assert document.signed?
68 |
69 | output = StringIO.new
70 | document.save(output)
71 |
72 | document = PDF.read(output.reopen(output.string,'r'), verbosity: Parser::VERBOSE_QUIET)
73 |
74 | refute document.verify
75 | assert document.verify(allow_self_signed: true)
76 | assert document.verify(trusted_certs: [@cert])
77 | refute document.verify(trusted_certs: [@other_cert])
78 |
79 | result = document.verify do |ctx|
80 | ctx.error == OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT and ctx.current_cert.to_pem == @cert.to_pem
81 | end
82 |
83 | assert result
84 | end
85 |
86 | def test_sign_pkcs7_sha1
87 | sign_document_with_method(Signature::PKCS7_SHA1)
88 | end
89 |
90 | def test_sign_pkcs7_detached
91 | sign_document_with_method(Signature::PKCS7_DETACHED)
92 | end
93 |
94 | def test_sign_x509_sha1
95 | sign_document_with_method(Signature::PKCS1_RSA_SHA1)
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/bin/pdfencrypt:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | =begin
4 |
5 | = Info
6 | Encrypts a PDF document.
7 |
8 | = License
9 | Copyright (C) 2016 Guillaume Delugré.
10 |
11 | Origami is free software: you can redistribute it and/or modify
12 | it under the terms of the GNU Lesser General Public License as published by
13 | the Free Software Foundation, either version 3 of the License, or
14 | (at your option) any later version.
15 |
16 | Origami is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU Lesser General Public License for more details.
20 |
21 | You should have received a copy of the GNU Lesser General Public License
22 | along with Origami. If not, see .
23 |
24 | =end
25 |
26 | begin
27 | require 'origami'
28 | rescue LoadError
29 | $: << File.join(__dir__, '../lib')
30 | require 'origami'
31 | end
32 | include Origami
33 |
34 | require 'optparse'
35 |
36 | class OptParser
37 | BANNER = <] [-p ] [-c ] [-s ] [--hardened] [-o ]
39 | Encrypts a PDF document. Supports RC4 40 to 128 bits, AES128, AES256.
40 | Bug reports or feature requests at: http://github.com/gdelugre/origami
41 |
42 | Options:
43 | USAGE
44 |
45 | def self.parser(options)
46 | OptionParser.new do |opts|
47 | opts.banner = BANNER
48 |
49 | opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o|
50 | options[:output] = o
51 | end
52 |
53 | opts.on("-p", "--password PASSWORD", "Password of the document") do |p|
54 | options[:password] = p
55 | end
56 |
57 | opts.on("-c", "--cipher CIPHER", "Cipher used to encrypt the document (Default: AES)") do |c|
58 | options[:cipher] = c
59 | end
60 |
61 | opts.on("-s", "--key-size KEYSIZE", "Key size in bits (Default: 128)") do |s|
62 | options[:key_size] = s.to_i
63 | end
64 |
65 | opts.on("--hardened", "Use stronger key validation scheme (only AES-256)") do
66 | options[:hardened] = true
67 | end
68 |
69 | opts.on_tail("-h", "--help", "Show this message") do
70 | puts opts
71 | exit
72 | end
73 | end
74 | end
75 |
76 | def self.parse(args)
77 | options =
78 | {
79 | output: STDOUT,
80 | password: '',
81 | cipher: 'aes',
82 | key_size: 128,
83 | hardened: false
84 | }
85 |
86 | self.parser(options).parse!(args)
87 |
88 | options
89 | end
90 | end
91 |
92 | begin
93 | @options = OptParser.parse(ARGV)
94 |
95 | target = (ARGV.empty?) ? STDIN : ARGV.shift
96 | params =
97 | {
98 | verbosity: Parser::VERBOSE_QUIET,
99 | }
100 |
101 | pdf = PDF.read(target, params)
102 | pdf.encrypt(
103 | user_passwd: @options[:password],
104 | owner_passwd: @options[:password],
105 | cipher: @options[:cipher],
106 | key_size: @options[:key_size],
107 | hardened: @options[:hardened]
108 | )
109 | pdf.save(@options[:output], noindent: true)
110 |
111 | rescue
112 | abort "#{$!.class}: #{$!.message}"
113 | end
114 |
--------------------------------------------------------------------------------
/lib/origami/graphics/instruction.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | class InvalidPDFInstructionError < Error; end
24 |
25 | class PDF::Instruction
26 | using TypeConversion
27 |
28 | attr_reader :operator
29 | attr_accessor :operands
30 |
31 | @insns = Hash.new(operands: [], render: lambda{})
32 |
33 | def initialize(operator, *operands)
34 | @operator = operator
35 | @operands = operands.map!{|arg| arg.is_a?(Origami::Object) ? arg.value : arg}
36 |
37 | if self.class.has_op?(operator)
38 | opdef = self.class.get_operands(operator)
39 |
40 | if not opdef.include?('*') and opdef.size != operands.size
41 | raise InvalidPDFInstructionError,
42 | "Numbers of operands mismatch for #{operator}: #{operands.inspect}"
43 | end
44 | end
45 | end
46 |
47 | def render(canvas)
48 | self.class.get_render_proc(@operator)[canvas, *@operands]
49 |
50 | self
51 | end
52 |
53 | def to_s
54 | "#{operands.map{|op| op.to_o.to_s}.join(' ')}#{' ' unless operands.empty?}#{operator}\n"
55 | end
56 |
57 | class << self
58 | def insn(operator, *operands, &render_proc)
59 | @insns[operator] = {}
60 | @insns[operator][:operands] = operands
61 | @insns[operator][:render] = render_proc || lambda{}
62 | end
63 |
64 | def has_op?(operator)
65 | @insns.has_key? operator
66 | end
67 |
68 | def get_render_proc(operator)
69 | @insns[operator][:render]
70 | end
71 |
72 | def get_operands(operator)
73 | @insns[operator][:operands]
74 | end
75 |
76 | def parse(stream)
77 | operands = []
78 | while type = Object.typeof(stream, true)
79 | operands.push type.parse(stream)
80 | end
81 |
82 | if not stream.eos?
83 | if stream.scan(/(?[[:graph:]&&[^\[\]<>()%\/]]+)/).nil?
84 | raise InvalidPDFInstructionError, "Operator: #{(stream.peek(10) + '...').inspect}"
85 | end
86 |
87 | operator = stream['operator']
88 | PDF::Instruction.new(operator, *operands)
89 | else
90 | unless operands.empty?
91 | raise InvalidPDFInstructionError, "No operator given for operands: #{operands.map(&:to_s).join(' ')}"
92 | end
93 | end
94 | end
95 | end
96 |
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/origami/webcapture.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module WebCapture
24 |
25 | class CommandSettings < Dictionary
26 | include StandardObject
27 |
28 | field :G, :Type => Dictionary
29 | field :C, :Type => Dictionary
30 | end
31 |
32 | class Command < Dictionary
33 | include StandardObject
34 |
35 | module Flags
36 | SAMESITE = 1 << 1
37 | SAMEPATH = 1 << 2
38 | SUBMIT = 1 << 3
39 | end
40 |
41 | field :URL, :Type => String, :Required => true
42 | field :L, :Type => Integer, :Default => 1
43 | field :F, :Type => Integer, :Default => 0
44 | field :P, :Type => [ String, Stream ]
45 | field :CT, :Type => String, :Default => "application/x-www-form-urlencoded"
46 | field :H, :Type => String
47 | field :S, :Type => CommandSettings
48 | end
49 |
50 | class SourceInformation < Dictionary
51 | include StandardObject
52 |
53 | module SubmissionType
54 | NOFORM = 0
55 | GETFORM = 1
56 | POSTFORM = 2
57 | end
58 |
59 | field :AU, :Type => [ String, Dictionary ], :Required => true
60 | field :TS, :Type => String
61 | field :E, :Type => String
62 | field :S, :Type => Integer, :Default => 0
63 | field :C, :Type => Command
64 | end
65 |
66 | class SpiderInfo < Dictionary
67 | include StandardObject
68 |
69 | field :V, :Type => Real, :Default => 1.0, :Version => "1.3", :Required => true
70 | field :C, :Type => Array.of(Command)
71 | end
72 |
73 | class ContentSet < Dictionary
74 | include StandardObject
75 |
76 | PAGE_SET = :SPS
77 | IMAGE_SET = :SIS
78 |
79 | field :Type, :Type => Name, :Default => :SpiderContentSet
80 | field :S, :Type => Name, :Required => true
81 | field :ID, :Type => String, :Required => true
82 | field :O, :Type => Array, :Required => true
83 | field :SI, :Type => [ SourceInformation, Array.of(SourceInformation) ], :Required => true
84 | field :CT, :Type => String
85 | field :TS, :Type => String
86 | end
87 |
88 | class PageContentSet < ContentSet
89 | field :S, :Type => Name, :Default => ContentSet::PAGE_SET, :Required => true
90 | field :T, :Type => String
91 | field :TID, :Type => String
92 | end
93 |
94 | class ImageContentSet < ContentSet
95 | field :S, :Type => Name, :Default => ContentSet::IMAGE_SET, :Required => true
96 | field :R, :Type => [ Integer, Array.of(Integer) ], :Required => true
97 | end
98 | end
99 |
100 | end
101 |
--------------------------------------------------------------------------------
/examples/forms/xfa.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/ruby
2 |
3 | begin
4 | require 'origami'
5 | rescue LoadError
6 | $: << File.join(__dir__, "../../lib")
7 | require 'origami'
8 | end
9 | include Origami
10 |
11 | require 'origami/template/widgets'
12 |
13 | OUTPUT_FILE = "#{File.basename(__FILE__, ".rb")}.pdf"
14 |
15 | #
16 | # Interactive FormCalc interpreter using a XFA form.
17 | #
18 |
19 | #
20 | # XDP Packet holding the Form.
21 | #
22 | class SampleXDP < XDP::Package
23 | def initialize(script = "")
24 | super()
25 |
26 | self.root.add_element(create_config_packet)
27 | self.root.add_element(create_template_packet(script))
28 | self.root.add_element(create_datasets_packet)
29 | end
30 |
31 | def create_config_packet
32 | config = XDP::Packet::Config.new
33 |
34 | present = config.add_element(XFA::Element.new("present"))
35 | pdf = present.add_element(XFA::Element.new("pdf"))
36 | interactive = pdf.add_element(XFA::Element.new("interactive"))
37 | interactive.text = 1
38 |
39 | config
40 | end
41 |
42 | def create_template_packet(script)
43 | template = XDP::Packet::Template.new
44 |
45 | form1 = template.add_subform(layout: 'tb', name: 'form1')
46 | form1.add_pageSet
47 | form1.add_event(activity: 'initialize', name: 'event__ready')
48 | .add_script(contentType: 'application/x-formcalc')
49 | .text = script
50 |
51 | subform = form1.add_subform
52 |
53 | button = subform.add_field(name: 'Button1')
54 | button.add_ui.add_button(highlight: 'inverted')
55 | btncaption = button.add_caption
56 | btncaption.add_value.add_text.text = "Send!"
57 | btncaption.add_para(vAlign: 'middle', hAlign: 'center')
58 | button.add_bind(match: 'none')
59 | button.add_event(activity: 'click', name: 'event__click')
60 | .add_script(contentType: 'application/x-formcalc')
61 | .text = script
62 |
63 | txtfield = subform.add_field(name: 'TextField1')
64 | txtfield.add_ui.add_textEdit.add_border.add_edge(stroke: 'lowered')
65 |
66 | template
67 | end
68 |
69 | def create_datasets_packet
70 | datasets = XDP::Packet::Datasets.new
71 | data = datasets.add_element(XDP::Packet::Datasets::Data.new)
72 |
73 | data.add_element(XFA::Element.new('form1'))
74 | .add_element(XFA::Element.new('TextField1'))
75 | .text = '$host.messageBox("Hello from FormCalc!")'
76 |
77 | datasets
78 | end
79 | end
80 |
81 | pdf = PDF.new.append_page(page = Page.new)
82 |
83 | contents = ContentStream.new.setFilter(:FlateDecode)
84 |
85 | contents.write "Write your FormCalc below and run it",
86 | x: 100, y: 750, size: 24, rendering: Text::Rendering::FILL,
87 | fill_color: Graphics::Color::RGB.new(0xFF, 0x80, 0x80)
88 |
89 | contents.write "You need at least Acrobat Reader 8 to use this document.",
90 | x: 50, y: 80, size: 12, rendering: Text::Rendering::FILL
91 |
92 | contents.write "\nGenerated with Origami #{Origami::VERSION}.",
93 | color: Graphics::Color::RGB.new(0, 0, 255)
94 |
95 | contents.draw_rectangle(45, 35, 320, 60,
96 | line_width: 2.0, dash: Graphics::DashPattern.new([3]),
97 | fill: false, stroke: true, stroke_color: Graphics::Color::GrayScale.new(0.7))
98 |
99 | page.Contents = contents
100 |
101 | ml = Template::MultiLineEdit.new('TextField1[0]', x: 50, y: 280, width: 500, height: 400)
102 | button = Template::Button.new('Send!', id: 'Button1[0]', x: 490, y: 240, width: 60, height: 30)
103 |
104 | page.add_annotation(ml, button)
105 |
106 | form1 = Field::Subform.new(T: "form1[0]")
107 | form1.add_fields(subform = Field::Subform.new(T: "#subform[0]"))
108 | subform.add_fields(ml, button)
109 |
110 | xdp = SampleXDP.new('Eval(Ref(form1[0].#subform[0].TextField1[0]))').to_s
111 | pdf.create_xfa_form(xdp, form1)
112 |
113 | pdf.save(OUTPUT_FILE)
114 |
115 | puts "PDF file saved as #{OUTPUT_FILE}."
116 |
--------------------------------------------------------------------------------
/bin/shell/console.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'tempfile'
22 | require 'hexdump'
23 | require 'colorize'
24 |
25 | String.disable_colorization(false)
26 |
27 | module Origami
28 | module Object
29 | def inspect
30 | to_s
31 | end
32 | end
33 |
34 | class Stream
35 | def edit(editor = ENV['EDITOR'])
36 | Tempfile.open("origami") do |tmpfile|
37 | tmpfile.write(self.data)
38 | tmpfile.flush
39 |
40 | Process.wait Kernel.spawn "#{editor} #{tmpfile.path}"
41 |
42 | self.data = File.read(tmpfile.path)
43 | tmpfile.unlink
44 | end
45 |
46 | true
47 | end
48 |
49 | def inspect
50 | self.data.hexdump
51 | end
52 | end
53 |
54 | class Page < Dictionary
55 | def edit
56 | each_content_stream do |stream|
57 | stream.edit
58 | end
59 | end
60 | end
61 |
62 | class PDF
63 | if defined?(PDF::JavaScript::Engine)
64 | class JavaScript::Engine
65 | def shell
66 | loop do
67 | print "js > ".magenta
68 | break if (line = gets).nil?
69 |
70 | begin
71 | puts exec(line)
72 | rescue V8::JSError => e
73 | puts "Error: #{e.message}"
74 | end
75 | end
76 | end
77 | end
78 | end
79 |
80 | class Revision
81 | def to_s
82 | puts "---------- Body ----------".white.bold
83 | @body.each_value do |obj|
84 | print "#{obj.reference.to_s.rjust(8,' ')}".ljust(10).magenta
85 | puts "#{obj.type}".yellow
86 | end
87 |
88 | puts "---------- Trailer ---------".white.bold
89 | if not @trailer.dictionary
90 | puts " [x] No trailer found.".blue
91 | else
92 | @trailer.dictionary.each_pair do |entry, value|
93 | print " [*] ".magenta
94 | print "#{entry}: ".yellow
95 | puts "#{value}".red
96 | end
97 |
98 | print " [+] ".magenta
99 | print "startxref: ".yellow
100 | puts "#{@trailer.startxref}".red
101 | end
102 | end
103 |
104 | def inspect
105 | to_s
106 | end
107 | end
108 |
109 | def to_s
110 | puts
111 |
112 | puts "---------- Header ----------".white.bold
113 | print " [+] ".magenta
114 | print "Version: ".yellow
115 | puts "#{@header.major_version}.#{@header.minor_version}".red
116 |
117 | @revisions.each do |revision|
118 | revision.to_s
119 | end
120 | puts
121 | end
122 |
123 | def inspect
124 | to_s
125 | end
126 | end
127 |
128 | end
129 |
--------------------------------------------------------------------------------
/lib/origami/reference.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | class InvalidReferenceError < Error #:nodoc:
24 | end
25 |
26 | #
27 | # Class representing a Reference Object.
28 | # Reference are like symbolic links pointing to a particular object into the file.
29 | #
30 | class Reference
31 | include Origami::Object
32 | include Comparable
33 |
34 | TOKENS = [ "(?\\d+)" + WHITESPACES + "(?\\d+)" + WHITESPACES + "R" ] #:nodoc:
35 | REGEXP_TOKEN = Regexp.new(TOKENS.first, Regexp::MULTILINE)
36 | @@regexp = Regexp.new(WHITESPACES + TOKENS.first + WHITESPACES)
37 |
38 | attr_accessor :refno, :refgen
39 |
40 | def initialize(refno, refgen)
41 | super()
42 |
43 | @refno, @refgen = refno, refgen
44 | end
45 |
46 | def self.parse(stream, _parser = nil) #:nodoc:
47 | scanner = Parser.init_scanner(stream)
48 | offset = scanner.pos
49 |
50 | if scanner.scan(@@regexp).nil?
51 | raise InvalidReferenceError, "Bad reference to indirect objet format"
52 | end
53 |
54 | no = scanner['no'].to_i
55 | gen = scanner['gen'].to_i
56 |
57 | ref = Reference.new(no, gen)
58 | ref.file_offset = offset
59 |
60 | ref
61 | end
62 |
63 | #
64 | # Returns the object pointed to by the reference.
65 | # The reference must be part of a document.
66 | # Raises an InvalidReferenceError if the object cannot be found.
67 | #
68 | def follow
69 | doc = self.document
70 |
71 | if doc.nil?
72 | raise InvalidReferenceError, "Not attached to any document"
73 | end
74 |
75 | target = doc.get_object(self)
76 |
77 | if target.nil? and not Origami::OPTIONS[:ignore_bad_references]
78 | raise InvalidReferenceError, "Cannot resolve reference : #{self}"
79 | end
80 |
81 | target or Null.new
82 | end
83 | alias solve follow
84 |
85 | #
86 | # Returns true if the reference points to an object.
87 | #
88 | def valid?
89 | begin
90 | self.solve
91 | true
92 | rescue InvalidReferenceError
93 | false
94 | end
95 | end
96 |
97 | def hash #:nodoc:
98 | self.to_a.hash
99 | end
100 |
101 | def <=>(ref) #:nodoc
102 | self.to_a <=> ref.to_a
103 | end
104 |
105 | #
106 | # Compares to Reference object.
107 | #
108 | def ==(ref)
109 | return false unless ref.is_a?(Reference)
110 |
111 | self.to_a == ref.to_a
112 | end
113 | alias eql? ==
114 |
115 | #
116 | # Returns a Ruby array with the object number and the generation this reference is pointing to.
117 | #
118 | def to_a
119 | [@refno, @refgen]
120 | end
121 |
122 | def to_s(eol: $/) #:nodoc:
123 | super("#{@refno} #{@refgen} R", eol: eol)
124 | end
125 |
126 | #
127 | # Returns the referenced object value.
128 | #
129 | def value
130 | self.solve.value
131 | end
132 | end
133 |
134 | end
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Origami
2 | =====
3 | [](https://rubygems.org/gems/origami)
4 | [](https://rubygems.org/gems/origami)
5 | [](https://travis-ci.org/gdelugre/origami)
6 | [](https://www.gnu.org/licenses/lgpl-3.0)
7 |
8 | Overview
9 | --------
10 |
11 | Origami is a framework written in pure Ruby to manipulate PDF files.
12 |
13 | It offers the possibility to parse the PDF contents, modify and save the PDF
14 | structure, as well as creating new documents.
15 |
16 | Origami supports some advanced features of the PDF specification:
17 |
18 | * Compression filters with predictor functions
19 | * Encryption using RC4 or AES, including the undocumented Revision 6 derivation algorithm
20 | * Digital signatures and Usage Rights
21 | * File attachments
22 | * AcroForm and XFA forms
23 | * Object streams
24 |
25 | Origami is able to parse PDF, FDF and PPKLite (Adobe certificate store) files.
26 |
27 | Requirements
28 | ------------
29 |
30 | As of version 2, the minimal version required to run Origami is Ruby 2.1.
31 |
32 | Some optional features require additional gems:
33 |
34 | * [therubyracer][the-ruby-racer] for JavaScript emulation of PDF scripts
35 |
36 | Quick start
37 | -----------
38 |
39 | First install Origami using the latest gem available:
40 |
41 | $ gem install origami
42 |
43 | Then import Origami with:
44 |
45 | ```ruby
46 | require 'origami'
47 | ```
48 |
49 | To process a PDF document, you can use the ``PDF.read`` method:
50 |
51 | ```ruby
52 | pdf = Origami::PDF.read "something.pdf"
53 |
54 | puts "This document has #{pdf.pages.size} page(s)"
55 | ```
56 |
57 | The default behavior is to parse the entire contents of the document at once. This can be changed by passing the ``lazy`` flag to parse objects on demand.
58 |
59 | ```ruby
60 | pdf = Origami::PDF.read "something.pdf", lazy: true
61 |
62 | pdf.each_page do |page|
63 | page.each_font do |name, font|
64 | # ... only parse the necessary bits
65 | end
66 | end
67 | ```
68 |
69 | You can also create documents directly by instanciating a new PDF object:
70 |
71 | ```ruby
72 | pdf = Origami::PDF.new
73 |
74 | pdf.append_page
75 | pdf.pages.first.write "Hello", size: 30
76 |
77 | pdf.save("example.pdf")
78 |
79 | # Another way of doing it
80 | Origami::PDF.write("example.pdf") do |pdf|
81 | pdf.append_page do |page|
82 | page.write "Hello", size: 30
83 | end
84 | end
85 | ```
86 |
87 | Take a look at the [examples](examples) and [bin](bin) directories for some examples of advanced usage.
88 |
89 | Tools
90 | -----
91 |
92 | Origami comes with a set of tools to manipulate PDF documents from the command line.
93 |
94 | * [pdfcop](bin/pdfcop): Runs some heuristic checks to detect dangerous contents.
95 | * [pdfdecompress](bin/pdfdecompress): Strips compression filters out of a document.
96 | * [pdfdecrypt](bin/pdfdecrypt): Removes encrypted contents from a document.
97 | * [pdfencrypt](bin/pdfencrypt): Encrypts a PDF document.
98 | * [pdfexplode](bin/pdfexplode): Explodes a document into several documents, each of them having one deleted resource. Useful for reduction of crash cases after a fuzzing session.
99 | * [pdfextract](bin/pdfextract): Extracts binary resources of a document (images, scripts, fonts, etc.).
100 | * [pdfmetadata](bin/pdfmetadata): Displays the metadata contained in a document.
101 | * [pdf2ruby](bin/pdf2ruby): Converts a PDF into an Origami script rebuilding an equivalent document (experimental).
102 | * [pdfsh](bin/pdfsh): An IRB shell running inside the Origami namespace.
103 |
104 | **Note**: Since version 2.1, [pdfwalker][pdfwalker-gem] has been moved to a [separate repository][pdfwalker-repo].
105 |
106 | License
107 | -------
108 |
109 | Origami is distributed under the [LGPL](COPYING.LESSER) license.
110 |
111 | Copyright © 2019 Guillaume Delugré.
112 |
113 | [the-ruby-racer]: https://rubygems.org/gems/therubyracer
114 | [pdfwalker-gem]: https://rubygems.org/gems/pdfwalker
115 | [pdfwalker-repo]: https://github.com/gdelugre/pdfwalker
116 |
--------------------------------------------------------------------------------
/lib/origami/name.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | REGULARCHARS = "([^ \\t\\r\\n\\0\\[\\]<>()%\\/]|#[a-fA-F0-9][a-fA-F0-9])*" #:nodoc:
24 |
25 | class InvalidNameObjectError < InvalidObjectError #:nodoc:
26 | end
27 |
28 | #
29 | # Class representing a Name Object.
30 | # Name objects are strings which identify some PDF file inner structures.
31 | #
32 | class Name
33 | include Origami::Object
34 | include Comparable
35 |
36 | TOKENS = %w{ / } #:nodoc:
37 |
38 | @@regexp = Regexp.new(WHITESPACES + TOKENS.first + "(?#{REGULARCHARS})" + WHITESPACES) #:nodoc
39 |
40 | #
41 | # Creates a new Name.
42 | # _name_:: A symbol representing the new Name value.
43 | #
44 | def initialize(name = "")
45 | unless name.is_a?(Symbol) or name.is_a?(::String)
46 | raise TypeError, "Expected type Symbol or String, received #{name.class}."
47 | end
48 |
49 | @value = name.to_s
50 |
51 | super()
52 | end
53 |
54 | def value
55 | @value.to_sym
56 | end
57 | alias to_sym value
58 |
59 | def <=>(name)
60 | return unless name.is_a?(Name)
61 |
62 | self.value <=> name.value
63 | end
64 |
65 | def ==(object) #:nodoc:
66 | self.eql?(object) or @value.to_sym == object
67 | end
68 |
69 | def eql?(object) #:nodoc:
70 | object.is_a?(Name) and self.value.eql?(object.value)
71 | end
72 |
73 | def hash #:nodoc:
74 | @value.hash
75 | end
76 |
77 | def to_s(eol: $/) #:nodoc:
78 | super(TOKENS.first + Name.expand(@value), eol: eol)
79 | end
80 |
81 | def self.parse(stream, _parser = nil) #:nodoc:
82 | scanner = Parser.init_scanner(stream)
83 | offset = scanner.pos
84 |
85 | name =
86 | if scanner.scan(@@regexp).nil?
87 | raise InvalidNameObjectError, "Bad name format"
88 | else
89 | value = scanner['name']
90 |
91 | Name.new(value.include?('#') ? contract(value) : value)
92 | end
93 |
94 | name.file_offset = offset
95 |
96 | name
97 | end
98 |
99 | def self.contract(name) #:nodoc:
100 | i = 0
101 | name = name.dup
102 |
103 | while i < name.length
104 | if name[i] == "#"
105 | digits = name[i+1, 2]
106 |
107 | unless digits =~ /^[A-Za-z0-9]{2}$/
108 | raise InvalidNameObjectError, "Irregular use of # token"
109 | end
110 |
111 | char = digits.hex.chr
112 |
113 | if char == "\0"
114 | raise InvalidNameObjectError, "Null byte forbidden inside name definition"
115 | end
116 |
117 | name[i, 3] = char
118 | end
119 |
120 | i = i + 1
121 | end
122 |
123 | name
124 | end
125 |
126 | def self.expand(name) #:nodoc:
127 | forbiddenchars = /[ #\t\r\n\0\[\]<>()%\/]/
128 |
129 | name.gsub(forbiddenchars) do |c|
130 | "#" + c.ord.to_s(16).rjust(2,"0")
131 | end
132 | end
133 | end
134 |
135 | end
136 |
--------------------------------------------------------------------------------
/bin/pdfmetadata:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | =begin
4 |
5 | = Info
6 | Prints out the metadata contained in a PDF document.
7 |
8 | = License
9 | Copyright (C) 2019 Guillaume Delugré.
10 |
11 | Origami is free software: you can redistribute it and/or modify
12 | it under the terms of the GNU Lesser General Public License as published by
13 | the Free Software Foundation, either version 3 of the License, or
14 | (at your option) any later version.
15 |
16 | Origami is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU Lesser General Public License for more details.
20 |
21 | You should have received a copy of the GNU Lesser General Public License
22 | along with Origami. If not, see .
23 |
24 | =end
25 |
26 | begin
27 | require 'origami'
28 | rescue LoadError
29 | $: << File.join(__dir__, '../lib')
30 | require 'origami'
31 | end
32 | include Origami
33 |
34 | require 'colorize'
35 | require 'optparse'
36 | require 'json'
37 |
38 | class OptParser
39 | BANNER = <] [-i] [-x]
41 | Prints out the metadata contained in a PDF document.
42 | Bug reports or feature requests at: http://github.com/gdelugre/origami
43 |
44 | Options:
45 | USAGE
46 |
47 | def self.parser(options)
48 | OptionParser.new do |opts|
49 | opts.banner = BANNER
50 |
51 | opts.on("-i", "--info", "Extracts document info metadata") do
52 | options[:doc_info] = true
53 | end
54 |
55 | opts.on("-x", "--xmp", "Extracts XMP document metadata stream") do
56 | options[:doc_stream] = true
57 | end
58 |
59 | opts.on("-f", "--format [FORMAT]", %i{text json}, "Output format ('text', 'json')") do |format|
60 | options[:output_format] = format
61 | end
62 |
63 | opts.on("-n", "--no-color", "Turn off colorized output.") do
64 | options[:disable_colors] = true
65 | end
66 |
67 | opts.on_tail("-h", "--help", "Show this message") do
68 | puts opts
69 | exit
70 | end
71 | end
72 | end
73 |
74 | def self.parse(args)
75 | options =
76 | {
77 | output_format: :text,
78 | disable_colors: false
79 | }
80 |
81 | self.parser(options).parse!(args)
82 |
83 | options
84 | end
85 | end
86 |
87 | def print_section(name, elements)
88 | puts "[*] #{name}:".magenta
89 |
90 | elements.each_pair do |name, item|
91 | print name.ljust(20, ' ').green
92 | puts ": #{item}"
93 | end
94 | end
95 |
96 | begin
97 | @options = OptParser.parse(ARGV)
98 |
99 | unless @options[:doc_info] or @options[:doc_stream]
100 | @options[:doc_info] = @options[:doc_stream] = true
101 | end
102 |
103 | String.disable_colorization @options[:disable_colors]
104 |
105 | target = (ARGV.empty?) ? STDIN : ARGV.shift
106 | params =
107 | {
108 | verbosity: Parser::VERBOSE_QUIET,
109 | lazy: true
110 | }
111 |
112 | pdf = PDF.read(target, params)
113 | result = {}
114 |
115 | if @options[:doc_info] and pdf.document_info?
116 | result[:document_info] = pdf.document_info.map {|k,v|
117 | key = k.value.to_s
118 | obj = v.solve
119 | str_value = obj.respond_to?(:to_utf8) ? obj.to_utf8 : obj.value.to_s
120 |
121 | [ key, str_value ]
122 | }.to_h
123 | end
124 |
125 | if @options[:doc_stream] and pdf.metadata?
126 | result[:xmp_metadata] = pdf.metadata
127 | end
128 |
129 |
130 | if @options[:output_format] == :text
131 | print_section("Document information dictionary", result[:document_info]) if result.key?(:document_info)
132 |
133 | if result.key?(:xmp_metadata)
134 | puts if result.key?(:document_info)
135 | print_section("Metadata stream", result[:xmp_metadata])
136 | end
137 | elsif @options[:output_format] == :json
138 | puts JSON.pretty_generate(result)
139 | end
140 |
141 | rescue
142 | puts $!.backtrace.join $/
143 | abort "#{$!.class}: #{$!.message}"
144 | end
145 |
--------------------------------------------------------------------------------
/lib/origami/parsers/pdf.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'origami/parser'
22 |
23 | module Origami
24 |
25 | class PDF
26 | class Parser < Origami::Parser
27 | def initialize(params = {})
28 | options =
29 | {
30 | decrypt: true, # Attempt to decrypt to document if encrypted (recommended).
31 | password: '', # Default password being tried when opening a protected document.
32 | prompt_password: lambda do # Callback procedure to prompt password when document is encrypted.
33 | require 'io/console'
34 | STDERR.print "Password: "
35 | STDIN.noecho(&:gets).chomp
36 | end,
37 | force: false # Force PDF header detection
38 | }.update(params)
39 |
40 | super(options)
41 | end
42 |
43 | private
44 |
45 | def parse_initialize #:nodoc:
46 | if @options[:force] == true
47 | @data.skip_until(/%PDF-/).nil?
48 | @data.pos = @data.pos - 5
49 | end
50 |
51 | pdf = PDF.new(self)
52 |
53 | info "...Reading header..."
54 | begin
55 | pdf.header = PDF::Header.parse(@data)
56 | @options[:callback].call(pdf.header)
57 | rescue InvalidHeaderError
58 | raise unless @options[:ignore_errors]
59 | warn "PDF header is invalid, ignoring..."
60 | end
61 |
62 | pdf
63 | end
64 |
65 | def parse_finalize(pdf) #:nodoc:
66 | cast_trailer_objects(pdf)
67 |
68 | warn "This file has been linearized." if pdf.linearized?
69 |
70 | propagate_types(pdf) if Origami::OPTIONS[:enable_type_propagation]
71 |
72 | #
73 | # Decrypt encrypted file contents
74 | #
75 | if pdf.encrypted?
76 | warn "This document contains encrypted data!"
77 |
78 | decrypt_document(pdf) if @options[:decrypt]
79 | end
80 |
81 | warn "This document has been signed!" if pdf.signed?
82 |
83 | pdf
84 | end
85 |
86 | def cast_trailer_objects(pdf) #:nodoc:
87 | trailer = pdf.trailer
88 |
89 | if trailer[:Root].is_a?(Reference)
90 | pdf.cast_object(trailer[:Root], Catalog)
91 | end
92 |
93 | if trailer[:Info].is_a?(Reference)
94 | pdf.cast_object(trailer[:Info], Metadata)
95 | end
96 |
97 | if trailer[:Encrypt].is_a?(Reference)
98 | pdf.cast_object(trailer[:Encrypt], Encryption::Standard::Dictionary)
99 | end
100 | end
101 |
102 | def decrypt_document(pdf) #:nodoc:
103 | passwd = @options[:password]
104 | begin
105 | pdf.decrypt(passwd)
106 | rescue EncryptionInvalidPasswordError
107 | if passwd.empty?
108 | passwd = @options[:prompt_password].call
109 | retry unless passwd.empty?
110 | end
111 |
112 | raise
113 | end
114 | end
115 | end
116 | end
117 |
118 | end
119 |
--------------------------------------------------------------------------------
/lib/origami/parsers/pdf/linear.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 |
22 | require 'origami/parsers/pdf'
23 |
24 | module Origami
25 |
26 | class PDF
27 |
28 | #
29 | # Create a new PDF linear Parser.
30 | #
31 | class LinearParser < Parser
32 | def parse(stream)
33 | super
34 |
35 | pdf = parse_initialize
36 |
37 | #
38 | # Parse each revision
39 | #
40 | revision = 0
41 | until @data.eos? do
42 | begin
43 | pdf.add_new_revision unless revision.zero?
44 |
45 | parse_revision(pdf, revision)
46 | revision = revision + 1
47 |
48 | rescue
49 | error "Cannot read : " + (@data.peek(10) + "...").inspect
50 | error "Stopped on exception : " + $!.message
51 | STDERR.puts $!.backtrace.join($/)
52 |
53 | break
54 | end
55 | end
56 |
57 | pdf.loaded!
58 |
59 | parse_finalize(pdf)
60 | end
61 |
62 | private
63 |
64 | def parse_revision(pdf, revision_no)
65 | revision = pdf.revisions[revision_no]
66 |
67 | info "...Parsing revision #{revision_no + 1}..."
68 | loop do
69 | break if (object = parse_object).nil?
70 | pdf.insert(object)
71 | end
72 |
73 | revision.xreftable = parse_xreftable
74 | revision.trailer = parse_trailer
75 |
76 | locate_xref_streams(pdf, revision_no)
77 |
78 | revision
79 | end
80 |
81 | def locate_xref_streams(pdf, revision_no)
82 | revision = pdf.revisions[revision_no]
83 | trailer = revision.trailer
84 | xrefstm = nil
85 |
86 | # Try to match the location of the last startxref / XRefStm with an XRefStream.
87 | if trailer.startxref != 0
88 | xrefstm = pdf.get_object_by_offset(trailer.startxref)
89 | elsif trailer.key?(:XRefStm)
90 | xrefstm = pdf.get_object_by_offset(trailer[:XRefStm])
91 | end
92 |
93 | if xrefstm.is_a?(XRefStream)
94 | warn "Found a XRefStream for revision #{revision_no + 1} at #{xrefstm.reference}"
95 | revision.xrefstm = xrefstm
96 |
97 | if xrefstm.key?(:Prev)
98 | locate_prev_xref_streams(pdf, revision_no, xrefstm)
99 | end
100 | end
101 | end
102 |
103 | def locate_prev_xref_streams(pdf, revision_no, xrefstm)
104 | return unless revision_no > 0 and xrefstm.Prev.is_a?(Integer)
105 |
106 | prev_revision = pdf.revisions[revision_no - 1]
107 | prev_offset = xrefstm.Prev.to_i
108 | prev_xrefstm = pdf.get_object_by_offset(prev_offset)
109 |
110 | if prev_xrefstm.is_a?(XRefStream)
111 | warn "Found a previous XRefStream for revision #{revision_no} at #{prev_xrefstm.reference}"
112 | prev_revision.xrefstm = prev_xrefstm
113 |
114 | if prev_xrefstm.key?(:Prev)
115 | locate_prev_xref_streams(pdf, revision_no - 1, prev_xrefstm)
116 | end
117 | end
118 | end
119 | end
120 | end
121 |
122 | end
123 |
--------------------------------------------------------------------------------
/lib/origami/filters/runlength.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module Filter
24 |
25 | class InvalidRunLengthDataError < DecodeError #:nodoc:
26 | end
27 |
28 | #
29 | # Class representing a Filter used to encode and decode data using RLE compression algorithm.
30 | #
31 | class RunLength
32 | include Filter
33 |
34 | EOD = 128 #:nodoc:
35 |
36 | #
37 | # Encodes data using RLE compression method.
38 | # _stream_:: The data to encode.
39 | #
40 | def encode(stream)
41 | result = "".b
42 | i = 0
43 |
44 | while i < stream.size
45 |
46 | # How many identical bytes coming?
47 | length = compute_run_length(stream, i)
48 |
49 | # If more than 1, then compress them.
50 | if length > 1
51 | result << (257 - length).chr << stream[i]
52 | i += length
53 |
54 | # Otherwise how many different bytes to copy?
55 | else
56 | next_pos = find_next_run(stream, i)
57 | length = next_pos - i
58 |
59 | result << (length - 1).chr << stream[i, length]
60 |
61 | i += length
62 | end
63 | end
64 |
65 | result << EOD.chr
66 | end
67 |
68 | #
69 | # Decodes data using RLE decompression method.
70 | # _stream_:: The data to decode.
71 | #
72 | def decode(stream)
73 | result = "".b
74 |
75 | i = 0
76 | until i >= stream.length or stream[i].ord == EOD do
77 |
78 | # At least two bytes are required.
79 | if i > stream.length - 2
80 | raise InvalidRunLengthDataError.new("Truncated run-length data", input_data: stream, decoded_data: result)
81 | end
82 |
83 | length = stream[i].ord
84 | if length < EOD
85 | result << stream[i + 1, length + 1]
86 | i = i + length + 2
87 | else
88 | result << stream[i + 1] * (257 - length)
89 | i = i + 2
90 | end
91 | end
92 |
93 | # Check if offset is beyond the end of data.
94 | if i > stream.length
95 | raise InvalidRunLengthDataError.new("Truncated run-length data", input_data: stream, decoded_data: result)
96 | end
97 |
98 | result
99 | end
100 |
101 | private
102 |
103 | #
104 | # Find the position of the next byte at which a new run starts.
105 | #
106 | def find_next_run(input, pos)
107 | start = pos
108 | pos += 1 while pos + 1 < input.size and (pos - start + 1) < EOD and input[pos] != input[pos + 1]
109 |
110 | pos + 1
111 | end
112 |
113 | #
114 | # Computes the length of the run at the given position.
115 | #
116 | def compute_run_length(input, pos)
117 | run_length = 1
118 | while pos + 1 < input.size and run_length < EOD and input[pos] == input[pos + 1]
119 | run_length += 1
120 | pos += 1
121 | end
122 |
123 | run_length
124 | end
125 | end
126 | RL = RunLength
127 |
128 | end
129 | end
130 |
--------------------------------------------------------------------------------
/lib/origami/xfa/xfa.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'rexml/element'
22 |
23 | module Origami
24 |
25 | module XFA
26 | class XFAError < Error #:nodoc:
27 | end
28 |
29 | module ClassMethods
30 |
31 | def xfa_attribute(name)
32 | # Attribute getter.
33 | attr_getter = "attr_#{name}"
34 | remove_method(attr_getter) rescue NameError
35 | define_method(attr_getter) do
36 | self.attributes[name.to_s]
37 | end
38 |
39 | # Attribute setter.
40 | attr_setter = "attr_#{name}="
41 | remove_method(attr_setter) rescue NameError
42 | define_method(attr_setter) do |value|
43 | self.attributes[name.to_s] = value
44 | end
45 | end
46 |
47 | def xfa_node(name, type, _range = (0..Float::INFINITY))
48 |
49 | adder = "add_#{name}"
50 | remove_method(adder) rescue NameError
51 | define_method(adder) do |*attr|
52 | elt = self.add_element(type.new)
53 |
54 | unless attr.empty?
55 | attr.first.each do |k,v|
56 | elt.attributes[k.to_s] = v
57 | end
58 | end
59 |
60 | elt
61 | end
62 | end
63 |
64 | def mime_type(type)
65 | define_method("mime_type") { return type }
66 | end
67 | end
68 |
69 | def self.included(receiver)
70 | receiver.extend(ClassMethods)
71 | end
72 |
73 | class Element < REXML::Element
74 | include XFA
75 |
76 | #
77 | # A permission flag for allowing or blocking attempted changes to the element.
78 | # 0 - Allow changes to properties and content.
79 | # 1 - Block changes to properties and content.
80 | #
81 | module Lockable
82 | def lock!
83 | self.attr_lock = 1
84 | end
85 |
86 | def unlock!
87 | self.attr_lock = 0
88 | end
89 |
90 | def locked?
91 | self.attr_lock == 1
92 | end
93 |
94 | def self.included(receiver)
95 | receiver.xfa_attribute "lock"
96 | end
97 | end
98 |
99 | #
100 | # An attribute to hold human-readable metadata.
101 | #
102 | module Descriptive
103 | def self.included(receiver)
104 | receiver.xfa_attribute "desc"
105 | end
106 | end
107 |
108 | #
109 | # A unique identifier that may be used to identify this element as a target.
110 | #
111 | module Referencable
112 | def self.included?(receiver)
113 | receiver.xfa_attribute "id"
114 | end
115 | end
116 |
117 | #
118 | # At template load time, invokes another object in the same document as a prototype for this object.
119 | #
120 | module Prototypable
121 | def self.included?(receiver)
122 | receiver.xfa_attribute "use"
123 | receiver.xfa_attribute "usehref"
124 | end
125 | end
126 |
127 | #
128 | # An identifier that may be used to identify this element in script expressions.
129 | #
130 | module Namable
131 | def self.included?(receiver)
132 | receiver.xfa_attribute "name"
133 | end
134 | end
135 | end
136 |
137 | class TemplateElement < Element
138 | include Referencable
139 | include Prototypable
140 | end
141 |
142 | class NamedTemplateElement < TemplateElement
143 | include Namable
144 | end
145 | end
146 | end
147 |
--------------------------------------------------------------------------------
/lib/origami/compound.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'set'
22 |
23 | module Origami
24 |
25 | #
26 | # Module for maintaining internal caches of objects for fast lookup.
27 | #
28 | module ObjectCache
29 | attr_reader :strings_cache, :names_cache, :xref_cache
30 |
31 | def initialize(*args)
32 | super(*args)
33 |
34 | init_caches
35 | end
36 |
37 | def rebuild_caches
38 | self.each do |*items|
39 | items.each do |object|
40 | object.rebuild_caches if object.is_a?(CompoundObject)
41 | cache_object(object)
42 | end
43 | end
44 | end
45 |
46 | private
47 |
48 | def init_caches
49 | @strings_cache = Set.new
50 | @names_cache = Set.new
51 | @xref_cache = {}
52 | end
53 |
54 | def cache_object(object)
55 | case object
56 | when String then cache_string(object)
57 | when Name then cache_name(object)
58 | when Reference then cache_reference(object)
59 | when CompoundObject then cache_compound(object)
60 | end
61 |
62 | object
63 | end
64 |
65 | def cache_compound(object)
66 | @strings_cache.merge(object.strings_cache)
67 | @names_cache.merge(object.names_cache)
68 | @xref_cache.update(object.xref_cache) do |_, cache1, cache2|
69 | cache1.concat(cache2)
70 | end
71 |
72 | object.strings_cache.clear
73 | object.names_cache.clear
74 | object.xref_cache.clear
75 | end
76 |
77 | def cache_string(str)
78 | @strings_cache.add(str)
79 | end
80 |
81 | def cache_name(name)
82 | @names_cache.add(name)
83 | end
84 |
85 | def cache_reference(ref)
86 | @xref_cache[ref] ||= []
87 | @xref_cache[ref].push(self)
88 | end
89 | end
90 |
91 | #
92 | # Module for objects containing other objects.
93 | #
94 | module CompoundObject
95 | include Origami::Object
96 | include ObjectCache
97 | using TypeConversion
98 |
99 | #
100 | # Returns true if the item is present in the compound object.
101 | #
102 | def include?(item)
103 | super(item.to_o)
104 | end
105 |
106 | #
107 | # Removes the item from the compound object if present.
108 | #
109 | def delete(item)
110 | obj = super(item.to_o)
111 | unlink_object(obj) unless obj.nil?
112 | end
113 |
114 | #
115 | # Creates a deep copy of the compound object.
116 | # This method can be quite expensive as nested objects are copied too.
117 | #
118 | def copy
119 | obj = self.update_values(&:copy)
120 |
121 | transfer_attributes(obj)
122 | end
123 |
124 | #
125 | # Returns a new compound object with updated values based on the provided block.
126 | #
127 | def update_values(&b)
128 | return enum_for(__method__) unless block_given?
129 | return self.class.new self.transform_values(&b) if self.respond_to?(:transform_values)
130 | return self.class.new self.map(&b) if self.respond_to?(:map)
131 |
132 | raise NotImplementedError, "This object does not implement this method"
133 | end
134 |
135 | #
136 | # Modifies the compound object's values based on the provided block.
137 | #
138 | def update_values!(&b)
139 | return enum_for(__method__) unless block_given?
140 | return self.transform_values!(&b) if self.respond_to?(:transform_values!)
141 | return self.map!(&b) if self.respond_to?(:map!)
142 |
143 | raise NotImplementedError, "This object does not implement this method"
144 | end
145 |
146 | private
147 |
148 | def link_object(item)
149 | obj = item.to_o
150 | obj.parent = self unless obj.indirect?
151 |
152 | cache_object(obj)
153 | end
154 |
155 | def unlink_object(obj)
156 | obj.parent = nil
157 | obj
158 | end
159 | end
160 |
161 | end
162 |
--------------------------------------------------------------------------------
/test/test_pdf_parse.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 |
3 | class TestPDFParser < Minitest::Test
4 | def setup
5 | @files =
6 | %w{
7 | dataset/empty.pdf
8 | dataset/calc.pdf
9 | dataset/crypto.pdf
10 | }
11 | end
12 |
13 | def test_parse_pdf
14 | @files.each do |file|
15 | pdf = PDF.read(File.join(__dir__, file), ignore_errors: false, verbosity: Parser::VERBOSE_QUIET)
16 |
17 | assert_instance_of PDF, pdf
18 |
19 | pdf.each_object do |object|
20 | assert_kind_of Origami::Object, object
21 | end
22 | end
23 | end
24 |
25 | def test_parse_dictionary
26 | dict = Dictionary.parse("<>>>")
27 |
28 | assert_instance_of Dictionary, dict
29 | assert_instance_of Dictionary, dict[:D]
30 | assert_instance_of Null, dict[:N]
31 | assert_instance_of Reference, dict[:Ref]
32 | assert_equal dict.size, 4
33 | assert_raises(InvalidReferenceError) { dict[:Ref].solve }
34 | assert dict[:Pi] == 3.14
35 | end
36 |
37 | def test_parse_array
38 | array = Origami::Array.parse("[/Test (abc) .2 \n 799 [<<>>]]")
39 |
40 | assert array.all?{|e| e.is_a?(Origami::Object)}
41 | assert_equal array.length, 5
42 | assert_raises(InvalidArrayObjectError) { Origami::Array.parse("[1 ") }
43 | assert_raises(InvalidArrayObjectError) { Origami::Array.parse("") }
44 | end
45 |
46 | def test_parse_string
47 | str = LiteralString.parse("(\\122\\125by\\n)")
48 | assert_instance_of LiteralString, str
49 | assert_equal str.value, "RUby\n"
50 |
51 | assert_raises(InvalidLiteralStringObjectError) { LiteralString.parse("") }
52 | assert_raises(InvalidLiteralStringObjectError) { LiteralString.parse("(test") }
53 | assert_equal "((O))", LiteralString.parse("(((O)))").value
54 | assert_equal LiteralString.parse("(ABC\\\nDEF\\\r\nGHI)").value, "ABCDEFGHI"
55 | assert_equal LiteralString.parse('(\r\n\b\t\f\\(\\)\\x\\\\)').value, "\r\n\b\t\f()x\\"
56 | assert_equal LiteralString.parse('(\r\n\b\t\f\\\\\\(\\))').to_s, '(\r\n\b\t\f\\\\\\(\\))'
57 |
58 | str = HexaString.parse("<52 55 62 79 0A>")
59 | assert_instance_of HexaString, str
60 | assert_equal str.value, "RUby\n"
61 |
62 | assert_equal HexaString.parse("<4>").value, 0x40.chr
63 |
64 | assert_raises(InvalidHexaStringObjectError) { HexaString.parse("") }
65 | assert_raises(InvalidHexaStringObjectError) { HexaString.parse("<12") }
66 | assert_raises(InvalidHexaStringObjectError) { HexaString.parse("<12X>") }
67 | end
68 |
69 | def test_parse_bool
70 | b_true = Boolean.parse("true")
71 | b_false = Boolean.parse("false")
72 |
73 | assert_instance_of Boolean, b_true
74 | assert_instance_of Boolean, b_false
75 |
76 | assert b_false.false?
77 | refute b_true.false?
78 |
79 | assert_raises(InvalidBooleanObjectError) { Boolean.parse("") }
80 | assert_raises(InvalidBooleanObjectError) { Boolean.parse("tru") }
81 | end
82 |
83 | def test_parse_real
84 | real = Real.parse("-3.141592653")
85 | assert_instance_of Real, real
86 | assert_equal real, -3.141592653
87 |
88 | real = Real.parse("+.00200")
89 | assert_instance_of Real, real
90 | assert_equal real, 0.002
91 |
92 | assert_raises(InvalidRealObjectError) { Real.parse("") }
93 | assert_raises(InvalidRealObjectError) { Real.parse(".") }
94 | assert_raises(InvalidRealObjectError) { Real.parse("+0x1") }
95 | end
96 |
97 | def test_parse_int
98 | int = Origami::Integer.parse("02000000000000")
99 | assert_instance_of Origami::Integer, int
100 | assert_equal int, 2000000000000
101 |
102 | int = Origami::Integer.parse("-98")
103 | assert_instance_of Origami::Integer, int
104 | assert_equal int, -98
105 |
106 | assert_raises(Origami::InvalidIntegerObjectError) { Origami::Integer.parse("") }
107 | assert_raises(Origami::InvalidIntegerObjectError) { Origami::Integer.parse("+-1") }
108 | assert_raises(Origami::InvalidIntegerObjectError) { Origami::Integer.parse("ABC") }
109 | end
110 |
111 | def test_parse_name
112 | name = Name.parse("/#52#55#62#79#0A")
113 | assert_instance_of Name, name
114 | assert_equal name.value, :"RUby\n"
115 |
116 | name = Name.parse("/")
117 | assert_instance_of Name, name
118 | assert_equal :"", name.value
119 |
120 | assert_raises(Origami::InvalidNameObjectError) { Name.parse("") }
121 | assert_raises(Origami::InvalidNameObjectError) { Name.parse("test") }
122 | end
123 |
124 | def test_parse_reference
125 | ref = Reference.parse("199 1 R")
126 | assert_instance_of Reference, ref
127 |
128 | assert_equal [199, 1], ref.to_a
129 | assert_raises(InvalidReferenceError) { ref.solve }
130 | assert_raises(InvalidReferenceError) { Reference.parse("-2 0 R") }
131 | assert_raises(InvalidReferenceError) { Reference.parse("0 R") }
132 | assert_raises(InvalidReferenceError) { Reference.parse("") }
133 | end
134 | end
135 |
--------------------------------------------------------------------------------
/lib/origami/numeric.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'delegate'
22 |
23 | module Origami
24 |
25 | class InvalidIntegerObjectError < InvalidObjectError #:nodoc:
26 | end
27 |
28 | #
29 | # Class representing a PDF number (Integer, or Real).
30 | #
31 | module Number
32 | include Origami::Object
33 |
34 | def ~
35 | self.class.new(~self.value)
36 | end
37 |
38 | def |(val)
39 | self.class.new(self.value | val)
40 | end
41 |
42 | def &(val)
43 | self.class.new(self.value & val)
44 | end
45 |
46 | def ^(val)
47 | self.class.new(self.value ^ val)
48 | end
49 |
50 | def <<(val)
51 | self.class.new(self.value << val)
52 | end
53 |
54 | def >>(val)
55 | self.class.new(self.value >> val)
56 | end
57 |
58 | def +(val)
59 | self.class.new(self.value + val)
60 | end
61 |
62 | def -(val)
63 | self.class.new(self.value - val)
64 | end
65 |
66 | def -@
67 | self.class.new(-self.value)
68 | end
69 |
70 | def *(val)
71 | self.class.new(self.value * val)
72 | end
73 |
74 | def /(val)
75 | self.class.new(self.value / val)
76 | end
77 |
78 | def abs
79 | self.class.new(self.value.abs)
80 | end
81 |
82 | def **(val)
83 | self.class.new(self.value ** val)
84 | end
85 | end
86 |
87 | #
88 | # Class representing an Integer Object.
89 | #
90 | class Integer < DelegateClass(::Integer)
91 | include Number
92 |
93 | TOKENS = [ "(\\+|-)?[\\d]+[^.]?" ] #:nodoc:
94 | REGEXP_TOKEN = Regexp.new(TOKENS.first)
95 |
96 | @@regexp = Regexp.new(WHITESPACES + "(?(\\+|-)?[\\d]+)")
97 |
98 | #
99 | # Creates a new Integer from a Ruby Fixnum / Bignum.
100 | # _i_:: The Integer value.
101 | #
102 | def initialize(i = 0)
103 | unless i.is_a?(::Integer)
104 | raise TypeError, "Expected type Fixnum or Bignum, received #{i.class}."
105 | end
106 |
107 | super(i)
108 | end
109 |
110 | def self.parse(stream, _parser = nil) #:nodoc:
111 | scanner = Parser.init_scanner(stream)
112 | offset = scanner.pos
113 |
114 | if not scanner.scan(@@regexp)
115 | raise InvalidIntegerObjectError, "Invalid integer format"
116 | end
117 |
118 | value = scanner['int'].to_i
119 | int = Integer.new(value)
120 | int.file_offset = offset
121 |
122 | int
123 | end
124 |
125 | def to_s(eol: $/) #:nodoc:
126 | super(self.value.to_s, eol: eol)
127 | end
128 |
129 | alias value to_i
130 | end
131 |
132 | class InvalidRealObjectError < InvalidObjectError #:nodoc:
133 | end
134 |
135 | #
136 | # Class representing a Real number Object.
137 | # PDF real numbers are arbitrary precision numbers, depending on architectures.
138 | #
139 | class Real < DelegateClass(Float)
140 | include Number
141 |
142 | TOKENS = [ "(\\+|-)?([\\d]*\\.[\\d]+|[\\d]+\\.[\\d]*)([eE](\\+|-)?[\\d]+)?" ] #:nodoc:
143 | REGEXP_TOKEN = Regexp.new(TOKENS.first)
144 |
145 | @@regexp = Regexp.new(WHITESPACES + "(?#{TOKENS.first})")
146 |
147 | #
148 | # Creates a new Real from a Ruby Float.
149 | # _f_:: The new Real value.
150 | #
151 | def initialize(f = 0)
152 | unless f.is_a?(Float)
153 | raise TypeError, "Expected type Float, received #{f.class}."
154 | end
155 |
156 | super(f)
157 | end
158 |
159 | def self.parse(stream, _parser = nil) #:nodoc:
160 | scanner = Parser.init_scanner(stream)
161 | offset = scanner.pos
162 |
163 | if not scanner.scan(@@regexp)
164 | raise InvalidRealObjectError, "Invalid real number format"
165 | end
166 |
167 | value = scanner['real'].to_f
168 | real = Real.new(value)
169 | real.file_offset = offset
170 |
171 | real
172 | end
173 |
174 | alias value to_f
175 |
176 | def to_s(eol: $/) #:nodoc:
177 | super(sprintf("%f", self).sub(/\.0*$|(\.\d*[^0])0*$/, '\1'), eol: eol)
178 | end
179 | end
180 |
181 | end
182 |
--------------------------------------------------------------------------------
/lib/origami/xfa/connectionset.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2017 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module XFA
24 | class ConnectionElement < Element
25 | xfa_attribute 'dataDescription'
26 | xfa_attribute 'name'
27 | end
28 | end
29 |
30 | module XDP
31 |
32 | module Packet
33 |
34 | #
35 | # The _connectionSet_ packet describes the connections used to initiate or conduct web services.
36 | #
37 | class ConnectionSet < XFA::Element
38 | mime_type 'text/xml'
39 |
40 | def initialize
41 | super("connectionSet")
42 |
43 | add_attribute 'xmlns', 'http://www.xfa.org/schema/xfa-connection-set/2.8/'
44 | end
45 |
46 | class EffectiveInputPolicy < XFA::NamedTemplateElement
47 | def initialize
48 | super('effectiveInputPolicy')
49 | end
50 | end
51 |
52 | class EffectiveOutputPolicy < XFA::NamedTemplateElement
53 | def initialize
54 | super('effectiveOutputPolicy')
55 | end
56 | end
57 |
58 | class Operation < XFA::NamedTemplateElement
59 | xfa_attribute 'input'
60 | xfa_attribute 'output'
61 |
62 | def initialize(name = "")
63 | super('operation')
64 |
65 | self.text = name
66 | end
67 | end
68 |
69 | class SOAPAction < XFA::NamedTemplateElement
70 | def initialize(uri = "")
71 | super('soapAction')
72 |
73 | self.text = uri
74 | end
75 | end
76 |
77 | class SOAPAddress < XFA::NamedTemplateElement
78 | def initialize(addr = "")
79 | super('soapAddress')
80 |
81 | self.text = addr
82 | end
83 | end
84 |
85 | class WSDLAddress < XFA::NamedTemplateElement
86 | def initialize(addr = "")
87 | super('wsdlAddress')
88 |
89 | self.text = addr
90 | end
91 | end
92 |
93 | class WSDLConnection < XFA::ConnectionElement
94 | xfa_node 'effectiveInputPolicy', ConnectionSet::EffectiveInputPolicy, 0..1
95 | xfa_node 'effectiveOutputPolicy', ConnectionSet::EffectiveOutputPolicy, 0..1
96 | xfa_node 'operation', ConnectionSet::Operation, 0..1
97 | xfa_node 'soapAction', ConnectionSet::SOAPAction, 0..1
98 | xfa_node 'soapAddress', ConnectionSet::SOAPAddress, 0..1
99 | xfa_node 'wsdlAddress', ConnectionSet::WSDLAddress, 0..1
100 |
101 | def initialize
102 | super('wsdlConnection')
103 | end
104 | end
105 |
106 | class URI < XFA::NamedTemplateElement
107 | def initialize(uri = "")
108 | super('uri')
109 |
110 | self.text = uri
111 | end
112 | end
113 |
114 | class RootElement < XFA::NamedTemplateElement
115 | def initialize(root = '')
116 | super('rootElement')
117 |
118 | self.text = root
119 | end
120 | end
121 |
122 | class XSDConnection < XFA::ConnectionElement
123 | xfa_node 'rootElement', ConnectionSet::RootElement, 0..1
124 | xfa_node 'uri', ConnectionSet::URI, 0..1
125 |
126 | def initialize
127 | super('xsdConnection')
128 | end
129 | end
130 |
131 | class XMLConnection < XFA::ConnectionElement
132 | xfa_node 'uri', ConnectionSet::URI, 0..1
133 |
134 | def initialize
135 | super('xmlConnection')
136 | end
137 | end
138 |
139 | xfa_node 'wsdlConnection', ConnectionSet::WSDLConnection
140 | xfa_node 'xmlConnection', ConnectionSet::XMLConnection
141 | xfa_node 'xsdConnection', ConnectionSet::XSDConnection
142 | end
143 |
144 | end
145 | end
146 | end
147 |
--------------------------------------------------------------------------------
/lib/origami/graphics/path.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | module Graphics
24 |
25 | module LineCapStyle
26 | BUTT_CAP = 0
27 | ROUND_CAP = 1
28 | PROJECTING_SQUARE_CAP = 2
29 | end
30 |
31 | module LineJoinStyle
32 | MITER_JOIN = 0
33 | ROUND_JOIN = 1
34 | BEVEL_JOIN = 2
35 | end
36 |
37 | class DashPattern
38 | attr_accessor :array, :phase
39 |
40 | def initialize(array, phase = 0)
41 | @array = array
42 | @phase = phase
43 | end
44 |
45 | def eql?(dash) #:nodoc
46 | dash.array == @array and dash.phase == @phase
47 | end
48 |
49 | def hash #:nodoc:
50 | [ @array, @phase ].hash
51 | end
52 | end
53 |
54 | class InvalidPathError < Error #:nodoc:
55 | end
56 |
57 | class Path
58 | module Segment
59 | attr_accessor :from, :to
60 |
61 | def initialize(from, to)
62 | @from, @to = from, to
63 | end
64 | end
65 |
66 | class Line
67 | include Segment
68 | end
69 |
70 | attr_accessor :current_point
71 | attr_reader :segments
72 |
73 | def initialize
74 | @segments = []
75 | @current_point = nil
76 | @closed = false
77 | end
78 |
79 | def is_closed?
80 | @closed
81 | end
82 |
83 | def close!
84 | from = @current_point
85 | to = @segments.first.from
86 |
87 | @segments << Line.new(from, to)
88 | @segments.freeze
89 | @closed = true
90 | end
91 |
92 | def add_segment(seg)
93 | raise GraphicsStateError, "Cannot modify closed subpath" if is_closed?
94 |
95 | @segments << seg
96 | @current_point = seg.to
97 | end
98 | end
99 | end
100 |
101 | class PDF::Instruction
102 | insn 'm', Real, Real do |canvas, x,y|
103 | canvas.gs.current_path << (subpath = Graphics::Path.new)
104 | subpath.current_point = [x,y]
105 | end
106 |
107 | insn 'l', Real, Real do |canvas, x,y|
108 | if canvas.gs.current_path.empty?
109 | raise InvalidPathError, "No current point is defined"
110 | end
111 |
112 | subpath = canvas.gs.current_path.last
113 |
114 | from = subpath.current_point
115 | to = [x,y]
116 | subpath.add_segment(Graphics::Path::Line.new(from, to))
117 | end
118 |
119 | insn 'h' do |canvas|
120 | unless canvas.gs.current_path.empty?
121 | subpath = canvas.gs.current_path.last
122 | subpath.close! unless subpath.is_closed?
123 | end
124 | end
125 |
126 | insn 're', Real, Real, Real, Real do |canvas, x,y,width,height|
127 | tx = x + width
128 | ty = y + height
129 | canvas.gs.current_path << (subpath = Graphics::Path.new)
130 | subpath.segments << Graphics::Path::Line.new([x,y], [tx,y])
131 | subpath.segments << Graphics::Path::Line.new([tx,y], [tx, ty])
132 | subpath.segments << Graphics::Path::Line.new([tx, ty], [x, ty])
133 | subpath.close!
134 | end
135 |
136 | insn 'S' do |canvas|
137 | canvas.stroke_path
138 | end
139 |
140 | insn 's' do |canvas|
141 | canvas.gs.current_path.last.close!
142 | canvas.stroke_path
143 | end
144 |
145 | insn 'f' do |canvas|
146 | canvas.fill_path
147 | end
148 |
149 | insn 'F' do |canvas|
150 | canvas.fill_path
151 | end
152 |
153 | insn 'f*' do |canvas|
154 | canvas.fill_path
155 | end
156 |
157 | insn 'B' do |canvas|
158 | canvas.fill_path
159 | canvas.stroke_path
160 | end
161 |
162 | insn 'B*' do |canvas|
163 | canvas.fill_path
164 | canvas.stroke_path
165 | end
166 |
167 | insn 'b' do |canvas|
168 | canvas.gs.current_path.last.close!
169 | canvas.fill_path
170 | canvas.stroke_path
171 | end
172 |
173 | insn 'b*' do |canvas|
174 | canvas.gs.current_path.last.close!
175 | canvas.fill_path
176 | canvas.stroke_path
177 | end
178 |
179 | insn 'n'
180 | end
181 |
182 | end
183 |
--------------------------------------------------------------------------------
/test/test_streams.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'stringio'
3 |
4 | class TestStreams < Minitest::Test
5 | def setup
6 | @target = PDF.new
7 | @output = StringIO.new
8 | @data = "0123456789" * 1024
9 | end
10 |
11 | def test_predictors
12 | stm = Stream.new(@data, :Filter => :FlateDecode)
13 | stm.set_predictor(Filter::Predictor::TIFF)
14 | raw = stm.encoded_data
15 | stm.data = nil
16 | stm.encoded_data = raw
17 |
18 | assert_equal @data, stm.data
19 |
20 | stm = Stream.new(@data, :Filter => :FlateDecode)
21 | stm.set_predictor(Filter::Predictor::PNG_SUB)
22 | raw = stm.encoded_data
23 | stm.data = nil
24 | stm.encoded_data = raw
25 |
26 | assert_equal @data, stm.data
27 |
28 | stm = Stream.new(@data, :Filter => :FlateDecode)
29 | stm.set_predictor(Filter::Predictor::PNG_UP)
30 | raw = stm.encoded_data
31 | stm.data = nil
32 | stm.encoded_data = raw
33 |
34 | assert_equal stm.data, @data
35 |
36 | stm = Stream.new(@data, :Filter => :FlateDecode)
37 | stm.set_predictor(Filter::Predictor::PNG_AVERAGE)
38 | raw = stm.encoded_data
39 | stm.data = nil
40 | stm.encoded_data = raw
41 |
42 | assert_equal stm.data, @data
43 |
44 | stm = Stream.new(@data, :Filter => :FlateDecode)
45 | stm.set_predictor(Filter::Predictor::PNG_PAETH)
46 | raw = stm.encoded_data
47 | stm.data = nil
48 | stm.encoded_data = raw
49 |
50 | assert_equal stm.data, @data
51 | end
52 |
53 | def test_filter_flate
54 | stm = Stream.new(@data, :Filter => :FlateDecode)
55 | raw = stm.encoded_data
56 | stm.data = nil
57 | stm.encoded_data = raw
58 |
59 | assert_equal stm.data, @data
60 |
61 | assert_equal Filter::Flate.decode(Filter::Flate.encode("")), ""
62 | end
63 |
64 | def test_filter_asciihex
65 | stm = Stream.new(@data, :Filter => :ASCIIHexDecode)
66 | raw = stm.encoded_data
67 | stm.data = nil
68 | stm.encoded_data = raw
69 |
70 | assert_equal stm.data, @data
71 |
72 | assert_raises(Filter::InvalidASCIIHexStringError) do
73 | Filter::ASCIIHex.decode("123456789ABCDEFGHIJKL")
74 | end
75 |
76 | assert_equal Filter::ASCIIHex.decode(Filter::ASCIIHex.encode("")), ""
77 | end
78 |
79 | def test_filter_ascii85
80 | stm = Stream.new(@data, :Filter => :ASCII85Decode)
81 | raw = stm.encoded_data
82 | stm.data = nil
83 | stm.encoded_data = raw
84 |
85 | assert_equal stm.data, @data
86 |
87 | assert_raises(Filter::InvalidASCII85StringError) do
88 | Filter::ASCII85.decode("ABCD\x01")
89 | end
90 |
91 | assert_equal Filter::ASCII85.decode(Filter::ASCII85.encode("")), ""
92 | end
93 |
94 | def test_filter_rle
95 | stm = Stream.new(@data, :Filter => :RunLengthDecode)
96 | raw = stm.encoded_data
97 | stm.data = nil
98 | stm.encoded_data = raw
99 |
100 | assert_equal stm.data, @data
101 |
102 | assert_raises(Filter::InvalidRunLengthDataError) do
103 | Filter::RunLength.decode("\x7f")
104 | end
105 |
106 | assert_equal Filter::RunLength.decode(Filter::RunLength.encode("")), ""
107 | end
108 |
109 | def test_filter_lzw
110 | stm = Stream.new(@data, :Filter => :LZWDecode)
111 | raw = stm.encoded_data
112 | stm.data = nil
113 | stm.encoded_data = raw
114 |
115 | assert_equal stm.data, @data
116 |
117 | assert_raises(Filter::InvalidLZWDataError) do
118 | Filter::LZW.decode("abcd")
119 | end
120 |
121 | assert_equal Filter::LZW.decode(Filter::LZW.encode("")), ""
122 | end
123 |
124 | def test_filter_ccittfax
125 | stm = Stream.new(@data[0, 216], :Filter => :CCITTFaxDecode)
126 |
127 | raw = stm.encoded_data
128 | stm.data = nil
129 | stm.encoded_data = raw
130 |
131 | assert_equal stm.data, @data[0, 216]
132 |
133 | assert_raises(Filter::InvalidCCITTFaxDataError) do
134 | Filter::CCITTFax.decode("abcd")
135 | end
136 |
137 | assert_equal Filter::CCITTFax.decode(Filter::CCITTFax.encode("")), ""
138 | end
139 |
140 | def test_stream
141 | chain = %i[FlateDecode LZWDecode ASCIIHexDecode]
142 |
143 | stm = Stream.new(@data, Filter: chain)
144 | @target << stm
145 | @target.save(@output)
146 |
147 | assert stm.Length == stm.encoded_data.length
148 | assert_equal stm.filters, chain
149 | assert_equal stm.data, @data
150 | end
151 |
152 | def test_object_stream
153 | objstm = ObjectStream.new
154 | objstm.Filter = %i[FlateDecode ASCIIHexDecode RunLengthDecode]
155 |
156 | @target << objstm
157 |
158 | assert_raises(InvalidObjectError) do
159 | objstm.insert Stream.new
160 | end
161 |
162 | 3.times do
163 | objstm.insert HexaString.new(@data)
164 | end
165 |
166 | assert_equal objstm.objects.size, 3
167 |
168 | objstm.each_object do |object|
169 | assert_instance_of HexaString, object
170 | assert_equal object.parent, objstm
171 | assert objstm.include?(object.no)
172 | assert_equal objstm.extract(object.no), object
173 | assert_equal objstm.extract_by_index(objstm.index(object.no)), object
174 | end
175 |
176 | objstm.delete(objstm.objects.first.no)
177 | assert_equal objstm.objects.size, 2
178 |
179 | @target.save(@output)
180 |
181 | assert_instance_of Origami::Integer, objstm.N
182 | assert_equal objstm.N, objstm.objects.size
183 | end
184 | end
185 |
--------------------------------------------------------------------------------
/lib/origami/collections.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | module Origami
22 |
23 | class PDF
24 | #
25 | # Returns true if the document behaves as a portfolio for embedded files.
26 | #
27 | def portfolio?
28 | self.Catalog.Collection.is_a?(Dictionary)
29 | end
30 | end
31 |
32 | class Collection < Dictionary
33 | include StandardObject
34 |
35 | module View
36 | DETAILS = :D
37 | TILE = :T
38 | HIDDEN = :H
39 | end
40 |
41 | class Schema < Dictionary
42 | include StandardObject
43 |
44 | field :Type, :Type => Name, :Default => :CollectionSchema
45 | end
46 |
47 | class Navigator < Dictionary
48 | include StandardObject
49 |
50 | module Type
51 | FLEX = :Module
52 | FLASH = :Default
53 | end
54 |
55 | field :SWF, :Type => String, :Required => true
56 | field :Name, :Type => String, :Required => true
57 | field :Desc, :Type => String
58 | field :Category, :Type => String
59 | field :ID, :Type => String, :Required => true
60 | field :Version, :Type => String
61 | field :APIVersion, :Type => String, :Required => true
62 | field :LoadType, :Type => Name, :Default => Type::FLASH
63 | field :Icon, :Type => String
64 | field :Locale, :Type => String
65 | field :Strings, :Type => NameTreeNode.of(String)
66 | field :InitialFields, :Type => Schema
67 | field :Resources, :Type => NameTreeNode.of(Stream), :Required => true
68 | end
69 |
70 | class Color < Dictionary
71 | include StandardObject
72 |
73 | field :Background, :Type => Array.of(Number, length: 3)
74 | field :CardBackground, :Type => Array.of(Number, length: 3)
75 | field :CardBorder, :Type => Array.of(Number, length: 3)
76 | field :PrimaryText, :Type => Array.of(Number, length: 3)
77 | field :SecondaryText, :Type => Array.of(Number, length: 3)
78 | end
79 |
80 | class Split < Dictionary
81 | include StandardObject
82 |
83 | HORIZONTAL = :H
84 | VERTICAL = :V
85 | NONE = :N
86 |
87 | field :Direction, :Type => Name
88 | field :Position, :Type => Number
89 | end
90 |
91 | class Item < Dictionary
92 | include StandardObject
93 |
94 | field :Type, :Type => Name, :Default => :CollectionItem
95 | end
96 |
97 | class Subitem < Dictionary
98 | include StandardObject
99 |
100 | field :Type, :Type => Name, :Default => :CollectionSubitem
101 | field :D, :Type => [ String, Number ]
102 | field :P, :Type => String
103 | end
104 |
105 | class Folder < Dictionary
106 | include StandardObject
107 |
108 | field :Type, :Type => Name, :Default => :Folder
109 | field :ID, :Type => Integer, :Required => true
110 | field :Name, :Type => String, :Required => true
111 | field :Parent, :Type => Folder
112 | field :Child, :Type => Folder
113 | field :Next, :Type => Folder
114 | field :CI, :Type => Item
115 | field :Desc, :Type => String
116 | field :CreationDate, :Type => String
117 | field :ModDate, :Type => String
118 | field :Thumb, :Type => Stream
119 | field :Free, :Type => Array.of(Array.of(Integer, length: 2))
120 | end
121 |
122 | class Sort < Dictionary
123 | include StandardObject
124 |
125 | field :Type, :Type => Name, :Default => :CollectionSort
126 | field :S, :Type => [ Name, Array.of(Name) ]
127 | field :A, :Type => [ Boolean, Array.of(Boolean) ]
128 | end
129 |
130 | #
131 | # Collection fields.
132 | #
133 | field :Type, :Type => Name, :Default => :Collection
134 | field :Schema, :Type => Schema
135 | field :D, :Type => String
136 | field :View, :Type => Name, :Default => View::DETAILS
137 | field :Sort, :Type => Sort
138 | field :Navigator, :Type => Navigator, :ExtensionLevel => 3
139 | field :Resources, :Type => NameTreeNode.of(Stream), :ExtensionLevel => 3
140 | field :Colors, :Type => Color, :ExtensionLevel => 3
141 | field :Folders, :Type => Folder, :ExtensionLevel => 3
142 | field :Split, :Type => Split, :ExtensionLevel => 3
143 | end
144 | end
145 |
--------------------------------------------------------------------------------
/lib/origami/filters/lzw.rb:
--------------------------------------------------------------------------------
1 | =begin
2 |
3 | This file is part of Origami, PDF manipulation framework for Ruby
4 | Copyright (C) 2016 Guillaume Delugré.
5 |
6 | Origami is free software: you can redistribute it and/or modify
7 | it under the terms of the GNU Lesser 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 | Origami 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 Lesser General Public License for more details.
15 |
16 | You should have received a copy of the GNU Lesser General Public License
17 | along with Origami. If not, see .
18 |
19 | =end
20 |
21 | require 'origami/filters/predictors'
22 |
23 | module Origami
24 |
25 | module Filter
26 |
27 | class InvalidLZWDataError < DecodeError #:nodoc:
28 | end
29 |
30 | #
31 | # Class representing a filter used to encode and decode data with LZW compression algorithm.
32 | #
33 | class LZW
34 | include Filter
35 | include Predictor
36 |
37 | EOD = 257 #:nodoc:
38 | CLEARTABLE = 256 #:nodoc:
39 |
40 | #
41 | # Encodes given data using LZW compression method.
42 | # _stream_:: The data to encode.
43 | #
44 | def encode(string)
45 | input = pre_prediction(string)
46 |
47 | table, codesize = reset_state
48 | result = Utils::BitWriter.new
49 | result.write(CLEARTABLE, codesize)
50 |
51 | s = ''
52 | input.each_byte do |byte|
53 | char = byte.chr
54 |
55 | if table.size == 4096
56 | result.write(CLEARTABLE, codesize)
57 | table, _ = reset_state
58 | end
59 |
60 | codesize = table.size.bit_length
61 |
62 | it = s + char
63 | if table.has_key?(it)
64 | s = it
65 | else
66 | result.write(table[s], codesize)
67 | table[it] = table.size
68 | s = char
69 | end
70 | end
71 |
72 | result.write(table[s], codesize) unless s.empty?
73 | result.write(EOD, codesize)
74 |
75 | result.final.to_s
76 | end
77 |
78 | #
79 | # Decodes given data using LZW compression method.
80 | # _stream_:: The data to decode.
81 | #
82 | def decode(string)
83 | result = "".b
84 | bstring = Utils::BitReader.new(string)
85 | table, codesize = reset_state
86 | prevbyte = nil
87 |
88 | until bstring.eod? do
89 | byte = bstring.read(codesize)
90 | break if byte == EOD
91 |
92 | if byte == CLEARTABLE
93 | table, codesize = reset_state
94 | prevbyte = nil
95 | redo
96 | end
97 |
98 | begin
99 | codesize = decode_codeword_size(table)
100 | result << decode_byte(table, prevbyte, byte, codesize)
101 | rescue InvalidLZWDataError => error
102 | error.message.concat " (bit pos #{bstring.pos - codesize})"
103 | error.input_data = string
104 | error.decoded_data = result
105 | raise(error)
106 | end
107 |
108 | prevbyte = byte
109 | end
110 |
111 | post_prediction(result)
112 | end
113 |
114 | private
115 |
116 | def decode_codeword_size(table)
117 | case table.size
118 | when 258...510 then 9
119 | when 510...1022 then 10
120 | when 1022...2046 then 11
121 | when 2046...4095 then 12
122 | else
123 | raise InvalidLZWDataError, "LZW table is full and no clear flag was set"
124 | end
125 | end
126 |
127 | def decode_byte(table, previous_byte, byte, codesize) #:nodoc:
128 |
129 | # Ensure the codeword can be decoded in the current state.
130 | check_codeword(table, previous_byte, byte, codesize)
131 |
132 | if previous_byte.nil?
133 | table.key(byte)
134 | else
135 | if table.value?(byte)
136 | entry = table.key(byte)
137 | else
138 | entry = table.key(previous_byte)
139 | entry += entry[0, 1]
140 | end
141 |
142 | table[table.key(previous_byte) + entry[0,1]] = table.size
143 |
144 | entry
145 | end
146 | end
147 |
148 | def check_codeword(table, previous_byte, byte, codesize) #:nodoc:
149 | if (previous_byte.nil? and not table.value?(byte)) or (previous_byte and not table.value?(previous_byte))
150 | codeword = previous_byte || byte
151 | raise InvalidLZWDataError, "No entry for codeword #{codeword.to_s(2).rjust(codesize, '0')}"
152 | end
153 | end
154 |
155 | def reset_state #:nodoc:
156 | table = {}
157 | 256.times do |i|
158 | table[i.chr] = i
159 | end
160 |
161 | table[CLEARTABLE] = CLEARTABLE
162 | table[EOD] = EOD
163 |
164 | # Codeword table, codeword size
165 | [table, 9]
166 | end
167 | end
168 |
169 | end
170 | end
171 |
--------------------------------------------------------------------------------