=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami begin require 'v8' class PDF module JavaScript module Platforms WINDOWS = "WIN" UNIX = "UNIX" MAC = "MAC" end module Viewers ADOBE_READER = "Reader" end class Error < Origami::Error; end class MissingArgError < Error def initialize; super("Missing required argument.") end end class TypeError < Error def initialize; super("Incorrect argument type.") end end class InvalidArgsError < Error def initialize; super("Incorrect arguments.") end end class NotAllowedError < Error def initialize; super("Security settings prevent access to this property or method.") end end class HelpError < Error def initialize; super("Help") end end class GeneralError < Error def initialize; super("Operation failed.") end end class Arg attr_reader :name, :type, :required, :default def initialize(declare = {}) @name = declare[:name] @type = declare[:type] @required = declare[:required] @default = declare[:default] end def self.[](declare = {}) self.new(declare) end def self.inspect(obj) case obj when V8::Function then "function #{obj.name}" when V8::Array then obj.to_a.inspect when V8::Object "{#{obj.to_a.map{|k,v| "#{k}:#{Arg.inspect(v)}"}.join(', ')}}" else obj.inspect end end end class AcrobatObject def initialize(engine) @engine = engine end def self.check_method_args(args, def_args) if args.first.is_a?(V8::Object) check_method_named_args(args.first, def_args) else check_method_ordered_args(args, def_args) end end def self.check_method_named_args(object, def_args) members = object.entries.map {|k, _| k} argv = [] def_args.each do |def_arg| raise MissingArgError if def_arg.required and not members.include?(def_arg.name) if members.include?(def_arg.name) arg = object[def_arg.name] raise TypeError if def_arg.type and not arg.is_a?(def_arg.type) else arg = def_arg.default end argv.push(arg) end argv end private_class_method :check_method_named_args def self.check_method_ordered_args(args, def_args) def_args.each_with_index do |def_arg, index| raise MissingArgError if def_arg.required and index >= args.length raise TypeError if def_arg.type and not args[index].is_a?(def_arg.type) args.push(def_arg.default) if index >= args.length end args end private_class_method :check_method_ordered_args def self.acro_method(name, *def_args, &b) define_method(name) do |*args| if @engine.options[:log_method_calls] @engine.options[:console].puts( "LOG: #{self.class}.#{name}(#{args.map{|arg| Arg.inspect(arg)}.join(',')})" ) end args = AcrobatObject.check_method_args(args, def_args) self.instance_exec(*args, &b) if b end end def self.acro_method_protected(name, *def_args, &b) define_method(name) do |*args| if @engine.options[:log_method_calls] @engine.options[:console].puts( "LOG: #{self.class}.#{name}(#{args.map{|arg| arg.inspect}.join(',')})" ) end unless @engine.privileged? raise NotAllowedError, "Security settings prevent access to this property or method." end args = AcrobatObject.check_method_args(args, def_args) self.instance_exec(*args, &b) if b end end def to_s "[object #{self.class.to_s.split('::').last}]" end alias inspect to_s end class AcroTimer < AcrobatObject def initialize(engine, timeout, code, repeat) @thr = Thread.start(engine, timeout, code, repeat) do loop do sleep(timeout / 1000.0) engine.exec(code.to_s) break if not repeat end end end end class TimeOut < AcroTimer def initialize(engine, timeout, code) super(engine, timeout, code, false) end end class Interval < AcroTimer def initialize(engine, timeout, code) super(engine, timeout, code, true) end end class ReadStream < AcrobatObject def initialize(engine, data) super(engine) @data = data end acro_method 'read', Arg[name: 'nBytes', type: Numeric, required: true] do |nBytes| @data.slice!(0, nBytes).unpack("H*")[0] end end class Acrohelp < AcrobatObject; end class Global < AcrobatObject def initialize(engine) super(engine) @vars = {} end def []=(name, value) @vars[name] ||= {callbacks: []} @vars[name][:value] = value @vars[name][:callbacks].each do |callback| callback.call(value) end end def [](name) @vars[name][:value] if @vars.include?(name) end acro_method 'setPersistent', Arg[name: 'cVariable', required: true], Arg[name: 'bPersist', required: true] do |cVariable, _bPersist| raise GeneralError unless @vars.include?(cVariable) end acro_method 'subscribe', Arg[name: 'cVariable', required: true], Arg[name: 'fCallback', type: V8::Function, require: true] do |cVariable, fCallback| if @vars.include?(cVariable) @vars[cVariable][:callbacks].push(fCallback) fCallback.call(@vars[cVariable][:value]) end end end class Doc < AcrobatObject attr_reader :info attr_accessor :disclosed attr_reader :hidden attr_reader :app, :acrohelp, :global, :console, :util class Info < AcrobatObject def initialize(engine, doc) super(engine) @doc = doc end def title; @doc.title.to_s end def author; @doc.author.to_s end def subject; @doc.subject.to_s end def keywords; @doc.keywords.to_s end def creator; @doc.creator.to_s end def creationDate; @doc.creation_date.to_s end def modDate; @doc.mod_date.to_s end end def initialize(*args) engine, pdf = args # XXX: Bypass therubyracer bug #238. Temporary. super(engine) @pdf = pdf @disclosed = false @hidden = false @info = Info.new(@engine, pdf) @app = JavaScript::App.new(@engine) @acrohelp = JavaScript::Acrohelp.new(@engine) @global = JavaScript::Global.new(@engine) @console = JavaScript::Console.new(@engine) @util = JavaScript::Util.new(@engine) end ### PROPERTIES ### def numFields fields = @pdf.fields fields.size end def numPages; @pdf.pages.size end def title; @info.title end def author; @info.author end def subject; @info.subject end def keywords; @info.keywords end def creator; @info.creator end def creationDate; @info.creationDate end def modDate; @info.modDate end def metadata meta = @pdf.Catalog.Metadata (meta.data if meta.is_a?(Stream)).to_s end def filesize; @pdf.original_filesize end def path; @pdf.original_filename.to_s end def documentFileName; File.basename(self.path) end def URL; "file://#{self.path}" end def baseURL; '' end def dataObjects data_objs = [] @pdf.each_attachment do |name, file_desc| if file_desc and file_desc.EF and (f = file_desc.EF.F) data_objs.push Data.new(@engine, name, f.data.size) if f.is_a?(Stream) end end data_objs end ### METHODS ### acro_method 'closeDoc' acro_method 'getDataObject', Arg[name: 'cName', type: ::String, required: true] do |cName| file_desc = @pdf.resolve_name(Names::EMBEDDED_FILES, cName) if file_desc and file_desc.EF and (f = file_desc.EF.F) Data.new(@engine, cName, f.data.size) if f.is_a?(Stream) else raise TypeError end end acro_method 'getDataObjectContents', Arg[name: 'cName', type: ::String, required: true], Arg[name: 'bAllowAuth', default: false] do |cName, _bAllowAuth| file_desc = @pdf.resolve_name(Names::EMBEDDED_FILES, cName) if file_desc and file_desc.EF and (f = file_desc.EF.F) ReadStream.new(@engine, f.data) if f.is_a?(Stream) else raise TypeError end end acro_method 'exportDataObject', Arg[name: 'cName', type: ::String, required: true], Arg[name: 'cDIPath' ], Arg[name: 'bAllowAuth'], Arg[name: 'nLaunch'] do |cName, _cDIPath, _bAllowAuth, _nLaunch| file_desc = @pdf.resolve_name(Names::EMBEDDED_FILES, cName) if file_desc and file_desc.EF and (f = file_desc.EF.F) else raise TypeError end raise TypeError if f.nil? end acro_method 'getField', Arg[name: 'cName', type: ::Object, required: true] do |cName| field = @pdf.get_field(cName) Field.new(@engine, field) if field end acro_method 'getNthFieldName', Arg[name: 'nIndex', type: ::Object, required: true] do |nIndex| nIndex = case nIndex when false then 0 when true then 1 else @engine.parseInt.call(nIndex) end raise TypeError if (nIndex.is_a?(Float) and nIndex.nan?) or nIndex < 0 fields = @pdf.fields if fields and nIndex <= fields.size - 1 Field.new(@engine, fields.take(nIndex + 1).last).name.to_s else "" end end end class App < AcrobatObject def platform; @engine.options[:platform] end def viewerType; @engine.options[:viewerType] end def viewerVariation; @engine.options[:viewerVariation] end def viewerVersion; @engine.options[:viewerVersion] end def activeDocs; [] end ### METHODS ### acro_method 'setInterval', Arg[name: 'cExpr', required: true], Arg[name: 'nMilliseconds', type: Numeric, required: true] do |cExpr, nMilliseconds| Interval.new(@engine, nMilliseconds, cExpr) end acro_method 'setTimeOut', Arg[name: 'cExpr', required: true], Arg[name: 'nMilliseconds', type: Numeric, required: true] do |cExpr, nMilliseconds| TimeOut.new(@engine, nMilliseconds, cExpr) end acro_method 'clearInterval', Arg[name: 'oInterval', type: Interval, required: true] do |oInterval| oInterval.instance_variable_get(:@thr).terminate nil end acro_method 'clearTimeOut', Arg[name: 'oInterval', type: TimeOut, required: true] do |oInterval| oInterval.instance_variable_get(:@thr).terminate nil end acro_method_protected 'addMenuItem' acro_method_protected 'addSubMenu' acro_method 'addToolButton' acro_method_protected 'beginPriv' acro_method 'beep' acro_method_protected 'browseForDoc' acro_method_protected 'endPriv' end class Console < AcrobatObject def println(*args) raise MissingArgError unless args.length > 0 @engine.options[:console].puts(args.first.to_s) end acro_method 'show' acro_method 'clear' acro_method 'hide' end class Util < AcrobatObject acro_method 'streamFromString', Arg[name: 'cString', type: ::Object, required: true], Arg[name: 'cCharset', type: ::Object, default: 'utf-8'] do |cString, _cCharset| ReadStream.new(@engine, cString.to_s) end acro_method 'stringFromStream', Arg[name: 'oStream', type: ReadStream, required: true], Arg[name: 'cCharset', type: ::Object, default: 'utf-8'] do |oStream, _cCharset| oStream.instance_variable_get(:@data).dup end end class Field < AcrobatObject def initialize(engine, field) super(engine) @field = field end def doc; Doc.new(@field.document) end def name (@field.T.value if @field.has_key?(:T)).to_s end def value @field.V.value if @field.has_key?(:V) end def valueAsString self.value.to_s end def type return '' unless @field.key?(:FT) type_name = case @field.FT.value when PDF::Field::Type::BUTTON button_type when PDF::Field::Type::TEXT then 'text' when PDF::Field::Type::SIGNATURE then 'signature' when PDF::Field::Type::CHOICE choice_type end type_name.to_s end private def button_type return if @field.key?(:Ff) and not @field.Ff.is_a?(Integer) flags = @field.Ff.to_i if (flags & Annotation::Widget::Button::Flags::PUSHBUTTON) != 0 'button' elsif (flags & Annotation::Widget::Button::Flags::RADIO) != 0 'radiobox' else 'checkbox' end end def choice_type return if @field.key?(:Ff) and not @field.Ff.is_a?(Integer) if (@field.Ff.to_i & Annotation::Widget::Choice::Flags::COMBO) != 0 'combobox' else 'listbox' end end end class Data < AcrobatObject attr_reader :name, :path, :size attr_reader :creationDate, :modDate attr_reader :description, :MIMEType def initialize(engine, name, size, **metadata) super(engine) @name, @size = name, size @path, @creationDate, @modDate, @description, @MIMEType = metadata.values_at(:path, :creationDate, :modDate, :description, :MIMEType) end end end class JavaScript::EngineError < Origami::Error; end class JavaScript::Engine attr_reader :doc attr_reader :context attr_reader :options attr_reader :privileged_mode attr_reader :parseInt def initialize(pdf) @options = { formsVersion: 11.008, viewerVersion: 11.008, viewerType: JavaScript::Viewers::ADOBE_READER, viewerVariation: JavaScript::Viewers::ADOBE_READER, platform: JavaScript::Platforms::WINDOWS, console: STDOUT, log_method_calls: false, privileged_mode: false } @doc = JavaScript::Doc.new(self, pdf) @context = V8::Context.new(with: @doc) @privileged_mode = @options[:privileged_mode] @parseInt = V8::Context.new['parseInt'] @hooks = {} end # # Returns true if the engine is set to execute in privileged mode. # Allows execution of security protected methods. # def privileged? @privileged_mode end # # Evaluates a JavaScript code in the current context. # def exec(script) @context.eval(script) end # # Set a hook on a JavaScript method. # def hook(name, &callback) ns = name.split('.') previous = @context ns.each do |n| raise JavaScript::EngineError, "#{name} does not exist" if previous.nil? previous = previous[n] end case previous when V8::Function, UnboundMethod, nil then @context[name] = lambda do |*args| callback[previous, *args] end @hooks[name] = [previous, callback] else raise JavaScript::EngineError, "#{name} is not a function" end end # # Removes an existing hook on a JavaScript method. # def unhook(name) @context[name] = @hooks[name][0] if @hooks.has_key?(name) end # # Returns an Hash of all defined members in specified object name. # def members(obj) members = {} list = @context.eval <<-JS (function(base) { var members = []; for (var i in base) members.push([i, base[i]]); return members; })(#{obj}) JS list.each do |var| members[var[0]] = var[1] end members end # # Returns all members in the global scope. # def scope members('this') end # # Binds the V8 remote debugging agent on the specified TCP _port_. # def enable_debugger(port = 5858) V8::C::Debug.EnableAgent("Origami", port) end def debugger_break exec 'debugger' end end end module String # # Evaluates the current String as JavaScript. # def eval_js self.document.eval_js(self.value) end end class Stream # # Evaluates the current Stream as JavaScript. # def eval_js self.document.eval_js(self.data) end end class PDF # # Executes a JavaScript script in the current document context. # def eval_js(code) js_engine.exec(code) end # # Returns the JavaScript engine (if JavaScript support is present). # def js_engine @js_engine ||= PDF::JavaScript::Engine.new(self) end end rescue LoadError # # V8 unavailable. # end end