# -*- 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/dictionary' module HexaPDF module Type # Represents a page label dictionary. # # A page label dictionary contains information about the numbering style, the label prefix and # the start number to construct page labels like 'A-1' or 'iii'. What is not stored is the page # to which it is applied since that is stored in a number tree referenced through the # /PageLabels entry in the document catalog. # # See HexaPDF::Document::Pages for details on how to create and manage page labels. # # Examples: # # * numbering style :decimal, prefix none, start number default value # # 1, 2, 3, 4, ... # # * numbering style :lowercase_letters, prefix 'Appendix ', start number 5 # # Appendix e, Appendix f, Appendix g, ... # # * numbering style :uppercase_roman, prefix none, start number 10 # # X, XI, XII, XIII, ... # # * numbering style :none, prefix 'Page', start number default value # # Page, Page, Page, Page, ... # # * numbering style :none, prefix none, start number default value # # "", "", "", ... (i.e. always the empty string) # # See: PDF2.0 s12.4.2, HexaPDF::Document::Pages, HexaPDF::Type::Catalog class PageLabel < Dictionary define_type :PageLabel define_field :Type, type: Symbol, default: type define_field :S, type: Symbol, allowed_values: [:D, :R, :r, :A, :a] define_field :P, type: String define_field :St, type: Integer, default: 1 # Constructs the page label for the given index which needs to be relative to the page index # of the first page in the associated labelling range. # # This method is usually not called directly but through HexaPDF::Document::Pages#page_label. def construct_label(index) label = (prefix || '').dup number = start_number + index case numbering_style when :decimal label + number.to_s when :uppercase_roman label + number_to_roman_numeral(number) when :lowercase_roman label + number_to_roman_numeral(number, lowercase: true) when :uppercase_letters label + number_to_letters(number) when :lowercase_letters label + number_to_letters(number, lowercase: true) when :none label end end NUMBERING_STYLE_MAPPING = { # :nodoc: decimal: :D, D: :D, uppercase_roman: :R, R: :R, lowercase_roman: :r, r: :r, uppercase_letters: :A, A: :A, lowercase_letters: :a, a: :a, none: nil } REVERSE_NUMBERING_STYLE_MAPPING = Hash[*NUMBERING_STYLE_MAPPING.flatten.reverse] # :nodoc: # :call-seq: # page_label.numbering_style -> numbering_style # page_label.numbering_style(value) -> numbering_style # # Returns the numbering style if no argument is given. Otherwise sets the numbering style to # the given value. # # The following numbering styles are available: # # :none:: No numbering is done; the label only consists of the prefix. # :decimal:: Decimal arabic numerals (1, 2, 3, 4, ...). # :uppercase_roman:: Uppercase roman numerals (I, II, III, IV, ...) # :lowercase_roman:: Lowercase roman numerals (i, ii, iii, iv, ...) # :uppercase_letters:: Uppercase letters (A, B, C, D, ...) # :lowercase_letters:: Lowercase letters (a, b, c, d, ...) def numbering_style(value = nil) if value self[:S] = NUMBERING_STYLE_MAPPING.fetch(value) do raise ArgumentError, "Invalid numbering style specified: #{value}" end else REVERSE_NUMBERING_STYLE_MAPPING.fetch(self[:S], :none) end end # :call-seq: # page_label.prefix -> prefix # page_label.prefix(value) -> prefix # # Returns the label prefix if no argument is given. Otherwise sets the label prefix to the # given string value. def prefix(value = nil) if value self[:P] = value else self[:P] end end # :call-seq: # page_label.start_number -> start_number # page_label.start_number(value) -> start_number # # Returns the start number if no argument is given. Otherwise sets the start number to the # given integer value. def start_number(value = nil) if value if !value.kind_of?(Integer) || value < 1 raise ArgumentError, "Start number must be an integer greater than or equal to 1" end self[:St] = value else self[:St] end end private ALPHABET = ('A'..'Z').to_a # :nodoc: # Maps the given number to uppercase (or, if +lowercase+ is +true+, lowercase) letters (e.g. 1 # -> A, 27 -> AA, 28 -> AB, ...). def number_to_letters(number, lowercase: false) result = "".dup while number > 0 number, rest = (number - 1).divmod(26) result.prepend(ALPHABET[rest]) end lowercase ? result.downcase : result end ROMAN_NUMERAL_MAPPING = { # :nodoc: 1000 => "M", 900 => "CM", 500 => "D", 400 => "CD", 100 => "C", 90 => "XC", 50 => "L", 40 => "XL", 10 => "X", 9 => "IX", 5 => "V", 4 => "IV", 1 => "I", } # Maps the given number to an uppercase (or, if +lowercase+ is +true+, lowercase) roman # numeral. def number_to_roman_numeral(number, lowercase: false) result = ROMAN_NUMERAL_MAPPING.inject("".dup) do |memo, (base, roman_numeral)| next memo if number < base quotient, number = number.divmod(base) memo << roman_numeral * quotient end lowercase ? result.downcase : result end end end end