# -*- 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 'stringio'
require 'hexapdf/stream'
require 'hexapdf/content'
module HexaPDF
module Type
# Represents a form XObject of a PDF document.
#
# See: PDF2.0 s8.10
class Form < Stream
# Represents a group attribute dictionary.
#
# See: PDF2.0 s8.10.3
class Group < Dictionary
define_type :Group
define_field :Type, type: Symbol, default: type
define_field :S, type: Symbol, required: true
end
# Represents a reference dictionary which allows an XObject to refer to content in an embedded
# or linked PDF document.
#
# See: PDF2.0 s8.10.4
class Reference < Dictionary
define_type :XXReference
define_field :F, type: :Filespec, required: true
define_field :Page, type: [Integer, String], required: true
define_field :ID, type: PDFArray
end
define_type :XObject
define_field :Type, type: Symbol, default: type
define_field :Subtype, type: Symbol, required: true, default: :Form
define_field :FormType, type: Integer, default: 1, allowed_values: 1
define_field :BBox, type: Rectangle, required: true
define_field :Matrix, type: PDFArray, default: [1, 0, 0, 1, 0, 0]
define_field :Resources, type: :XXResources, version: '1.2'
define_field :Group, type: :Group, version: '1.4'
define_field :Ref, type: :XXReference, version: '1.4'
define_field :Metadata, type: Stream, version: '1.4'
define_field :PieceInfo, type: Dictionary, version: '1.3'
define_field :LastModified, type: PDFDate, version: '1.3'
define_field :StructParent, type: Integer, version: '1.3'
define_field :StructParents, type: Integer, version: '1.3'
define_field :OPI, type: Dictionary, version: '1.2'
define_field :OC, type: Dictionary, version: '1.5'
# Returns the path to the PDF file that was used when creating the form object.
#
# This value is only set when the form object was created by using the image loading
# facility (i.e. when treating a single page PDF file as image) and not when the form object
# was created in any other way (i.e. manually created or already part of a loaded PDF file).
attr_accessor :source_path
# Returns the rectangle defining the bounding box of the form.
def box
self[:BBox]
end
# Returns the width of the bounding box (see #box).
def width
box.width
end
# Returns the height of the bounding box (see #box).
def height
box.height
end
# Returns the contents of the form XObject.
#
# Note: This is the same as #stream but here for interface compatibility with Page.
def contents
stream
end
# Replaces the contents of the form XObject with the given string.
#
# This also clears the cache to avoid returning invalid objects.
#
# Note: This is the same as #stream= but here for interface compatibility with Page.
def contents=(data)
self.stream = data
clear_cache
end
# Returns the resource dictionary which is automatically created if it doesn't exist.
def resources
self[:Resources] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
type: :XXResources)
end
# Processes the content stream of the form XObject with the given processor object.
#
# The +original_resources+ argument has to be set to a page's resources if this form XObject
# is processed as part of this page.
#
# See: HexaPDF::Content::Processor
def process_contents(processor, original_resources: nil)
form = referenced_content || self
processor.resources = if form[:Resources]
form[:Resources]
elsif original_resources
original_resources
else
document.wrap({}, type: :XXResources)
end
Content::Parser.parse(form.contents, processor)
end
# Returns the canvas for the form XObject.
#
# The canvas object is cached once it is created so that its graphics state is correctly
# retained without the need for parsing its contents.
#
# If the bounding box of the form XObject doesn't have its origin at (0, 0), the canvas origin
# is translated into the bottom left corner so that this detail doesn't matter when using the
# canvas. This means that the canvas' origin is always at the bottom left corner of the
# bounding box.
#
# *Note* that a canvas can only be retrieved for initially empty form XObjects!
def canvas
cache(:canvas) do
unless stream.empty?
raise HexaPDF::Error, "Cannot create a canvas for a form XObjects with contents"
end
canvas = Content::Canvas.new(self)
if box.left != 0 || box.bottom != 0
canvas.save_graphics_state.translate(box.left, box.bottom)
end
self.stream = canvas.stream_data
set_filter(:FlateDecode)
canvas
end
end
# Returns +true+ if the Form XObject is a reference XObject.
def reference_xobject?
!self[:Ref].nil?
end
# Returns the referenced page as Form XObject, if this Form XObject is a Reference XObject and
# the referenced page is found. Otherwise returns +nil+.
def referenced_content
return unless (ref = self[:Ref])
doc = if ref[:F].embedded_file?
HexaPDF::Document.new(io: StringIO.new(ref[:F].embedded_file_stream.stream))
elsif File.exist?(ref[:F].path)
HexaPDF::Document.open(ref[:F].path)
end
return unless doc
page = ref[:Page]
if page.kind_of?(Integer)
page = doc.pages[page]
else
labels = []
doc.pages.each_labelling_range do |first_index, count, label|
count.times {|i| labels << label.construct_label(i) }
end
index = labels.index(page)
page = index && doc.pages[index]
end
return unless page
# See PDF2.0 s8.10.4.3
print_annots = page.each_annotation.select {|annot| annot.flagged?(:print) }
page.flatten_annotations(print_annots) unless print_annots.empty?
obj = page.to_form_xobject
obj[:BBox] = self[:BBox].dup
obj[:Matrix] = self[:Matrix].dup
obj
rescue
nil
end
end
end
end