class HexaPDF::Dictionary
Implementation of the PDF dictionary type.
Subclasses should use the available class method ::define_field
to create fields according to the PDF specification. This allows, among other things, automatic type checking and basic validation.
Fields defined in superclasses are inherited by their subclasses. This avoids duplicating basic field information.
See: PDF1.7 s7.3.7
Public Class Methods
Defines an entry for the field name
and returns the initalized HexaPDF::DictionaryFields::Field
object. A suitable converter module (see HexaPDF::DictionaryFields::Field#converter) is selected based on the type argument.
Options:
- type
-
The class (or an array of classes) that a value of this field must have. Here is a mapping from PDF object types to classes:
- Boolean
-
[TrueClass, FalseClass] (or use the Boolean constant)
- Integer
-
Integer
- Real
-
Float
- String
-
String (for text strings), PDFByteString (for binary strings)
- Date
-
PDFDate
- Name
-
Symbol
- Array
-
PDFArray
or Array Dictionary
-
Dictionary
(or any subclass) or Hash Stream
-
Stream
(or any subclass) - Null
-
NilClass
If an array of classes is provided, the value can be an instance of any of these classes.
If a Symbol object instead of a class is provided, the class is looked up using the 'object.type_map' global configuration option when necessary to support lazy loading.
Note that if multiple types are allowed and one of the allowed types is
Dictionary
(or a Symbol), it has to be the first in the list. Otherwise automatic type conversion functions won't work correctly. - required
-
Specifies whether this field is required.
- default
-
Specifies the default value for the field, if any.
- indirect
-
Specifies whether the value (or the values in the array value) of this field has to be an indirect object (
true
), a direct object (false
) or if it doesn't matter (unspecified ornil
). - allowed_values
-
An array of allowed values for this field.
- version
-
Specifies the minimum version of the PDF specification needed for this value.
# File lib/hexapdf/dictionary.rb, line 98 def self.define_field(name, type:, required: false, default: nil, indirect: nil, allowed_values: nil, version: '1.0') @fields ||= {} @fields[name] = Field.new(type, required: required, default: default, indirect: indirect, allowed_values: allowed_values, version: version) end
Defines the static PDF type of the class in cases where this is possible, i.e. when the class implements one specific PDF type (e.g. the HexaPDF::Type::Catalog
class).
# File lib/hexapdf/dictionary.rb, line 131 def self.define_type(type) @type = type end
Calls the block once for each field defined either in this class or in one of the ancestor classes.
# File lib/hexapdf/dictionary.rb, line 123 def self.each_field(&block) # :yields: name, data return to_enum(__method__) unless block_given? superclass.each_field(&block) if self != Dictionary && superclass != Dictionary @fields.each(&block) if defined?(@fields) end
Returns the field entry for the given field name.
The ancestor classes are also searched for such a field entry if none is found for the current class.
# File lib/hexapdf/dictionary.rb, line 109 def self.field(name) if defined?(@fields) && @fields.key?(name) @fields[name] elsif superclass.respond_to?(:field) superclass.field(name) end end
Returns the statically defined PDF type of the class.
See ::define_type
# File lib/hexapdf/dictionary.rb, line 138 def self.type defined?(@type) && @type end
Public Instance Methods
Returns the value for the given dictionary entry.
This method should be used instead of direct access to the value because it provides numerous advantages:
-
References are automatically resolved.
-
Returns the native Ruby object for values with class
HexaPDF::Object
. However, all subclasses ofHexaPDF::Object
are returned as is (it makes no sense, for example, to return the hash that describes the Catalog instead of the Catalog object). -
Automatically wraps hash values in specific subclasses of this class if field information is available (see
::define_field
). -
Returns the default value if one is specified and no value is available.
Note: If field information is available for the entry, a Hash or Array value will always be wrapped by Dictionary
or PDFArray
. Otherwise, the value will be returned as-is.
Note: This method may throw a “can't add a new key into hash during iteration” error in certain cases because it potentially modifies the underlying hash!
# File lib/hexapdf/dictionary.rb, line 163 def [](name) field = self.class.field(name) data = if key?(name) value[name] elsif field&.default? value[name] = field.default end value[name] = data = document.deref(data) if data.kind_of?(HexaPDF::Reference) if data.instance_of?(HexaPDF::Object) || (data.kind_of?(HexaPDF::Object) && data.value.nil?) data = data.value end if (result = field&.convert(data, document)) self[name] = data = result end data end
Stores the data under name in the dictionary. Name has to be a Symbol object.
If the current value for this name has the class HexaPDF::Object
(and only this, no subclasses) and the given value has not (including subclasses), the value is stored inside the HexaPDF::Object
.
# File lib/hexapdf/dictionary.rb, line 185 def []=(name, data) unless name.kind_of?(Symbol) raise ArgumentError, "Only Symbol (Name) keys are allowed to be used in PDF dictionaries" end if value[name].instance_of?(HexaPDF::Object) && !data.kind_of?(HexaPDF::Object) && !data.kind_of?(HexaPDF::Reference) value[name].value = data else value[name] = data end end
Deletes the name-value pair from the dictionary and returns the value. If such a pair does not exist, nil
is returned.
# File lib/hexapdf/dictionary.rb, line 205 def delete(name) value.delete(name) { nil } end
Calls the given block once for every name-value entry that is stored in the dictionary.
Note that the yielded value is already preprocessed like in []
.
# File lib/hexapdf/dictionary.rb, line 216 def each return to_enum(__method__) unless block_given? value.each_key {|name| yield(name, self[name]) } self end
Returns true
if the dictionary contains no entries.
# File lib/hexapdf/dictionary.rb, line 229 def empty? value.empty? end
Returns true
if the given key is present in the dictionary and not nil
.
# File lib/hexapdf/dictionary.rb, line 199 def key?(key) !value[key].nil? end
Returns a dup of the underlying hash.
# File lib/hexapdf/dictionary.rb, line 234 def to_h value.dup end
Returns, in order or availability, the value of ::type
, the /Type field or the result of Object#type
.
HexaPDF::Object#type
# File lib/hexapdf/dictionary.rb, line 224 def type self.class.type || self[:Type] || super end
Private Instance Methods
Iterates over all currently set fields and those that are required.
# File lib/hexapdf/dictionary.rb, line 262 def each_set_key_or_required_field #:yields: name, field value.keys.each {|name| yield(name, self.class.field(name)) } self.class.each_field do |name, field| yield(name, field) if field.required? && !value.key?(name) end end
Performs validation tasks based on the currently set keys and defined fields.
HexaPDF::Object#perform_validation
# File lib/hexapdf/dictionary.rb, line 270 def perform_validation(&block) super each_set_key_or_required_field do |name, field| obj = key?(name) ? self[name] : nil # The checks below need a valid field definition next if field.nil? # Check that required fields are set if field.required? && obj.nil? yield("Required field #{name} is not set", field.default?) self[name] = obj = field.default if field.default? end # Check if the document version is set high enough if field.version > document.instance_variable_get(:@version) yield("Field #{name} requires document version to be #{field.version}", true) document.version = field.version end # The checks below assume that the field has a value next if obj.nil? # Check the type of the field unless field.valid_object?(obj) msg = "Type of field #{name} is invalid: #{obj.class}" if field.type.include?(String) && obj.kind_of?(Symbol) yield(msg, true) self[name] = obj.to_s elsif field.type.include?(Symbol) && obj.kind_of?(String) yield(msg, true) self[name] = obj.intern else yield(msg, false) end end # Check the value of the field against the allowed values. if field.allowed_values && !field.allowed_values.include?(obj) yield("Field #{name} does not contain an allowed value: #{obj.inspect}") end # Check if field value needs to be (in)direct unless field.indirect.nil? obj = value[name] # we need the unwrapped object! if field.indirect && (!obj.kind_of?(HexaPDF::Object) || !obj.indirect?) yield("Field #{name} needs to be an indirect object", true) value[name] = document.add(obj) elsif !field.indirect && obj.kind_of?(HexaPDF::Object) && obj.indirect? yield("Field #{name} needs to be a direct object", true) document.delete(obj) value[name] = obj.value end end end end
Sets all required fields that have no current value but a default value to their respective default value.
# File lib/hexapdf/dictionary.rb, line 253 def set_required_fields_with_defaults self.class.each_field do |name, field| if !key?(name) && field.required? && field.default? value[name] = field.default end end end