# -*- 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-2023 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/configuration'
require 'hexapdf/dictionary'
require 'hexapdf/content/color_space'
module HexaPDF
module Type
# Represents the resources needed by a content stream.
#
# See: PDF2.0 s7.8.3
class Resources < Dictionary
define_type :XXResources
define_field :ExtGState, type: Dictionary
define_field :ColorSpace, type: Dictionary
define_field :Pattern, type: Dictionary
define_field :Shading, type: Dictionary, version: '1.3'
define_field :XObject, type: Dictionary
define_field :Font, type: Dictionary
define_field :ProcSet, type: PDFArray
define_field :Properties, type: Dictionary, version: '1.2'
# Returns the color space stored under the given name.
#
# If the color space is not found, an error is raised.
#
# Note: The color spaces :DeviceGray, :DeviceRGB and :DeviceCMYK are returned without a
# lookup since they are fixed.
def color_space(name)
case name
when :DeviceRGB, :DeviceGray, :DeviceCMYK
GlobalConfiguration.constantize('color_space.map', name).new
else
space_definition = (name == :Pattern ? name : self[:ColorSpace]&.[](name))
if space_definition.nil?
raise HexaPDF::Error, "Color space '#{name}' not found in the resources"
elsif space_definition.kind_of?(Array)
space_family = space_definition[0]
else
space_family = space_definition
space_definition = [space_definition]
end
GlobalConfiguration.constantize('color_space.map', space_family) do
HexaPDF::Content::ColorSpace::Universal
end.new(space_definition)
end
end
# Adds the color space to the resources and returns the name under which it is stored.
#
# If there already exists a color space with the same definition, it is reused. The device
# color spaces +:DeviceGray+, +:DeviceRGB+ and +:DeviceCMYK+ are never stored, their
# respective name is just returned.
def add_color_space(color_space)
family = color_space.family
return family if family == :DeviceRGB || family == :DeviceGray || family == :DeviceCMYK
definition = color_space.definition
self[:ColorSpace] = {} unless key?(:ColorSpace)
color_space_dict = self[:ColorSpace]
name, _value = color_space_dict.value.find do |_k, v|
v.map! {|item| document.deref(item) }
v == definition
end
unless name
name = create_resource_name(color_space_dict.value, 'CS')
color_space_dict[name] = definition
end
name
end
# Returns the XObject stored under the given name.
#
# If the XObject is not found, an error is raised.
def xobject(name)
object_getter(:XObject, name)
end
# Adds the XObject to the resources and returns the name under which it is stored.
#
# If there already exists a name for the given XObject, it is just returned.
def add_xobject(object)
object_setter(:XObject, 'XO', object)
end
# Returns the graphics state parameter dictionary (see Type::GraphicsStateParameter) stored
# under the given name.
#
# If the dictionary is not found, an error is raised.
def ext_gstate(name)
object_getter(:ExtGState, name)
end
# Adds the graphics state parameter dictionary to the resources and returns the name under
# which it is stored.
#
# If there already exists a name for the given dictionary, it is just returned.
def add_ext_gstate(object)
object_setter(:ExtGState, 'GS', object)
end
# Returns the font dictionary stored under the given name.
#
# If the dictionary is not found, an error is raised.
def font(name)
object_getter(:Font, name)
end
# Adds the font dictionary to the resources and returns the name under which it is stored.
#
# If there already exists a name for the given dictionary, it is just returned.
def add_font(object)
object_setter(:Font, 'F', object)
end
# Returns the property list stored under the given name.
#
# If the property list is not found, an error is raised.
def property_list(name)
object_getter(:Properties, name)
end
# Adds the property list to the resources and returns the name under which it is stored.
#
# If there already exists a name for the given property list, it is just returned.
def add_property_list(dict)
object_setter(:Properties, 'P', dict)
end
# Returns the pattern dictionary stored under the given name.
#
# If the dictionary is not found, an error is raised.
def pattern(name)
object_getter(:Pattern, name)
end
# Adds the pattern dictionary to the resources and returns the name under which it is stored.
#
# If there already exists a name for the given dictionary, it is just returned.
def add_pattern(object)
object_setter(:Pattern, 'P', object)
end
private
# Helper method for returning an entry of a subdictionary.
def object_getter(dict_name, name)
obj = self[dict_name] && self[dict_name][name]
if obj.nil?
raise HexaPDF::Error, "No object called '#{name}' stored under /#{dict_name}"
end
obj
end
# Helper method for setting an entry of a subdictionary.
def object_setter(dict_name, prefix, object)
self[dict_name] = {} unless key?(dict_name)
dict = self[dict_name]
name, _value = dict.each.find {|_, dict_obj| dict_obj == object }
unless name
name = create_resource_name(dict.value, prefix)
dict[name] = object
end
name
end
# Returns a unique name that can be used to store a resource in the given hash.
def create_resource_name(hash, prefix)
n = hash.size + 1
while true
name = :"#{prefix}#{n}"
return name unless hash.key?(name)
n += 1
end
end
# Ensures that a valid procedure set is available.
def perform_validation
super
return unless (val = self[:ProcSet])
if val.kind_of?(Symbol)
yield("Procedure set is a single value instead of an Array", true)
val = value[:ProcSet] = [val]
end
val.reject! do |name|
case name
when :PDF, :Text, :ImageB, :ImageC, :ImageI
false
else
yield("Invalid page procedure set name /#{name}", true)
true
end
end
end
end
end
end