# -*- 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-2024 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/type/annotation' require 'hexapdf/content' require 'hexapdf/serializer' module HexaPDF module Type module Annotations # Widget annotations are used by interactive forms to represent the appearance of fields and # to manage user interactions. # # See: PDF2.0 s12.5.6.19, HexaPDF::Type::Annotation class Widget < Annotation # The dictionary used by the /MK key of the widget annotation. class AppearanceCharacteristics < Dictionary define_type :XXAppearanceCharacteristics define_field :R, type: Integer, default: 0 define_field :BC, type: PDFArray define_field :BG, type: PDFArray define_field :CA, type: String define_field :RC, type: String define_field :AC, type: String define_field :I, type: Stream define_field :RI, type: Stream define_field :IX, type: Stream define_field :IF, type: :XXIconFit define_field :TP, type: Integer, default: 0, allowed_values: [0, 1, 2, 3, 4, 5, 6] private def perform_validation #:nodoc: super if key?(:R) && self[:R] % 90 != 0 yield("Value of field R needs to be a multiple of 90") end end end define_field :Subtype, type: Symbol, required: true, default: :Widget define_field :H, type: Symbol, allowed_values: [:N, :I, :O, :P, :T] define_field :MK, type: :XXAppearanceCharacteristics define_field :A, type: Dictionary, version: '1.1' define_field :AA, type: Dictionary, version: '1.2' define_field :BS, type: :Border, version: '1.2' define_field :Parent, type: Dictionary # Returns the AcroForm field object to which this widget annotation belongs. # # Since a widget and a field can share the same dictionary object, the returned object is # often just the widget re-wrapped in the correct field class. def form_field field = if key?(:Parent) && (tmp = document.wrap(self[:Parent], type: :XXAcroFormField)).terminal_field? tmp else document.wrap(self, type: :XXAcroFormField) end document.wrap(field, type: :XXAcroFormField, subtype: field[:FT]) end # :call-seq: # widget.background_color => background_color or nil # widget.background_color(*color) => widget # # Returns the current background color as device color object, or +nil+ if no background # color is set, when no argument is given. Otherwise sets the background color using the # +color+ argument and returns self. # # See HexaPDF::Content::ColorSpace.device_color_from_specification for information on the # allowed arguments. def background_color(*color) if color.empty? components = self[:MK]&.[](:BG) if components && !components.empty? Content::ColorSpace.prenormalized_device_color(components) end else color = Content::ColorSpace.device_color_from_specification(color) (self[:MK] ||= {})[:BG] = color.components self end end # Describes the border of an annotation. # # The +color+ property is either +nil+ if the border is transparent or else a device color # object - see HexaPDF::Content::ColorSpace. # # The +style+ property can be one of the following: # # :solid:: Solid line. # :beveled:: Embossed rectangle seemingly raised above the surface of the page. # :inset:: Engraved rectangle receeding into the page. # :underlined:: Underlined, i.e. only the bottom border is draw. # Array: Dash array describing how to dash the line. BorderStyle = Struct.new(:width, :color, :style, :horizontal_corner_radius, :vertical_corner_radius) # :call-seq: # widget.border_style => border_style # widget.border_style(color: 0, width: 1, style: :solid) => widget # # Returns a BorderStyle instance representing the border style of the widget when no # argument is given. Otherwise sets the border style of the widget and returns self. # # When setting a border style, arguments that are not provided will use the default: a # border with a solid, black, 1pt wide line. This also means that multiple invocations will # reset *all* prior values. # # +color+:: The color of the border. See # HexaPDF::Content::ColorSpace.device_color_from_specification for information on # the allowed arguments. # # If the special value +:transparent+ is used when setting the color, a # transparent is used. A transparent border will return a +nil+ value when getting # the border color. # # +width+:: The width of the border. If set to 0, no border is shown. # # +style+:: Defines how the border is drawn. can be one of the following: # # +:solid+:: Draws a solid border. # +:beveled+:: Draws a beveled border. # +:inset+:: Draws an inset border. # +:underlined+:: Draws only the bottom border. # Array:: An array specifying a line dash pattern (see # HexaPDF::Content::LineDashPattern) def border_style(color: nil, width: nil, style: nil) if color || width || style color = if color == :transparent [] else Content::ColorSpace.device_color_from_specification(color || 0).components end width ||= 1 style ||= :solid (self[:MK] ||= {})[:BC] = color bs = self[:BS] = {W: width} case style when :solid then bs[:S] = :S when :beveled then bs[:S] = :B when :inset then bs[:S] = :I when :underlined then bs[:S] = :U when Array bs[:S] = :D bs[:D] = style else raise ArgumentError, "Unknown value #{style} for style argument" end self else result = BorderStyle.new(1, nil, :solid, 0, 0) if (ac = self[:MK]) && (bc = ac[:BC]) && !bc.empty? result.color = Content::ColorSpace.prenormalized_device_color(bc.value) end if (bs = self[:BS]) result.width = bs[:W] if bs.key?(:W) result.style = case bs[:S] when :S then :solid when :B then :beveled when :I then :inset when :U then :underlined when :D then bs[:D].value else :solid end elsif key?(:Border) border = self[:Border] result.horizontal_corner_radius = border[0] result.vertical_corner_radius = border[1] result.width = border[2] result.style = border[3] if border[3] end result end end # Describes the marker style of a check box or radio button widget. class MarkerStyle # The kind of marker that is shown inside the widget. Can either be one of the symbols # +:check+, +:circle+, +:cross+, +:diamond+, +:square+ or +:star+, or a one character # string. The latter is interpreted using the ZapfDingbats font. # # If an empty string is set, it is treated as if +nil+ was set, i.e. it shows the default # marker for the field type. attr_reader :style # The size of the marker in PDF points that is shown inside the widget. The special value # 0 means that the marker should be auto-sized based on the widget's rectangle. attr_reader :size # A device color object representing the color of the marker - see # HexaPDF::Content::ColorSpace. attr_reader :color # Creates a new instance with the given values. def initialize(style, size, color) @style = style @size = size @color = color end end # :call-seq: # widget.marker_style => marker_style # widget.marker_style(style: nil, size: nil, color: nil) => widget # # Returns a MarkerStyle instance representing the marker style of the widget when no # argument is given. Otherwise sets the button marker style of the widget and returns self. # # This method returns valid information only for check boxes and radio buttons! # # When setting a marker style, arguments that are not provided will use the default: a black # auto-sized checkmark (i.e. :check for for check boxes) or circle (:circle for radio # buttons). This also means that multiple invocations will reset *all* prior values. # # Note: The marker is called "normal caption" in the PDF 1.7 spec and the /CA entry of the # associated appearance characteristics dictionary. The marker size and color are set using # the /DA key on the widget (although /DA is not defined for widget, this is how Acrobat # does it). # # See: PDF2.0 s12.5.6.19 and s12.7.4.3 def marker_style(style: nil, size: nil, color: nil) field = form_field if style || size || color style ||= (field.check_box? ? :check : :cicrle) size ||= 0 color = Content::ColorSpace.device_color_from_specification(color || 0) serialized_color = Content::ColorSpace.serialize_device_color(color) self[:MK] ||= {} self[:MK][:CA] = case style when :check then '4' when :circle then 'l' when :cross then '8' when :diamond then 'u' when :square then 'n' when :star then 'H' when String then style else raise ArgumentError, "Unknown value #{style} for argument 'style'" end self[:DA] = "/ZaDb #{size} Tf #{serialized_color}".strip else style = case self[:MK]&.[](:CA) when '4' then :check when 'l' then :circle when '8' then :cross when 'u' then :diamond when 'n' then :square when 'H' then :star when String then self[:MK][:CA] else if field.check_box? :check else :circle end end size = 0 color = HexaPDF::Content::ColorSpace.prenormalized_device_color([0]) if (da = self[:DA] || field[:DA]) _, size, color = HexaPDF::Type::AcroForm::VariableTextField.parse_appearance_string(da) end MarkerStyle.new(style, size, color) end end end end end end