# -*- encoding: utf-8; frozen_string_literal: true -*-
#
#--
# This file is part of HexaPDF.
#
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
# Copyright (C) 2014-2022 Thomas Leitner
#
# HexaPDF is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License version 3 as
# published by the Free Software Foundation with the addition of the
# following permission added to Section 15 as permitted in Section 7(a):
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
# INFRINGEMENT OF THIRD PARTY RIGHTS.
#
# HexaPDF 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 Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with HexaPDF. If not, see .
#
# The interactive user interfaces in modified source and object code
# versions of HexaPDF must display Appropriate Legal Notices, as required
# under Section 5 of the GNU Affero General Public License version 3.
#
# In accordance with Section 7(b) of the GNU Affero General Public
# License, a covered work must retain the producer line in every PDF that
# is created or manipulated using HexaPDF.
#
# If the GNU Affero General Public License doesn't fit your need,
# commercial licenses are available at .
#++
require 'hexapdf/error'
require 'hexapdf/object'
require 'hexapdf/dictionary_fields'
require 'hexapdf/reference'
module HexaPDF
# 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
class Dictionary < HexaPDF::Object
include DictionaryFields
# 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 or +nil+).
#
# allowed_values:: An array of allowed values for this field.
#
# version:: Specifies the minimum version of the PDF specification needed for this value.
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
# 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.
def self.field(name)
@fields&.[](name) || superclass.field(name)
end
# :call-seq:
# class.each_field {|name, data| block } -> class
# class.each_field -> Enumerator
#
# Calls the block once for each field defined either in this class or in one of the ancestor
# classes.
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
# 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).
def self.define_type(type)
@type = type
end
# Returns the statically defined PDF type of the class.
#
# See ::define_type
def self.type
defined?(@type) && @type
end
# 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 of HexaPDF::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!
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.
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
# Returns +true+ if the given key is present in the dictionary and not +nil+.
def key?(key)
!value[key].nil?
end
# Deletes the name-value pair from the dictionary and returns the value. If such a pair does
# not exist, +nil+ is returned.
def delete(name)
value.delete(name) { nil }
end
# :call-seq:
# dict.each {|name, value| block} -> dict
# dict.each -> Enumerator
#
# 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 #[].
def each
return to_enum(__method__) unless block_given?
value.each_key {|name| yield(name, self[name]) }
self
end
# Returns, in order or availability, the value of ::type, the /Type field or the result of
# Object#type.
def type
self.class.type || self[:Type] || super
end
# Returns +true+ if the dictionary contains no entries.
def empty?
value.empty?
end
# Returns a dup of the underlying hash.
def to_h
value.dup
end
private
# Ensures that the value is useful for a Dictionary and updates the object's value with
# information from the dictionary's field.
def after_data_change # :nodoc:
super
data.value ||= {}
unless value.kind_of?(Hash)
raise ArgumentError, "A PDF dictionary object needs a hash value, not a #{value.class}"
end
set_required_fields_with_defaults
end
# Sets all required fields that have no current value but a default value to their respective
# default value.
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
# Performs validation tasks based on the currently set keys and defined fields.
def perform_validation(&block)
super
self.class.each_field do |name, field|
next unless field.required? || value.key?(name)
obj = key?(name) ? self[name] : 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)
value[name] = obj.value
document.delete(obj)
end
end
end
end
end
# :nodoc:
# Forward declaration of Stream to circumvent circular require problem
class Stream < Dictionary
end
end