├── .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 | [![Gem Version](https://badge.fury.io/rb/origami.svg)](https://rubygems.org/gems/origami) 4 | [![Downloads](https://img.shields.io/gem/dt/origami.svg)](https://rubygems.org/gems/origami) 5 | [![Build Status](https://secure.travis-ci.org/gdelugre/origami.svg?branch=master)](https://travis-ci.org/gdelugre/origami) 6 | [![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](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 | --------------------------------------------------------------------------------