=begin = File catalog.rb = Info This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2010 Guillaume DelugrÈ All right reserved. 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 class PDF # # Sets PDF extension level and version. Only supported values are "1.7" and 3. # def set_extension_level(version, level) exts = (self.Catalog.Extensions ||= Extensions.new) exts[:ADBE] = DeveloperExtension.new exts[:ADBE].BaseVersion = Name.new(version) exts[:ADBE].ExtensionLevel = level self end # # Returns the current Catalog Dictionary. # def Catalog cat = get_doc_attr(:Root) case cat when Catalog then cat when Dictionary then casted = Catalog.new(cat) casted.no, casted.generation = cat.no, cat.generation casted.set_indirect(true) casted.set_pdf(self) casted else raise InvalidPDFError, "Broken catalog" end end # # Sets the current Catalog Dictionary. # def Catalog=(cat) #unless cat.is_a?(Catalog) # raise TypeError, "Expected type Catalog, received #{cat.class}" #end cat = Catalog.new(cat) unless cat.is_a? Catalog if @revisions.last.trailer.Root delete_object(@revisions.last.trailer[:Root]) end @revisions.last.trailer.Root = self << cat end # # Sets an action to run on document opening. # _action_:: An Action Object. # def onDocumentOpen(action) unless action.is_a?(Action) or action.is_a?(Destination) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end unless self.Catalog raise InvalidPDFError, "A catalog object must exist to add this action." end self.Catalog.OpenAction = action self end # # Sets an action to run on document closing. # _action_:: A JavaScript Action Object. # def onDocumentClose(action) unless action.is_a?(Action::JavaScript) or action.is_a?(Reference) raise TypeError, "An Action::JavaScript object must be passed." end unless self.Catalog raise InvalidPDFError, "A catalog object must exist to add this action." end self.Catalog.AA ||= CatalogAdditionalActions.new self.Catalog.AA.WC = action self end # # Sets an action to run on document printing. # _action_:: A JavaScript Action Object. # def onDocumentPrint(action) unless action.is_a?(Action::JavaScript) or action.is_a?(Reference) raise TypeError, "An Action::JavaScript object must be passed." end unless self.Catalog raise InvalidPDFError, "A catalog object must exist to add this action." end self.Catalog.AA ||= CatalogAdditionalActions.new self.Catalog.AA.WP = action end # # Registers an object into a specific Names root dictionary. # _root_:: The root dictionary (see Names::Root) # _name_:: The value name. # _value_:: The value to associate with this name. # def register(root, name, value) self.Catalog.Names ||= Names.new value.set_indirect(true) unless value.is_a? Reference namesroot = self.Catalog.Names[root] if namesroot.nil? names = NameTreeNode.new(:Names => []).set_indirect(true) self.Catalog.Names[root] = names names.Names << name << value else namesroot.solve[:Names] << name << value end end def each_name(root, &b) namesroot = get_names_root(root) return if namesroot.nil? each_name_from_node(namesroot, [], &b) self end # # Retrieve the corresponding value associated with _name_ in # the specified _root_ name directory, or nil if the value does # not exist. # def resolve_name(root, name) namesroot = get_names_root(root) return nil if namesroot.nil? resolve_name_from_node(namesroot, name) end # # Returns a Hash of all names under specified _root_ name directory. # Returns nil if the directory does not exist. # def ls_names(root) namesroot = get_names_root(root) return {} if namesroot.nil? names = names_from_node(namesroot) if names.length % 2 != 0 return InvalidNameTreeError, "Odd number of elements" end Hash[*names] end private def names_from_node(node, browsed_nodes = []) #:nodoc: children = [] unless browsed_nodes.any? {|browsed| browsed.equal?(node)} browsed_nodes.push(node) if node.has_key?(:Names) # leaf node children.concat(node.Names) elsif node.has_key?(:Kids) # intermediate node node.Kids.each do |kid| children.concat(names_from_node(kid.solve, browsed_nodes)) end end end children end def resolve_name_from_node(node, name, browsed_nodes = []) #:nodoc: unless browsed_nodes.any? {|browsed| browsed.equal?(node)} browsed_nodes.push(node) if node.has_key?(:Names) # leaf node limits = node.Limits if limits min, max = limits[0].value, limits[1].value if (min..max) === name.to_str names = Hash[*node.Names] target = names[name] return target && target.solve end else names = Hash[*node.Names] target = names[name] return target && target.solve end elsif node.has_key?(:Kids) # intermediate node node.Kids.each do |kid| kid = kid.solve limits = kid.Limits min, max = limits[0].value, limits[1].value if (min..max) === name.to_str return resolve_name_from_node(kid, name, browsed_nodes) end end end end end def each_name_from_node(node, browsed_nodes = [], &b) #:nodoc: if node.has_key?(:Names) # leaf node names = Hash[*node.Names] names.each_pair do |name, value| b.call(name, value.solve) end elsif node.has_key?(:Kids) # intermediate node node.Kids.each do |kid| each_name_from_node(kid.solve, browsed_nodes, &b) end end end def get_names_root(root) #:nodoc: namedirs = self.Catalog.Names return nil if namedirs.nil? or namedirs[root].nil? namedirs[root].solve end end module PageLayout #:nodoc: SINGLE = :SinglePage ONE_COLUMN = :OneColumn TWO_COLUMN_LEFT = :TwoColumnLeft TWO_COLUMN_RIGHT = :TwoColumnRight TWO_PAGE_LEFT = :TwoPageLeft TWO_PAGE_RIGHT = :TwoPageRight end module PageMode #:nodoc: NONE = :UseNone OUTLINES = :UseOutlines THUMBS = :UseThumbs FULLSCREEN = :FullScreen OPTIONAL_CONTENT = :UseOC ATTACHMENTS = :UseAttachments end # # Class representing the Catalog Dictionary of a PDF file. # class Catalog < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Catalog, :Required => true field :Version, :Type => Name, :Version => "1.4" field :Pages, :Type => Dictionary, :Required => true field :PageLabels, :Type => Dictionary, :Version => "1.3" field :Names, :Type => Dictionary, :Version => "1.2" field :Dests, :Type => Dictionary, :Version => "1.1" field :ViewerPreferences, :Type => Dictionary, :Version => "1.2" field :PageLayout, :Type => Name, :Default => PageLayout::SINGLE field :PageMode, :Type => Name, :Default => PageMode::NONE field :Outlines, :Type => Dictionary field :Threads, :Type => Array, :Version => "1.1" field :OpenAction, :Type => [ Array, Dictionary ], :Version => "1.1" field :AA, :Type => Dictionary, :Version => "1.4" field :URI, :Type => Dictionary, :Version => "1.1" field :AcroForm, :Type => Dictionary, :Version => "1.2" field :Metadata, :Type => Stream, :Version => "1.4" field :StructTreeRoot, :Type => Dictionary, :Version => "1.3" field :MarkInfo, :Type => Dictionary, :Version => "1.4" field :Lang, :Type => String, :Version => "1.4" field :SpiderInfo, :Type => Dictionary, :Version => "1.3" field :OutputIntents, :Type => Array, :Version => "1.4" field :PieceInfo, :Type => Dictionary, :Version => "1.4" field :OCProperties, :Type => Dictionary, :Version => "1.5" field :Perms, :Type => Dictionary, :Version => "1.5" field :Legal, :Type => Dictionary, :Version => "1.5" field :Requirements, :Type => Array, :Version => "1.7" field :Collection, :Type => Dictionary, :Version => "1.7" field :NeedsRendering, :Type => Boolean, :Version => "1.7", :Default => false field :Extensions, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3 def initialize(hash = {}) set_indirect(true) super(hash) end end # # Class representing additional actions which can be associated with a Catalog. # class CatalogAdditionalActions < Dictionary include StandardObject field :WC, :Type => Dictionary, :Version => "1.4" field :WS, :Type => Dictionary, :Version => "1.4" field :DS, :Type => Dictionary, :Version => "1.4" field :WP, :Type => Dictionary, :Version => "1.4" field :DP, :Type => Dictionary, :Version => "1.4" end class InvalidNameTreeError < Exception #:nodoc: end # # Class representing the Names Dictionary of a PDF file. # class Names < Dictionary include StandardObject # # Defines constants for Names tree root entries. # module Root DESTS = :Dests AP = :AP JAVASCRIPT = :JavaScript PAGES = :Pages TEMPLATES = :Templates IDS = :IDS URLS = :URLS EMBEDDEDFILES = :EmbeddedFiles ALTERNATEPRESENTATIONS = :AlternatePresentations RENDITIONS = :Renditions XFARESOURCES = :XFAResources end field Root::DESTS, :Type => Dictionary, :Version => "1.2" field Root::AP, :Type => Dictionary, :Version => "1.3" field Root::JAVASCRIPT, :Type => Dictionary, :Version => "1.3" field Root::PAGES, :Type => Dictionary, :Version => "1.3" field Root::TEMPLATES, :Type => Dictionary, :Version => "1.3" field Root::IDS, :Type => Dictionary, :Version => "1.3" field Root::URLS, :Type => Dictionary, :Version => "1.3" field Root::EMBEDDEDFILES, :Type => Dictionary, :Version => "1.4" field Root::ALTERNATEPRESENTATIONS, :Type => Dictionary, :Version => "1.4" field Root::RENDITIONS, :Type => Dictionary, :Version => "1.5" field Root::XFARESOURCES, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3 end # # Class representing a node in a Name tree. # class NameTreeNode < Dictionary include StandardObject field :Kids, :Type => Array field :Names, :Type => Array field :Limits, :Type => Array end # # Class representing a leaf in a Name tree. # class NameLeaf < Origami::Array # # Creates a new leaf in a Name tree. # _hash_:: A hash of couples, associating a Name with an Reference. # def initialize(hash = {}) names = [] hash.each_pair do |k,v| names << k.to_o << v.to_o end super(names) end end # # Class representing the ViewerPreferences Dictionary of a PDF. # This dictionary modifies the way the UI looks when the file is opened in a viewer. # class ViewerPreferences < Dictionary include StandardObject field :HideToolbar, :Type => Boolean, :Default => false field :HideMenubar, :Type => Boolean, :Default => false field :HideWindowUI, :Type => Boolean, :Default => false field :FitWindow, :Type => Boolean, :Default => false field :CenterWindow, :Type => Boolean, :Default => false field :DisplayDocTitle, :Type => Boolean, :Default => false, :Version => "1.4" field :NonFullScreenPageMode, :Type => Name, :Default => :UseNone field :Direction, :Type => Name, :Default => :L2R field :ViewArea, :Type => Name, :Default => :CropBox, :Version => "1.4" field :ViewClip, :Type => Name, :Default => :CropBox, :Version => "1.4" field :PrintArea, :Type => Name, :Default => :CropBox, :Version => "1.4" field :PrintClip, :Type => Name, :Default => :CropBox, :Version => "1.4" field :PrintScaling, :Type => Name, :Default => :AppDefault, :Version => "1.6" field :Duplex, :Type => Name, :Default => :Simplex, :Version => "1.7" field :PickTrayByPDFSize, :Type => Boolean, :Version => "1.7" field :PrintPageRange, :Type => Array, :Version => "1.7" field :NumCopies, :Type => Integer, :Version => "1.7" field :Enforce, :Type => Array, :Version => "1.7", :ExtensionLevel => 3 end class Requirement < Dictionary include StandardObject class Handler < Dictionary include StandardObject module Type JS = :JS NOOP = :NoOp end field :Type, :Type => Name, :Default => :ReqHandler field :S, :Type => Name, :Default => Type::NOOP, :Required => true field :Script, :Type => ByteString end field :Type, :Type => Name, :Default => :Requirement field :S, :Type => Name, :Default => :EnableJavaScripts, :Version => "1.7", :Required => true field :RH, :Type => Array end # # Class representing an extension Dictionary. # class Extensions < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Extensions end # # Class representing a developer extension. # class DeveloperExtension < Dictionary include StandardObject field :Type, :Type => Name, :Default => :DeveloperExtensions field :BaseVersion, :Type => Name, :Required => true field :ExtensionLevel, :Type => Integer, :Required => true end end