hexapdf 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -0
- data/examples/019-acro_form.rb +12 -23
- data/examples/027-composer_optional_content.rb +1 -1
- data/examples/030-pdfa.rb +6 -6
- data/examples/031-acro_form_java_script.rb +113 -0
- data/lib/hexapdf/cli/command.rb +25 -11
- data/lib/hexapdf/cli/files.rb +31 -7
- data/lib/hexapdf/cli/form.rb +46 -38
- data/lib/hexapdf/cli/info.rb +4 -0
- data/lib/hexapdf/cli/inspect.rb +1 -1
- data/lib/hexapdf/cli/usage.rb +215 -0
- data/lib/hexapdf/cli.rb +2 -0
- data/lib/hexapdf/configuration.rb +11 -1
- data/lib/hexapdf/content/canvas.rb +2 -0
- data/lib/hexapdf/document/layout.rb +8 -1
- data/lib/hexapdf/encryption/aes.rb +13 -6
- data/lib/hexapdf/encryption/security_handler.rb +6 -4
- data/lib/hexapdf/font/cmap/parser.rb +1 -5
- data/lib/hexapdf/font/cmap.rb +22 -3
- data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
- data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
- data/lib/hexapdf/font_loader.rb +1 -0
- data/lib/hexapdf/layout/style.rb +5 -4
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
- data/lib/hexapdf/type/acro_form/button_field.rb +7 -5
- data/lib/hexapdf/type/acro_form/field.rb +14 -0
- data/lib/hexapdf/type/acro_form/form.rb +70 -8
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +649 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +90 -0
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/annotations/widget.rb +1 -1
- data/lib/hexapdf/type/resources.rb +2 -1
- data/lib/hexapdf/utils.rb +19 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/encryption/test_aes.rb +18 -8
- data/test/hexapdf/encryption/test_security_handler.rb +17 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
- data/test/hexapdf/font/cmap/test_parser.rb +5 -3
- data/test/hexapdf/font/test_cmap.rb +8 -0
- data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
- data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
- data/test/hexapdf/test_utils.rb +16 -0
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
- data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_field.rb +11 -0
- data/test/hexapdf/type/acro_form/test_form.rb +80 -0
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +327 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +62 -0
- data/test/hexapdf/type/test_resources.rb +5 -0
- metadata +8 -2
    
        data/lib/hexapdf/cli/info.rb
    CHANGED
    
    | @@ -192,6 +192,10 @@ module HexaPDF | |
| 192 192 | 
             
                        puts "WARNING: Parse error at position #{pos}: #{msg}"
         | 
| 193 193 | 
             
                        false
         | 
| 194 194 | 
             
                      end
         | 
| 195 | 
            +
                      options[:config]['encryption.on_decryption_error'] = lambda do |obj, msg|
         | 
| 196 | 
            +
                        puts "WARNING: Decryption problem for object (#{obj.oid},#{obj.gen}): #{msg}"
         | 
| 197 | 
            +
                        false
         | 
| 198 | 
            +
                      end
         | 
| 195 199 | 
             
                      options
         | 
| 196 200 | 
             
                    else
         | 
| 197 201 | 
             
                      super
         | 
    
        data/lib/hexapdf/cli/inspect.rb
    CHANGED
    
    | @@ -270,7 +270,7 @@ module HexaPDF | |
| 270 270 | 
             
                        if (rev_index = data.shift)
         | 
| 271 271 | 
             
                          rev_index = rev_index.to_i - 1
         | 
| 272 272 | 
             
                          if rev_index < 0 || rev_index >= @doc.revisions.count
         | 
| 273 | 
            -
                            $stderr.puts("Error: Invalid revision  | 
| 273 | 
            +
                            $stderr.puts("Error: Invalid revision number specified")
         | 
| 274 274 | 
             
                            next
         | 
| 275 275 | 
             
                          end
         | 
| 276 276 | 
             
                          length = 0
         | 
| @@ -0,0 +1,215 @@ | |
| 1 | 
            +
            # -*- encoding: utf-8; frozen_string_literal: true -*-
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            #--
         | 
| 4 | 
            +
            # This file is part of HexaPDF.
         | 
| 5 | 
            +
            #
         | 
| 6 | 
            +
            # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
         | 
| 7 | 
            +
            # Copyright (C) 2014-2024 Thomas Leitner
         | 
| 8 | 
            +
            #
         | 
| 9 | 
            +
            # HexaPDF is free software: you can redistribute it and/or modify it
         | 
| 10 | 
            +
            # under the terms of the GNU Affero General Public License version 3 as
         | 
| 11 | 
            +
            # published by the Free Software Foundation with the addition of the
         | 
| 12 | 
            +
            # following permission added to Section 15 as permitted in Section 7(a):
         | 
| 13 | 
            +
            # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
         | 
| 14 | 
            +
            # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
         | 
| 15 | 
            +
            # INFRINGEMENT OF THIRD PARTY RIGHTS.
         | 
| 16 | 
            +
            #
         | 
| 17 | 
            +
            # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
         | 
| 18 | 
            +
            # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
         | 
| 19 | 
            +
            # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
         | 
| 20 | 
            +
            # License for more details.
         | 
| 21 | 
            +
            #
         | 
| 22 | 
            +
            # You should have received a copy of the GNU Affero General Public License
         | 
| 23 | 
            +
            # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
         | 
| 24 | 
            +
            #
         | 
| 25 | 
            +
            # The interactive user interfaces in modified source and object code
         | 
| 26 | 
            +
            # versions of HexaPDF must display Appropriate Legal Notices, as required
         | 
| 27 | 
            +
            # under Section 5 of the GNU Affero General Public License version 3.
         | 
| 28 | 
            +
            #
         | 
| 29 | 
            +
            # In accordance with Section 7(b) of the GNU Affero General Public
         | 
| 30 | 
            +
            # License, a covered work must retain the producer line in every PDF that
         | 
| 31 | 
            +
            # is created or manipulated using HexaPDF.
         | 
| 32 | 
            +
            #
         | 
| 33 | 
            +
            # If the GNU Affero General Public License doesn't fit your need,
         | 
| 34 | 
            +
            # commercial licenses are available at <https://gettalong.at/hexapdf/>.
         | 
| 35 | 
            +
            #++
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            require 'hexapdf/cli/command'
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            module HexaPDF
         | 
| 40 | 
            +
              module CLI
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                # Shows the space usage of various parts of a PDF file.
         | 
| 43 | 
            +
                class Usage < Command
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  # Modifies the HexaPDF::PDFData class to store the size information
         | 
| 46 | 
            +
                  module PDFDataExtension
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    # Used to store the size of the indirect object.
         | 
| 49 | 
            +
                    attr_accessor :size
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    # Used to store the size of the object inside the object stream.
         | 
| 52 | 
            +
                    attr_accessor :size_in_object_stream
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  # Modifies HexaPDF::Parser to retrieve space used by indirect objects.
         | 
| 57 | 
            +
                  module ParserExtension
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    # :nodoc:
         | 
| 60 | 
            +
                    def initialize(*)
         | 
| 61 | 
            +
                      super
         | 
| 62 | 
            +
                      @last_size = nil
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    # :nodoc:
         | 
| 66 | 
            +
                    def load_object(xref_entry)
         | 
| 67 | 
            +
                      super.tap do |obj|
         | 
| 68 | 
            +
                        if xref_entry.type == :compressed
         | 
| 69 | 
            +
                          obj.data.size_in_object_stream = @last_size
         | 
| 70 | 
            +
                        elsif xref_entry.type == :in_use
         | 
| 71 | 
            +
                          obj.data.size = @last_size
         | 
| 72 | 
            +
                        end
         | 
| 73 | 
            +
                        @last_size = nil
         | 
| 74 | 
            +
                      end
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                    # :nodoc:
         | 
| 78 | 
            +
                    def parse_indirect_object(offset = nil)
         | 
| 79 | 
            +
                      real_offset = (offset ? @header_offset + offset : @tokenizer.pos)
         | 
| 80 | 
            +
                      result = super
         | 
| 81 | 
            +
                      @last_size = @tokenizer.pos - real_offset
         | 
| 82 | 
            +
                      result
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    # :nodoc:
         | 
| 86 | 
            +
                    def load_compressed_object(xref_entry)
         | 
| 87 | 
            +
                      result = super
         | 
| 88 | 
            +
                      offsets = @object_stream_data[xref_entry.objstm].instance_variable_get(:@offsets)
         | 
| 89 | 
            +
                      @last_size = if xref_entry.pos == offsets.size - 1
         | 
| 90 | 
            +
                                     @object_stream_data[xref_entry.objstm].instance_variable_get(:@tokenizer).
         | 
| 91 | 
            +
                                       io.size - offsets[xref_entry.pos]
         | 
| 92 | 
            +
                                   else
         | 
| 93 | 
            +
                                     offsets[xref_entry.pos + 1] - offsets[xref_entry.pos]
         | 
| 94 | 
            +
                                   end
         | 
| 95 | 
            +
                      result
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  def initialize #:nodoc:
         | 
| 101 | 
            +
                    super('usage', takes_commands: false)
         | 
| 102 | 
            +
                    short_desc("Show space usage of various parts of a PDF file")
         | 
| 103 | 
            +
                    long_desc(<<~EOF)
         | 
| 104 | 
            +
                      This command displays some usage statistics of the PDF file, i.e. which parts take which
         | 
| 105 | 
            +
                      approximate space in the file.
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                      Each statistic line shows the space used followed by the number of indirect objects in
         | 
| 108 | 
            +
                      parentheses. If some of those objects are in object streams, that number is displayed
         | 
| 109 | 
            +
                      after a slash.
         | 
| 110 | 
            +
                    EOF
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    options.on("--password PASSWORD", "-p", String,
         | 
| 113 | 
            +
                               "The password for decryption. Use - for reading from standard input.") do |pwd|
         | 
| 114 | 
            +
                      @password = (pwd == '-' ? read_password : pwd)
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    @password = nil
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  def execute(file) #:nodoc:
         | 
| 121 | 
            +
                    HexaPDF::Parser.prepend(ParserExtension)
         | 
| 122 | 
            +
                    HexaPDF::PDFData.prepend(PDFDataExtension)
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    with_document(file, password: @password) do |doc|
         | 
| 125 | 
            +
                      # Prepare cache of outline items
         | 
| 126 | 
            +
                      outline_item_cache = {}
         | 
| 127 | 
            +
                      if doc.catalog.key?(:Outlines)
         | 
| 128 | 
            +
                        doc.outline.each_item {|item| outline_item_cache[item] = true }
         | 
| 129 | 
            +
                        outline_item_cache[doc.outline] = true
         | 
| 130 | 
            +
                      end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                      doc.revisions.each.with_index do |rev, index|
         | 
| 133 | 
            +
                        sum = count = 0
         | 
| 134 | 
            +
                        categories = {
         | 
| 135 | 
            +
                          Content: [],
         | 
| 136 | 
            +
                          Files: [],
         | 
| 137 | 
            +
                          Fonts: [],
         | 
| 138 | 
            +
                          Images: [],
         | 
| 139 | 
            +
                          Metadata: [],
         | 
| 140 | 
            +
                          ObjectStreams: [],
         | 
| 141 | 
            +
                          Outline: [],
         | 
| 142 | 
            +
                          XObjects: [],
         | 
| 143 | 
            +
                        }
         | 
| 144 | 
            +
                        puts if index > 0
         | 
| 145 | 
            +
                        puts "Usage information for revision #{index + 1}" if doc.revisions.count > 1
         | 
| 146 | 
            +
                        rev.each do |obj|
         | 
| 147 | 
            +
                          if command_parser.verbosity_info?
         | 
| 148 | 
            +
                            print "(#{obj.oid},#{obj.gen}): #{obj.data.size.to_i}"
         | 
| 149 | 
            +
                            print " (#{obj.data.size_in_object_stream})" if obj.data.size.nil?
         | 
| 150 | 
            +
                            puts
         | 
| 151 | 
            +
                          end
         | 
| 152 | 
            +
                          next unless obj.kind_of?(HexaPDF::Dictionary)
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                          case obj.type
         | 
| 155 | 
            +
                          when :Page
         | 
| 156 | 
            +
                            Array(obj[:Contents]).each do |content|
         | 
| 157 | 
            +
                              categories[:Content] << content if object_in_rev?(content, rev)
         | 
| 158 | 
            +
                            end
         | 
| 159 | 
            +
                          when :Font
         | 
| 160 | 
            +
                            categories[:Fonts] << obj
         | 
| 161 | 
            +
                          when :FontDescriptor
         | 
| 162 | 
            +
                            categories[:Fonts] << obj
         | 
| 163 | 
            +
                            [:FontFile, :FontFile2, :FontFile3].each do |name|
         | 
| 164 | 
            +
                              categories[:Fonts] << obj[name] if object_in_rev?(obj[name], rev)
         | 
| 165 | 
            +
                            end
         | 
| 166 | 
            +
                          when :Metadata
         | 
| 167 | 
            +
                            categories[:Metadata] << obj
         | 
| 168 | 
            +
                          when :Filespec
         | 
| 169 | 
            +
                            categories[:Files] << obj
         | 
| 170 | 
            +
                            categories[:Files] << obj.embedded_file_stream if obj.embedded_file?
         | 
| 171 | 
            +
                          when :ObjStm
         | 
| 172 | 
            +
                            categories[:ObjectStreams] << obj
         | 
| 173 | 
            +
                          else
         | 
| 174 | 
            +
                            if obj[:Subtype] == :Image
         | 
| 175 | 
            +
                              categories[:Images] << obj
         | 
| 176 | 
            +
                            elsif obj[:Subtype] == :Form
         | 
| 177 | 
            +
                              categories[:XObjects] << obj
         | 
| 178 | 
            +
                            end
         | 
| 179 | 
            +
                          end
         | 
| 180 | 
            +
                          sum += obj.data.size if obj.data.size
         | 
| 181 | 
            +
                          count += 1
         | 
| 182 | 
            +
                        end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                        # Populate Outline category
         | 
| 185 | 
            +
                        outline_item_cache.reject! do |obj, _val|
         | 
| 186 | 
            +
                          object_in_rev?(obj, rev) && categories[:Outline] << obj
         | 
| 187 | 
            +
                        end
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                        categories.each do |name, data|
         | 
| 190 | 
            +
                          next if data.empty?
         | 
| 191 | 
            +
                          object_stream_count = 0
         | 
| 192 | 
            +
                          category_sum = data.sum do |o|
         | 
| 193 | 
            +
                            object_stream_count += 1 unless o.data.size
         | 
| 194 | 
            +
                            o.data.size.to_i
         | 
| 195 | 
            +
                          end
         | 
| 196 | 
            +
                          object_stream_count = object_stream_count > 0 ? "/#{object_stream_count}" : ''
         | 
| 197 | 
            +
                          size = human_readable_file_size(category_sum)
         | 
| 198 | 
            +
                          puts "#{name.to_s.ljust(15)} #{size.rjust(8)} (#{data.count}#{object_stream_count})"
         | 
| 199 | 
            +
                        end
         | 
| 200 | 
            +
                        puts "#{'Total'.ljust(15)} #{human_readable_file_size(sum).rjust(8)} (#{count})"
         | 
| 201 | 
            +
                      end
         | 
| 202 | 
            +
                    end
         | 
| 203 | 
            +
                  end
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                  private
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                  # Returns +true+ if the +obj+ is in the given +rev+.
         | 
| 208 | 
            +
                  def object_in_rev?(obj, rev)
         | 
| 209 | 
            +
                    obj && rev.object(obj) == obj
         | 
| 210 | 
            +
                  end
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                end
         | 
| 213 | 
            +
             | 
| 214 | 
            +
              end
         | 
| 215 | 
            +
            end
         | 
    
        data/lib/hexapdf/cli.rb
    CHANGED
    
    | @@ -48,6 +48,7 @@ require 'hexapdf/cli/watermark' | |
| 48 48 | 
             
            require 'hexapdf/cli/image2pdf'
         | 
| 49 49 | 
             
            require 'hexapdf/cli/form'
         | 
| 50 50 | 
             
            require 'hexapdf/cli/fonts'
         | 
| 51 | 
            +
            require 'hexapdf/cli/usage'
         | 
| 51 52 | 
             
            require 'hexapdf/version'
         | 
| 52 53 | 
             
            require 'hexapdf/document'
         | 
| 53 54 |  | 
| @@ -107,6 +108,7 @@ module HexaPDF | |
| 107 108 | 
             
                    add_command(HexaPDF::CLI::Image2PDF.new)
         | 
| 108 109 | 
             
                    add_command(HexaPDF::CLI::Form.new)
         | 
| 109 110 | 
             
                    add_command(HexaPDF::CLI::Fonts.new)
         | 
| 111 | 
            +
                    add_command(HexaPDF::CLI::Usage.new)
         | 
| 110 112 | 
             
                    add_command(CmdParse::HelpCommand.new)
         | 
| 111 113 | 
             
                    version_command = CmdParse::VersionCommand.new(add_switches: false)
         | 
| 112 114 | 
             
                    add_command(version_command)
         | 
| @@ -255,6 +255,12 @@ module HexaPDF | |
| 255 255 | 
             
              #    PDF defines a standard security handler that is implemented
         | 
| 256 256 | 
             
              #    (HexaPDF::Encryption::StandardSecurityHandler) and assigned the :Standard name.
         | 
| 257 257 | 
             
              #
         | 
| 258 | 
            +
              # encryption.on_decryption_error::
         | 
| 259 | 
            +
              #    Callback hook when HexaPDF encounters a decryption error that can potentially be ignored.
         | 
| 260 | 
            +
              #
         | 
| 261 | 
            +
              #    The value needs to be an object that responds to \#call(obj, message) and returns +true+ if
         | 
| 262 | 
            +
              #    an error should be raised.
         | 
| 263 | 
            +
              #
         | 
| 258 264 | 
             
              # encryption.sub_filter_map::
         | 
| 259 265 | 
             
              #    A mapping from a PDF name (a Symbol) to a security handler class (see
         | 
| 260 266 | 
             
              #    HexaPDF::Encryption::SecurityHandler). If the value is a String, it should contain the name
         | 
| @@ -475,7 +481,7 @@ module HexaPDF | |
| 475 481 | 
             
                                  'acro_form.fallback_font' => 'Helvetica',
         | 
| 476 482 | 
             
                                  'acro_form.on_invalid_value' => proc do |field, value|
         | 
| 477 483 | 
             
                                    raise HexaPDF::Error, "Invalid value #{value.inspect} for " \
         | 
| 478 | 
            -
                                      "#{field.concrete_field_type} field #{field.full_field_name}"
         | 
| 484 | 
            +
                                      "#{field.concrete_field_type} field named '#{field.full_field_name}'"
         | 
| 479 485 | 
             
                                  end,
         | 
| 480 486 | 
             
                                  'acro_form.text_field.default_width' => 100,
         | 
| 481 487 | 
             
                                  'debug' => false,
         | 
| @@ -488,6 +494,9 @@ module HexaPDF | |
| 488 494 | 
             
                                  'encryption.filter_map' => {
         | 
| 489 495 | 
             
                                    Standard: 'HexaPDF::Encryption::StandardSecurityHandler',
         | 
| 490 496 | 
             
                                  },
         | 
| 497 | 
            +
                                  'encryption.on_decryption_error' => proc do |_obj, _error|
         | 
| 498 | 
            +
                                    false
         | 
| 499 | 
            +
                                  end,
         | 
| 491 500 | 
             
                                  'encryption.sub_filter_map' => {},
         | 
| 492 501 | 
             
                                  'filter.map' => {
         | 
| 493 502 | 
             
                                    ASCIIHexDecode: 'HexaPDF::Filter::ASCIIHexDecode',
         | 
| @@ -523,6 +532,7 @@ module HexaPDF | |
| 523 532 | 
             
                                    'HexaPDF::FontLoader::Standard14',
         | 
| 524 533 | 
             
                                    'HexaPDF::FontLoader::FromConfiguration',
         | 
| 525 534 | 
             
                                    'HexaPDF::FontLoader::FromFile',
         | 
| 535 | 
            +
                                    'HexaPDF::FontLoader::VariantFromName',
         | 
| 526 536 | 
             
                                  ],
         | 
| 527 537 | 
             
                                  'graphic_object.arc.max_curves' => 6,
         | 
| 528 538 | 
             
                                  'graphic_object.map' => {
         | 
| @@ -2245,6 +2245,8 @@ module HexaPDF | |
| 2245 2245 | 
             
                  #   canvas.text("Times at size 10", at: [10, 150])
         | 
| 2246 2246 | 
             
                  #   canvas.font("Times", variant: :bold_italic, size: 15)
         | 
| 2247 2247 | 
             
                  #   canvas.text("Times bold+italic at size 15", at: [10, 100])
         | 
| 2248 | 
            +
                  #   canvas.font("Times bold")
         | 
| 2249 | 
            +
                  #   canvas.text("Times bold using the variant-from-name method", at: [10, 50])
         | 
| 2248 2250 | 
             
                  #
         | 
| 2249 2251 | 
             
                  # See: PDF2.0 s9.2.2, #font_size, #text
         | 
| 2250 2252 | 
             
                  def font(name = nil, size: nil, **options)
         | 
| @@ -92,6 +92,13 @@ module HexaPDF | |
| 92 92 | 
             
                #
         | 
| 93 93 | 
             
                #     style.font = ['Helvetica', variant: :bold]
         | 
| 94 94 | 
             
                #
         | 
| 95 | 
            +
                #   Helvetica in bold could also be set the conventional way:
         | 
| 96 | 
            +
                #
         | 
| 97 | 
            +
                #     style.font = 'Helvetica bold'
         | 
| 98 | 
            +
                #
         | 
| 99 | 
            +
                #   However, using an array it is also possible to specify other options when setting a font,
         | 
| 100 | 
            +
                #   like the :subset option.
         | 
| 101 | 
            +
                #
         | 
| 95 102 | 
             
                class Layout
         | 
| 96 103 |  | 
| 97 104 | 
             
                  # This class is used when a box can contain child boxes and the creation of such boxes should
         | 
| @@ -539,7 +546,7 @@ module HexaPDF | |
| 539 546 | 
             
                  #     # assign the predefined style :cell_text to all texts
         | 
| 540 547 | 
             
                  #     args[] = {style: :cell_text}
         | 
| 541 548 | 
             
                  #     # row 0 has a grey background and bold text
         | 
| 542 | 
            -
                  #     args[0] = {font:  | 
| 549 | 
            +
                  #     args[0] = {font: 'Helvetica bold', cell: {background_color: 'eee'}}
         | 
| 543 550 | 
             
                  #     # text in last column is right aligned
         | 
| 544 551 | 
             
                  #     args[0..-1, -1] = {text_align: :right}
         | 
| 545 552 | 
             
                  #   end
         | 
| @@ -112,11 +112,15 @@ module HexaPDF | |
| 112 112 | 
             
                    # It is assumed that the initialization vector is included in the first BLOCK_SIZE bytes
         | 
| 113 113 | 
             
                    # of the data. After the decryption the PKCS#5 padding is removed.
         | 
| 114 114 | 
             
                    #
         | 
| 115 | 
            +
                    # If a problem is encountered, an error message is yielded. If no block is given or if the
         | 
| 116 | 
            +
                    # supplied block returns +true+, an error is raised.
         | 
| 117 | 
            +
                    #
         | 
| 115 118 | 
             
                    # See: PDF2.0 s7.6.3
         | 
| 116 | 
            -
                    def decrypt(key, data)
         | 
| 119 | 
            +
                    def decrypt(key, data) # :yields: error_message
         | 
| 117 120 | 
             
                      return data if data.empty? # Handle invalid files with empty strings
         | 
| 118 121 | 
             
                      if data.length % BLOCK_SIZE != 0 || data.length < BLOCK_SIZE
         | 
| 119 | 
            -
                         | 
| 122 | 
            +
                        msg = "Invalid data for decryption, need 32 + 16*n bytes"
         | 
| 123 | 
            +
                        (!block_given? || yield(msg)) && raise(HexaPDF::EncryptionError, msg)
         | 
| 120 124 | 
             
                      end
         | 
| 121 125 | 
             
                      iv = data.slice!(0, BLOCK_SIZE)
         | 
| 122 126 | 
             
                      # Handle invalid files with missing padding
         | 
| @@ -126,8 +130,9 @@ module HexaPDF | |
| 126 130 | 
             
                    # Returns a Fiber object that decrypts the data from the given source fiber with the
         | 
| 127 131 | 
             
                    # +key+.
         | 
| 128 132 | 
             
                    #
         | 
| 129 | 
            -
                    # Padding  | 
| 130 | 
            -
                     | 
| 133 | 
            +
                    # Padding, the initialization vector and an optionally given block are handled like in
         | 
| 134 | 
            +
                    # #decrypt.
         | 
| 135 | 
            +
                    def decryption_fiber(key, source) # :yields: error_message
         | 
| 131 136 | 
             
                      Fiber.new do
         | 
| 132 137 | 
             
                        data = ''.b
         | 
| 133 138 | 
             
                        while data.length < BLOCK_SIZE && source.alive? && (new_data = source.resume)
         | 
| @@ -145,8 +150,10 @@ module HexaPDF | |
| 145 150 | 
             
                        end
         | 
| 146 151 |  | 
| 147 152 | 
             
                        if data.length % BLOCK_SIZE != 0
         | 
| 148 | 
            -
                           | 
| 149 | 
            -
             | 
| 153 | 
            +
                          msg = "Invalid data for decryption, need 32 + 16*n bytes"
         | 
| 154 | 
            +
                          (!block_given? || yield(msg)) && raise(HexaPDF::EncryptionError, msg)
         | 
| 155 | 
            +
                        end
         | 
| 156 | 
            +
                        if data.empty?
         | 
| 150 157 | 
             
                          data # Handle invalid files with missing padding
         | 
| 151 158 | 
             
                        else
         | 
| 152 159 | 
             
                          unpad(algorithm.process(data))
         | 
| @@ -153,17 +153,18 @@ module HexaPDF | |
| 153 153 |  | 
| 154 154 | 
             
                    # Creates a new encrypted stream data object by utilizing the given stream data object +obj+
         | 
| 155 155 | 
             
                    # as template. The arguments +key+ and +algorithm+ are used for decrypting purposes.
         | 
| 156 | 
            -
                    def initialize(obj, key, algorithm)
         | 
| 156 | 
            +
                    def initialize(obj, key, algorithm, &error_block)
         | 
| 157 157 | 
             
                      obj.instance_variables.each {|v| instance_variable_set(v, obj.instance_variable_get(v)) }
         | 
| 158 158 | 
             
                      @key = key
         | 
| 159 159 | 
             
                      @algorithm = algorithm
         | 
| 160 | 
            +
                      @error_block = error_block
         | 
| 160 161 | 
             
                    end
         | 
| 161 162 |  | 
| 162 163 | 
             
                    alias undecrypted_fiber fiber
         | 
| 163 164 |  | 
| 164 165 | 
             
                    # Returns a fiber like HexaPDF::StreamData#fiber, but one wrapped in a decrypting fiber.
         | 
| 165 166 | 
             
                    def fiber(*args)
         | 
| 166 | 
            -
                      @algorithm.decryption_fiber(@key, super(*args))
         | 
| 167 | 
            +
                      @algorithm.decryption_fiber(@key, super(*args), &@error_block)
         | 
| 167 168 | 
             
                    end
         | 
| 168 169 |  | 
| 169 170 | 
             
                  end
         | 
| @@ -268,17 +269,18 @@ module HexaPDF | |
| 268 269 | 
             
                  def decrypt(obj)
         | 
| 269 270 | 
             
                    return obj if @is_encrypt_dict[obj] || obj.type == :XRef
         | 
| 270 271 |  | 
| 272 | 
            +
                    error_proc = proc {|msg| document.config['encryption.on_decryption_error'].call(obj, msg) }
         | 
| 271 273 | 
             
                    key = object_key(obj.oid, obj.gen, string_algorithm)
         | 
| 272 274 | 
             
                    each_string_in_object(obj.value) do |str|
         | 
| 273 275 | 
             
                      next if str.empty? || (obj.type == :Sig && obj[:Contents].equal?(str))
         | 
| 274 | 
            -
                      str.replace(string_algorithm.decrypt(key, str))
         | 
| 276 | 
            +
                      str.replace(string_algorithm.decrypt(key, str, &error_proc))
         | 
| 275 277 | 
             
                    end
         | 
| 276 278 |  | 
| 277 279 | 
             
                    if obj.kind_of?(HexaPDF::Stream) && obj.raw_stream.filter[0] != :Crypt
         | 
| 278 280 | 
             
                      unless string_algorithm == stream_algorithm
         | 
| 279 281 | 
             
                        key = object_key(obj.oid, obj.gen, stream_algorithm)
         | 
| 280 282 | 
             
                      end
         | 
| 281 | 
            -
                      obj.data.stream = EncryptedStreamData.new(obj.raw_stream, key, stream_algorithm)
         | 
| 283 | 
            +
                      obj.data.stream = EncryptedStreamData.new(obj.raw_stream, key, stream_algorithm, &error_proc)
         | 
| 282 284 | 
             
                    end
         | 
| 283 285 |  | 
| 284 286 | 
             
                    obj
         | 
| @@ -163,11 +163,7 @@ module HexaPDF | |
| 163 163 | 
             
                        dest = tokenizer.next_object
         | 
| 164 164 |  | 
| 165 165 | 
             
                        if dest.kind_of?(String)
         | 
| 166 | 
            -
                           | 
| 167 | 
            -
                          code1.upto(code2) do |code|
         | 
| 168 | 
            -
                            cmap.add_unicode_mapping(code, +'' << codepoint)
         | 
| 169 | 
            -
                            codepoint += 1
         | 
| 170 | 
            -
                          end
         | 
| 166 | 
            +
                          cmap.add_unicode_range_mapping(code1, code2, dest.unpack("n*"))
         | 
| 171 167 | 
             
                        elsif dest.kind_of?(Array)
         | 
| 172 168 | 
             
                          code1.upto(code2) do |code|
         | 
| 173 169 | 
             
                            str = dest[code - code1].encode!(::Encoding::UTF_8, ::Encoding::UTF_16BE)
         | 
    
        data/lib/hexapdf/font/cmap.rb
    CHANGED
    
    | @@ -100,8 +100,10 @@ module HexaPDF | |
| 100 100 | 
             
                  # The writing mode of the CMap: 0 for horizontal, 1 for vertical writing.
         | 
| 101 101 | 
             
                  attr_accessor :wmode
         | 
| 102 102 |  | 
| 103 | 
            -
                  attr_reader :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping | 
| 104 | 
            -
             | 
| 103 | 
            +
                  attr_reader :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping,
         | 
| 104 | 
            +
                              :unicode_range_mappings # :nodoc:
         | 
| 105 | 
            +
                  protected :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping,
         | 
| 106 | 
            +
                            :unicode_range_mappings
         | 
| 105 107 |  | 
| 106 108 | 
             
                  # Creates a new CMap object.
         | 
| 107 109 | 
             
                  def initialize
         | 
| @@ -109,6 +111,7 @@ module HexaPDF | |
| 109 111 | 
             
                    @cid_mapping = {}
         | 
| 110 112 | 
             
                    @cid_range_mappings = []
         | 
| 111 113 | 
             
                    @unicode_mapping = {}
         | 
| 114 | 
            +
                    @unicode_range_mappings = []
         | 
| 112 115 | 
             
                  end
         | 
| 113 116 |  | 
| 114 117 | 
             
                  # Add all mappings from the given CMap to this CMap.
         | 
| @@ -117,6 +120,7 @@ module HexaPDF | |
| 117 120 | 
             
                    @cid_mapping.merge!(cmap.cid_mapping)
         | 
| 118 121 | 
             
                    @cid_range_mappings.concat(cmap.cid_range_mappings)
         | 
| 119 122 | 
             
                    @unicode_mapping.merge!(cmap.unicode_mapping)
         | 
| 123 | 
            +
                    @unicode_range_mappings.concat(cmap.unicode_range_mappings)
         | 
| 120 124 | 
             
                  end
         | 
| 121 125 |  | 
| 122 126 | 
             
                  # Add a codespace range using an array of ranges for the individual bytes.
         | 
| @@ -193,10 +197,25 @@ module HexaPDF | |
| 193 197 | 
             
                    @unicode_mapping[code] = string
         | 
| 194 198 | 
             
                  end
         | 
| 195 199 |  | 
| 200 | 
            +
                  # Adds a mapping from a range of character codes to strings starting with the given 16-bit
         | 
| 201 | 
            +
                  # integer values (representing the raw UTF-16BE characters).
         | 
| 202 | 
            +
                  def add_unicode_range_mapping(start_code, end_code, start_values)
         | 
| 203 | 
            +
                    @unicode_range_mappings << [start_code..end_code, start_values]
         | 
| 204 | 
            +
                  end
         | 
| 205 | 
            +
             | 
| 196 206 | 
             
                  # Returns the Unicode string in UTF-8 encoding for the given character code, or +nil+ if no
         | 
| 197 207 | 
             
                  # mapping was found.
         | 
| 198 208 | 
             
                  def to_unicode(code)
         | 
| 199 | 
            -
                    unicode_mapping | 
| 209 | 
            +
                    @unicode_mapping.fetch(code) do
         | 
| 210 | 
            +
                      @unicode_range_mappings.reverse_each do |range, start_values|
         | 
| 211 | 
            +
                        if range.cover?(code)
         | 
| 212 | 
            +
                          str = start_values[0..-2].append(start_values[-1] + code - range.first).
         | 
| 213 | 
            +
                            pack('n*').encode(::Encoding::UTF_8, ::Encoding::UTF_16BE)
         | 
| 214 | 
            +
                          return @unicode_mapping[code] = str
         | 
| 215 | 
            +
                        end
         | 
| 216 | 
            +
                      end
         | 
| 217 | 
            +
                      nil
         | 
| 218 | 
            +
                    end
         | 
| 200 219 | 
             
                  end
         | 
| 201 220 |  | 
| 202 221 | 
             
                end
         | 
| @@ -62,7 +62,7 @@ module HexaPDF | |
| 62 62 | 
             
                  #     Specifies whether the font should be subset if possible.
         | 
| 63 63 | 
             
                  #
         | 
| 64 64 | 
             
                  # This method uses the FromFile font loader behind the scenes.
         | 
| 65 | 
            -
                  def self.call(document, name, variant: :none, subset: true)
         | 
| 65 | 
            +
                  def self.call(document, name, variant: :none, subset: true, **)
         | 
| 66 66 | 
             
                    file = document.config['font.map'].dig(name, variant)
         | 
| 67 67 | 
             
                    return nil if file.nil?
         | 
| 68 68 |  | 
| @@ -0,0 +1,72 @@ | |
| 1 | 
            +
            # -*- encoding: utf-8; frozen_string_literal: true -*-
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            #--
         | 
| 4 | 
            +
            # This file is part of HexaPDF.
         | 
| 5 | 
            +
            #
         | 
| 6 | 
            +
            # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
         | 
| 7 | 
            +
            # Copyright (C) 2014-2024 Thomas Leitner
         | 
| 8 | 
            +
            #
         | 
| 9 | 
            +
            # HexaPDF is free software: you can redistribute it and/or modify it
         | 
| 10 | 
            +
            # under the terms of the GNU Affero General Public License version 3 as
         | 
| 11 | 
            +
            # published by the Free Software Foundation with the addition of the
         | 
| 12 | 
            +
            # following permission added to Section 15 as permitted in Section 7(a):
         | 
| 13 | 
            +
            # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
         | 
| 14 | 
            +
            # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
         | 
| 15 | 
            +
            # INFRINGEMENT OF THIRD PARTY RIGHTS.
         | 
| 16 | 
            +
            #
         | 
| 17 | 
            +
            # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
         | 
| 18 | 
            +
            # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
         | 
| 19 | 
            +
            # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
         | 
| 20 | 
            +
            # License for more details.
         | 
| 21 | 
            +
            #
         | 
| 22 | 
            +
            # You should have received a copy of the GNU Affero General Public License
         | 
| 23 | 
            +
            # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
         | 
| 24 | 
            +
            #
         | 
| 25 | 
            +
            # The interactive user interfaces in modified source and object code
         | 
| 26 | 
            +
            # versions of HexaPDF must display Appropriate Legal Notices, as required
         | 
| 27 | 
            +
            # under Section 5 of the GNU Affero General Public License version 3.
         | 
| 28 | 
            +
            #
         | 
| 29 | 
            +
            # In accordance with Section 7(b) of the GNU Affero General Public
         | 
| 30 | 
            +
            # License, a covered work must retain the producer line in every PDF that
         | 
| 31 | 
            +
            # is created or manipulated using HexaPDF.
         | 
| 32 | 
            +
            #
         | 
| 33 | 
            +
            # If the GNU Affero General Public License doesn't fit your need,
         | 
| 34 | 
            +
            # commercial licenses are available at <https://gettalong.at/hexapdf/>.
         | 
| 35 | 
            +
            #++
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            require 'hexapdf/font/true_type_wrapper'
         | 
| 38 | 
            +
            require 'hexapdf/font_loader/from_file'
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            module HexaPDF
         | 
| 41 | 
            +
              module FontLoader
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                # This module translates font names like 'Helvetica bold' into the arguments 'Helvetica' and
         | 
| 44 | 
            +
                # {variant: :bold}.
         | 
| 45 | 
            +
                #
         | 
| 46 | 
            +
                # This eases the usage of font names where specifying a font variant is not straight-forward.
         | 
| 47 | 
            +
                # The actual loading of the font is deferred to Document::Fonts#add.
         | 
| 48 | 
            +
                #
         | 
| 49 | 
            +
                # Note that this should be the last entry in the list of font loaders to ensure correct
         | 
| 50 | 
            +
                # operation.
         | 
| 51 | 
            +
                module VariantFromName
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  # Returns a font wrapper for the given font by splitting the font name into the font name part
         | 
| 54 | 
            +
                  # and variant selector part. If the the resulting font cannot be resolved, +nil+ is returned.
         | 
| 55 | 
            +
                  #
         | 
| 56 | 
            +
                  # A font name should have the form 'Fontname selector' where selector can be 'bold', 'italic'
         | 
| 57 | 
            +
                  # or 'bold_italic', for example 'Helvetica bold'.
         | 
| 58 | 
            +
                  #
         | 
| 59 | 
            +
                  # Note that a supplied :variant keyword argument is ignored!
         | 
| 60 | 
            +
                  def self.call(document, name, recursive_invocation: false, **options)
         | 
| 61 | 
            +
                    return if recursive_invocation
         | 
| 62 | 
            +
                    name, variant = name.split(/ (?=(?:bold|italic|bold_italic)\z)/, 2)
         | 
| 63 | 
            +
                    return if variant.nil?
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    options[:variant] = variant.to_sym
         | 
| 66 | 
            +
                    document.fonts.add(name, **options, recursive_invocation: true) rescue nil
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
            end
         | 
    
        data/lib/hexapdf/font_loader.rb
    CHANGED
    
    | @@ -88,6 +88,7 @@ module HexaPDF | |
| 88 88 | 
             
                autoload(:Standard14, 'hexapdf/font_loader/standard14')
         | 
| 89 89 | 
             
                autoload(:FromConfiguration, 'hexapdf/font_loader/from_configuration')
         | 
| 90 90 | 
             
                autoload(:FromFile, 'hexapdf/font_loader/from_file')
         | 
| 91 | 
            +
                autoload(:VariantFromName, 'hexapdf/font_loader/variant_from_name')
         | 
| 91 92 |  | 
| 92 93 | 
             
              end
         | 
| 93 94 |  | 
    
        data/lib/hexapdf/layout/style.rb
    CHANGED
    
    | @@ -614,7 +614,7 @@ module HexaPDF | |
| 614 614 | 
             
                  # The font to be used, must be set to a valid font wrapper object before it can be used.
         | 
| 615 615 | 
             
                  #
         | 
| 616 616 | 
             
                  # HexaPDF::Composer handles this property specially in that it resolves a set string or array
         | 
| 617 | 
            -
                  # to a font wrapper object before doing else with the style object.
         | 
| 617 | 
            +
                  # to a font wrapper object before doing anything else with the style object.
         | 
| 618 618 | 
             
                  #
         | 
| 619 619 | 
             
                  # This is the only style property without a default value!
         | 
| 620 620 | 
             
                  #
         | 
| @@ -624,11 +624,12 @@ module HexaPDF | |
| 624 624 | 
             
                  #
         | 
| 625 625 | 
             
                  #   #>pdf-composer100
         | 
| 626 626 | 
             
                  #   composer.text("Helvetica", font: composer.document.fonts.add("Helvetica"))
         | 
| 627 | 
            -
                  #   composer.text("Courier", font: "Courier") | 
| 627 | 
            +
                  #   composer.text("Courier", font: "Courier")
         | 
| 628 628 | 
             
                  #
         | 
| 629 629 | 
             
                  #   helvetica_bold = composer.document.fonts.add("Helvetica", variant: :bold)
         | 
| 630 630 | 
             
                  #   composer.text("Helvetica Bold", font: helvetica_bold)
         | 
| 631 | 
            -
                  #   composer.text("Courier Bold", font:  | 
| 631 | 
            +
                  #   composer.text("Courier Bold", font: "Courier bold")
         | 
| 632 | 
            +
                  #   composer.text("Courier Bold also", font: ["Courier", variant: :bold])
         | 
| 632 633 |  | 
| 633 634 | 
             
                  ##
         | 
| 634 635 | 
             
                  # :method: font_size
         | 
| @@ -1413,7 +1414,7 @@ module HexaPDF | |
| 1413 1414 | 
             
                  #   #>pdf-composer100
         | 
| 1414 1415 | 
             
                  #   composer.text("This is some longer text that does appear in two lines.")
         | 
| 1415 1416 | 
             
                  #   composer.text("This is some longer text that does not appear in two lines.",
         | 
| 1416 | 
            -
                  #                 height: 15,  | 
| 1417 | 
            +
                  #                 height: 15, overflow: :truncate)
         | 
| 1417 1418 |  | 
| 1418 1419 | 
             
                  [
         | 
| 1419 1420 | 
             
                    [:font, "raise HexaPDF::Error, 'No font set'"],
         |