hexapdf 0.19.0 → 0.19.1

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/lib/hexapdf/content/graphics_state.rb +24 -5
  4. data/lib/hexapdf/document/signatures.rb +221 -0
  5. data/lib/hexapdf/layout/style.rb +2 -1
  6. data/lib/hexapdf/type/font.rb +5 -0
  7. data/lib/hexapdf/type/font_type3.rb +20 -0
  8. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +125 -0
  9. data/lib/hexapdf/version.rb +1 -1
  10. data/test/hexapdf/content/test_graphics_state.rb +9 -1
  11. data/test/hexapdf/content/test_operator.rb +8 -3
  12. data/test/hexapdf/layout/test_style.rb +11 -0
  13. data/test/hexapdf/test_writer.rb +2 -2
  14. data/test/hexapdf/type/test_font.rb +4 -0
  15. data/test/hexapdf/type/test_font_type3.rb +16 -1
  16. metadata +4 -32
  17. data/test/data/cert/create.sh +0 -171
  18. data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF39.pem +0 -119
  19. data/test/data/cert/root-ca/certs/84E66B6F4C359E741C0AFA014790DF3A.pem +0 -125
  20. data/test/data/cert/root-ca/db/crlnumber +0 -1
  21. data/test/data/cert/root-ca/db/index +0 -2
  22. data/test/data/cert/root-ca/db/index.attr +0 -1
  23. data/test/data/cert/root-ca/db/index.attr.old +0 -1
  24. data/test/data/cert/root-ca/db/index.old +0 -1
  25. data/test/data/cert/root-ca/db/serial +0 -1
  26. data/test/data/cert/root-ca/db/serial.old +0 -1
  27. data/test/data/cert/root-ca/private/root-ca.key +0 -52
  28. data/test/data/cert/root-ca/root-ca.conf +0 -65
  29. data/test/data/cert/root-ca/root-ca.crt +0 -119
  30. data/test/data/cert/root-ca/root-ca.csr +0 -28
  31. data/test/data/cert/signature-1-pkcs7-detached.pdf +0 -182
  32. data/test/data/cert/sub-ca/certs/453FF080E3EDCD6A388D5368DFC320D9.pem +0 -125
  33. data/test/data/cert/sub-ca/db/crlnumber +0 -1
  34. data/test/data/cert/sub-ca/db/index +0 -1
  35. data/test/data/cert/sub-ca/db/index.attr +0 -1
  36. data/test/data/cert/sub-ca/db/index.old +0 -0
  37. data/test/data/cert/sub-ca/db/serial +0 -1
  38. data/test/data/cert/sub-ca/db/serial.old +0 -1
  39. data/test/data/cert/sub-ca/private/signing.key +0 -52
  40. data/test/data/cert/sub-ca/private/sub-ca.key +0 -52
  41. data/test/data/cert/sub-ca/signing.crt +0 -125
  42. data/test/data/cert/sub-ca/signing.csr +0 -28
  43. data/test/data/cert/sub-ca/signing.p12 +0 -0
  44. data/test/data/cert/sub-ca/sub-ca.conf +0 -65
  45. data/test/data/cert/sub-ca/sub-ca.crt +0 -125
  46. data/test/data/cert/sub-ca/sub-ca.csr +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55ddbf5c737ab446c5793660957e64aaf6f8872c6bb1c4a76ba39a87193b951f
4
- data.tar.gz: eb29fbb41cbb20c060c43cc80406bd4b11fa36b0d86a95daa9d1093d060ab89e
3
+ metadata.gz: 496c87d6cffcfde7b155fbfcdd78673fb34931d4d0fd997bcee934decfba175d
4
+ data.tar.gz: ce72598c9d33735c6ea4738c072b1f00202dc7bac24577c9c27a062add38ea8e
5
5
  SHA512:
6
- metadata.gz: 28368cd415cd9adf28050df8812a0cda999b8140fcba4a9f307d7e37f6dc0254d99abf5173a81a1b2b59f9f6a658a46f6998d7834b93ffc19840d578e507ce92
7
- data.tar.gz: b25e409653a4bd9359f3a39c2920187261a43151942c4120d2bd818886dbf236f77483a42ca7a7fb39ffbcdfc7da8cf9d7a87609a4f4af514cf7f93f495eb5ed
6
+ metadata.gz: cee5a6f86084490ba5602c40ec48945b9a3c0bdecb22fe03e2d7b8b6ad02c791900f9fd7354557a9ac3dfdebb4a8d95c7335fc329c0054156082853f33717144
7
+ data.tar.gz: eaed67cce68e703eddbbb357e790a9f0bc62083ff8ba0b0856aa360d499f75e0e022b1ccd41ebcdd53e2fe269a93b5f587c47db41311c3f6a0beb9ed293e02cd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.19.1 - 2021-12-12
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Type::FontType3#bounding_box] to fix content stream processing error
6
+
7
+ ### Fixed
8
+
9
+ * Calculation of scaled font size for [HexaPDF::Content::GraphicsState] and
10
+ [HexaPDF::Layout::Style] when Type3 fonts are used
11
+
12
+
1
13
  ## 0.19.0 - 2021-11-24
2
14
 
3
15
  ### Added
@@ -512,7 +512,7 @@ module HexaPDF
512
512
  attr_accessor :leading
513
513
 
514
514
  # The font for the text.
515
- attr_accessor :font
515
+ attr_reader :font
516
516
 
517
517
  # The font size.
518
518
  attr_reader :font_size
@@ -552,9 +552,11 @@ module HexaPDF
552
552
 
553
553
  # The scaled font size used in glyph displacement calculations.
554
554
  #
555
- # This returns the font size divided by 1000 multiplied by #scaled_horizontal_scaling.
555
+ # This returns the font size multiplied by the scaling factor from glyph space to text space
556
+ # (0.001 for all fonts except Type3 fonts or the scaling specified in /FontMatrix for Type3
557
+ # fonts) and multiplied by #scaled_horizontal_scaling.
556
558
  #
557
- # See PDF1.7 s9.4.4
559
+ # See PDF1.7 s9.4.4, HexaPDF::Type::FontType3
558
560
  attr_reader :scaled_font_size
559
561
 
560
562
  # The scaled horizontal scaling used in glyph displacement calculations.
@@ -665,6 +667,15 @@ module HexaPDF
665
667
  self.fill_color = color_space.default_color
666
668
  end
667
669
 
670
+ ##
671
+ # :attr_writer: font
672
+ #
673
+ # Sets the font and updates the glyph space to text space scaling.
674
+ def font=(font)
675
+ @font = font
676
+ update_scaled_font_size
677
+ end
678
+
668
679
  ##
669
680
  # :attr_writer: character_spacing
670
681
  #
@@ -689,7 +700,7 @@ module HexaPDF
689
700
  # Sets the font size and updates the scaled font size.
690
701
  def font_size=(size)
691
702
  @font_size = size
692
- @scaled_font_size = size / 1000.0 * @scaled_horizontal_scaling
703
+ update_scaled_font_size
693
704
  end
694
705
 
695
706
  ##
@@ -702,7 +713,15 @@ module HexaPDF
702
713
  @scaled_horizontal_scaling = scaling / 100.0
703
714
  @scaled_character_spacing = @character_spacing * @scaled_horizontal_scaling
704
715
  @scaled_word_spacing = @word_spacing * @scaled_horizontal_scaling
705
- @scaled_font_size = @font_size / 1000.0 * @scaled_horizontal_scaling
716
+ update_scaled_font_size
717
+ end
718
+
719
+ private
720
+
721
+ # Updates the cached value for the scaled font size.
722
+ def update_scaled_font_size
723
+ @scaled_font_size = @font_size * (@font&.glyph_scaling_factor || 0.001) *
724
+ @scaled_horizontal_scaling
706
725
  end
707
726
 
708
727
  end
@@ -0,0 +1,221 @@
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-2021 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 'openssl'
38
+
39
+ module HexaPDF
40
+ class Document
41
+
42
+ # This class provides methods for interacting with digital signatures of a PDF file.
43
+ class Signatures
44
+
45
+ # This is the default signing handler which provides the ability to sign a document with a
46
+ # provided certificate using the adb.pkcs7.detached algorithm.
47
+ class DefaultHandler
48
+
49
+ # Creates a new DefaultHandler with the given signing certificate, the associated signing
50
+ # key and an optional array of certificates that should also be present in the signature.
51
+ def initialize(certificate, key, certificate_chain = [])
52
+ @certificate = certificate
53
+ @key = key
54
+ @certificate_chain = certificate_chain
55
+ end
56
+
57
+ # Returns the name to be set on the /Filter key when using this signing handler.
58
+ def filter_name
59
+ :"Adobe.PPKLite"
60
+ end
61
+
62
+ # Returns the name to be set on the /SubFilter key when using this signing handler.
63
+ def sub_filter_name
64
+ :"adbe.pkcs7.detached"
65
+ end
66
+
67
+ # Returns the size of the signature that would be created.
68
+ def signature_size
69
+ sign("").size
70
+ end
71
+
72
+ # Returns the DER serialized OpenSSL::PKCS7 structure containing the signature for the given
73
+ # data.
74
+ def sign(data)
75
+ OpenSSL::PKCS7.sign(@certificate, @key, data, @certificate_chain,
76
+ OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
77
+ end
78
+
79
+ end
80
+
81
+ include Enumerable
82
+
83
+ # Creates a new Signatures object for the given PDF document.
84
+ def initialize(document)
85
+ @document = document
86
+ end
87
+
88
+ # Adds a signature to the document and returns the corresponding signature object.
89
+ #
90
+ # This method will add a new signature to the document and write the updated document to the
91
+ # given file or IO stream. Afterwards the document can't be modified anymore and still retain
92
+ # a correct digital signature; create a new document based on the file or IO stream instead.
93
+ #
94
+ # +signature+::
95
+ # Can either be a signature object or +nil+. Providing a signature object provides for
96
+ # more control, e.g.:
97
+ #
98
+ # * Setting values for optional fields like /Reason and /Location.
99
+ # * Indirectly specifying which signature field should be used.
100
+ #
101
+ # If the +signature+ is not associated with an AcroForm signature field, a new signature
102
+ # field is created and added to the main AcroForm object, creating that if necessary.
103
+ #
104
+ # If the associated signature field doesn't have a widget, a non-visible one is created on
105
+ # the first page.
106
+ #
107
+ # +handler+::
108
+ # The signature handler that provides the necessary methods for signing, see
109
+ # DefaultHandler.
110
+ #
111
+ # +write_options+::
112
+ # These options will be passed on to the HexaPDF::Document#write command. Note that
113
+ # +incremental+ will be automatically set if signing an already existing file.
114
+ def add(file_or_io, handler, signature: nil, **write_options)
115
+ signature ||= @document.add({Type: :Sig})
116
+ signature[:Filter] = handler.filter_name
117
+ signature[:SubFilter] = handler.sub_filter_name
118
+ signature[:ByteRange] = [0, 1_000_000_000_000, 1_000_000_000_000, 1_000_000_000_000]
119
+ signature[:Contents] = '00' * handler.signature_size # twice the size due to hex encoding
120
+
121
+ # Prepare signature field
122
+ form = @document.acro_form(create: true)
123
+ form.signature_flag(:signatures_exist)
124
+
125
+ signature_field = each.find {|value| value == signature }
126
+ unless signature_field
127
+ signature_field = form.create_signature_field(generate_field_name)
128
+ signature_field.field_value = signature
129
+ end
130
+
131
+ if signature_field.each_widget.to_a.empty?
132
+ signature_field.create_widget(@document.pages[0], Rect: [0, 0, 0, 0])
133
+ end
134
+
135
+ io = if file_or_io.kind_of?(String)
136
+ File.open(file_or_io, 'w+')
137
+ else
138
+ file_or_io
139
+ end
140
+
141
+ # Save the current state so that we can determine the correct /ByteRange value and set the
142
+ # values
143
+ section = @document.write(io, incremental: true, **write_options)
144
+ data = section.map {|oid, _gen, entry| [entry.pos.to_i, oid] }.sort
145
+ index = data.index {|_pos, oid| oid == signature.oid }
146
+ signature_offset = data[index][0]
147
+ signature_length = data[index + 1][0] - data[index][0]
148
+ io.pos = signature_offset
149
+ signature_data = io.read(signature_length)
150
+
151
+ io.rewind
152
+ file_data = io.read
153
+
154
+ # Calculate the offsets for the /ByteRange
155
+ contents_offset = signature_offset + signature_data.index('Contents(') + 8
156
+ offset2 = contents_offset + signature[:Contents].size + 2 # +2 because of the needed < and >
157
+ length2 = file_data.size - offset2
158
+ signature[:ByteRange] = [0, contents_offset, offset2, length2]
159
+
160
+ # Set the correct /ByteRange value
161
+ signature_data.sub!(/ByteRange\[0 1000000000000 1000000000000 1000000000000\]/) do |match|
162
+ length = match.size
163
+ result = "ByteRange[0 #{contents_offset} #{offset2} #{length2}]"
164
+ result.ljust(length)
165
+ end
166
+
167
+ # Now everything besides the /Contents value is correct, so we can read the contents for
168
+ # signing
169
+ file_data[signature_offset, signature_length] = signature_data
170
+ signed_contents = file_data[0, contents_offset] << file_data[offset2, length2]
171
+ signature[:Contents] = handler.sign(signed_contents)
172
+
173
+ # Set the correct /Contents value as hexstring
174
+ signature_data.sub!(/Contents\(0+\)/) do |match|
175
+ length = match.size
176
+ result = "Contents<#{signature[:Contents].unpack1('H*')}"
177
+ "#{result.ljust(length - 1, '0')}>"
178
+ end
179
+
180
+ io.pos = signature_offset
181
+ io.write(signature_data)
182
+
183
+ signature
184
+ ensure
185
+ io.close if io && io != file_or_io
186
+ end
187
+
188
+ # :call-seq:
189
+ # signatures.each {|signature| block } -> signatures
190
+ # signatures.each -> Enumerator
191
+ #
192
+ # Iterates over all signatures in the order they are found.
193
+ def each
194
+ return to_enum(__method__) unless block_given?
195
+
196
+ return [] unless (form = @document.acro_form)
197
+ form.each_field do |field|
198
+ yield(field.field_value) if field.field_type == :Sig && field.field_value
199
+ end
200
+ end
201
+
202
+ # Returns the number of signatures in the PDF document. May be zero if the document has no
203
+ # signatures.
204
+ def count
205
+ each.to_a.size
206
+ end
207
+
208
+ private
209
+
210
+ # Generates a field name for a signature field.
211
+ def generate_field_name
212
+ index = (@document.acro_form.each_field.
213
+ map {|field| field.full_field_name.scan(/\ASignature(\d+)/).first&.first.to_i }.
214
+ max || 0) + 1
215
+ "Signature#{index}"
216
+ end
217
+
218
+ end
219
+
220
+ end
221
+ end
@@ -1069,7 +1069,8 @@ module HexaPDF
1069
1069
 
1070
1070
  # The font size scaled appropriately.
1071
1071
  def scaled_font_size
1072
- @scaled_font_size ||= calculated_font_size / 1000.0 * scaled_horizontal_scaling
1072
+ @scaled_font_size ||= calculated_font_size * font.pdf_object.glyph_scaling_factor *
1073
+ scaled_horizontal_scaling
1073
1074
  end
1074
1075
 
1075
1076
  # The character spacing scaled appropriately.
@@ -98,6 +98,11 @@ module HexaPDF
98
98
  embedded?
99
99
  end
100
100
 
101
+ # Returns the glyph scaling factor for transforming from glyph space to text space.
102
+ def glyph_scaling_factor
103
+ 0.001
104
+ end
105
+
101
106
  private
102
107
 
103
108
  # Parses and caches the ToUnicode CMap.
@@ -41,6 +41,10 @@ module HexaPDF
41
41
 
42
42
  # Represents a Type 3 font.
43
43
  #
44
+ # Note: We assume the /FontMatrix is only used for scaling, i.e. of the form [x 0 0 +/-x 0 0].
45
+ # If it is of a different form, things won't work correctly. This will be handled once such a
46
+ # case is found.
47
+ #
44
48
  # See: PDF1.7 s9.6.5
45
49
  class FontType3 < FontSimple
46
50
 
@@ -51,6 +55,22 @@ module HexaPDF
51
55
  define_field :CharProcs, type: Dictionary, required: true
52
56
  define_field :Resources, type: Dictionary, version: '1.2'
53
57
 
58
+ # Returns the bounding box of the font.
59
+ def bounding_box
60
+ matrix = self[:FontMatrix]
61
+ bbox = self[:FontBBox].value
62
+ if matrix[3] < 0 # Some writers invert the y-axis
63
+ bbox = bbox.dup
64
+ bbox[1], bbox[3] = -bbox[3], -bbox[1]
65
+ end
66
+ bbox
67
+ end
68
+
69
+ # Returns the glyph scaling factor for transforming from glyph space to text space.
70
+ def glyph_scaling_factor
71
+ self[:FontMatrix][0]
72
+ end
73
+
54
74
  private
55
75
 
56
76
  def perform_validation
@@ -0,0 +1,125 @@
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-2021 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 'openssl'
38
+ require 'hexapdf/type/signature'
39
+
40
+ module HexaPDF
41
+ module Type
42
+ class Signature
43
+
44
+ # The signature handler for the adbe.pkcs7.detached sub-filter.
45
+ class AdbePkcs7Detached < Handler
46
+
47
+ # Creates a new signature handler for the given signature dictionary.
48
+ def initialize(signature_dict)
49
+ super
50
+ @pkcs7 = OpenSSL::PKCS7.new(signature_dict.contents)
51
+ end
52
+
53
+ # Returns the common name of the signer.
54
+ def signer_name
55
+ signer_certificate.subject.to_a.assoc("CN")&.[](1) || super
56
+ end
57
+
58
+ # Returns the time of signing.
59
+ def signing_time
60
+ signer_info.signed_time rescue super
61
+ end
62
+
63
+ # Returns the certificate chain.
64
+ def certificate_chain
65
+ @pkcs7.certificates
66
+ end
67
+
68
+ # Returns the signer certificate (an instance of OpenSSL::X509::Certificate).
69
+ def signer_certificate
70
+ info = signer_info
71
+ certificate_chain.find {|cert| cert.issuer == info.issuer && cert.serial == info.serial }
72
+ end
73
+
74
+ # Returns the signer information object (an instance of OpenSSL::PKCS7::SignerInfo).
75
+ def signer_info
76
+ @pkcs7.signers.first
77
+ end
78
+
79
+ # Verifies the signature using the provided OpenSSL::X509::Store object.
80
+ def verify(store, allow_self_signed: false)
81
+ result = VerificationResult.new
82
+
83
+ signer_info = self.signer_info
84
+ signer_certificate = self.signer_certificate
85
+ certificate_chain = self.certificate_chain
86
+
87
+ if certificate_chain.empty?
88
+ result.log(:error, "No certificates found in signature")
89
+ return result
90
+ end
91
+
92
+ if @pkcs7.signers.size != 1
93
+ result.log(:error, "Exactly one signer needed, found #{@pkcs7.signers.size}")
94
+ end
95
+
96
+ unless signer_certificate
97
+ result.log(:error, "Signer serial=#{signer_info.serial} issuer=#{signer_info.issuer} " \
98
+ "not found in certificates stored in PKCS7 object")
99
+ return result
100
+ end
101
+
102
+ key_usage = signer_certificate.extensions.find {|ext| ext.oid == 'keyUsage' }
103
+ unless key_usage && key_usage.value.split(', ').include?("Digital Signature")
104
+ result.log(:error, "Certificate key usage is missing 'Digital Signature'")
105
+ end
106
+
107
+ verify_signing_time(result)
108
+
109
+ store.verify_callback = store_verification_callback(result,
110
+ allow_self_signed: allow_self_signed)
111
+ if @pkcs7.verify(certificate_chain, store, signature_dict.signed_data,
112
+ OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
113
+ result.log(:info, "Signature valid")
114
+ else
115
+ result.log(:error, "Signature verification failed")
116
+ end
117
+
118
+ result
119
+ end
120
+
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.19.0'
40
+ VERSION = '0.19.1'
41
41
 
42
42
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'test_helper'
4
4
  require 'hexapdf/content/graphics_state'
5
+ require 'ostruct'
5
6
 
6
7
  # Dummy class used as wrapper so that constant lookup works correctly
7
8
  class GraphicsStateWrapper < Minitest::Spec
@@ -146,6 +147,13 @@ class GraphicsStateWrapper < Minitest::Spec
146
147
  it "fails when restoring the graphics state if the stack is empty" do
147
148
  assert_raises(HexaPDF::Error) { @gs.restore }
148
149
  end
149
- end
150
150
 
151
+ it "uses the correct glyph to text space scaling" do
152
+ font = OpenStruct.new
153
+ font.glyph_scaling_factor = 0.002
154
+ @gs.font = font
155
+ @gs.font_size = 10
156
+ assert_equal(0.02, @gs.scaled_font_size)
157
+ end
158
+ end
151
159
  end
@@ -4,6 +4,7 @@ require 'test_helper'
4
4
  require 'hexapdf/content/operator'
5
5
  require 'hexapdf/content/processor'
6
6
  require 'hexapdf/serializer'
7
+ require 'ostruct'
7
8
 
8
9
  describe HexaPDF::Content::Operator::BaseOperator do
9
10
  before do
@@ -190,9 +191,11 @@ end
190
191
 
191
192
  describe_operator :SetGraphicsStateParameters, :gs do
192
193
  it "applies parameters from an ExtGState dictionary" do
194
+ font = OpenStruct.new
195
+ font.glyph_scaling_factor = 0.01
193
196
  @processor.resources[:ExtGState] = {Name: {LW: 10, LC: 2, LJ: 2, ML: 2, D: [[3, 5], 2],
194
197
  RI: 2, SA: true, BM: :Multiply, CA: 0.5, ca: 0.5,
195
- AIS: true, TK: false, Font: [:Test, 10]}}
198
+ AIS: true, TK: false, Font: [font, 10]}}
196
199
  @processor.resources.define_singleton_method(:document) do
197
200
  Object.new.tap {|obj| obj.define_singleton_method(:deref) {|o| o } }
198
201
  end
@@ -210,7 +213,7 @@ describe_operator :SetGraphicsStateParameters, :gs do
210
213
  assert_equal(0.5, gs.stroke_alpha)
211
214
  assert_equal(0.5, gs.fill_alpha)
212
215
  assert(gs.alpha_source)
213
- assert_equal(:Test, gs.font)
216
+ assert_equal(font, gs.font)
214
217
  assert_equal(10, gs.font_size)
215
218
  refute(gs.text_knockout)
216
219
  end
@@ -448,7 +451,9 @@ describe_operator :SetFontAndSize, :Tf do
448
451
  self[:Font] && self[:Font][name]
449
452
  end
450
453
 
451
- @processor.resources[:Font] = {F1: :test}
454
+ font = OpenStruct.new
455
+ font.glyph_scaling_factor = 0.01
456
+ @processor.resources[:Font] = {F1: font}
452
457
  invoke(:F1, 10)
453
458
  assert_equal(@processor.resources.font(:F1), @processor.graphics_state.font)
454
459
  assert_equal(10, @processor.graphics_state.font_size)
@@ -597,6 +597,11 @@ end
597
597
  describe HexaPDF::Layout::Style do
598
598
  before do
599
599
  @style = HexaPDF::Layout::Style.new
600
+ @style.font = Object.new.tap do |obj|
601
+ obj.define_singleton_method(:pdf_object) do
602
+ Object.new.tap {|pdf| pdf.define_singleton_method(:glyph_scaling_factor) { 0.001 } }
603
+ end
604
+ end
600
605
  end
601
606
 
602
607
  it "can assign values on initialization" do
@@ -644,6 +649,7 @@ describe HexaPDF::Layout::Style do
644
649
  end
645
650
 
646
651
  it "has several simple and dynamically generated properties with default values" do
652
+ @style = HexaPDF::Layout::Style.new
647
653
  assert_raises(HexaPDF::Error) { @style.font }
648
654
  assert_equal(10, @style.font_size)
649
655
  assert_equal(0, @style.character_spacing)
@@ -725,6 +731,11 @@ describe HexaPDF::Layout::Style do
725
731
  font = Object.new
726
732
  font.define_singleton_method(:scaling_factor) { 1 }
727
733
  font.define_singleton_method(:wrapped_font) { wrapped_font }
734
+ font.define_singleton_method(:pdf_object) do
735
+ obj = Object.new
736
+ obj.define_singleton_method(:glyph_scaling_factor) { 0.001 }
737
+ obj
738
+ end
728
739
  @style.font = font
729
740
  end
730
741
 
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.19.0)>>
43
+ <</Producer(HexaPDF version 0.19.1)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.19.0)>>
75
+ <</Producer(HexaPDF version 0.19.1)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -64,4 +64,8 @@ describe HexaPDF::Type::Font do
64
64
  assert_equal(5, @font.font_file)
65
65
  end
66
66
  end
67
+
68
+ it "returns the glyph scaling factor" do
69
+ assert_equal(0.001, @font.glyph_scaling_factor)
70
+ end
67
71
  end
@@ -9,10 +9,25 @@ describe HexaPDF::Type::FontType3 do
9
9
  @doc = HexaPDF::Document.new
10
10
  @font = @doc.add({Type: :Font, Subtype: :Type3, Encoding: :WinAnsiEncoding,
11
11
  FirstChar: 32, LastChar: 34, Widths: [600, 0, 700],
12
- FontBBox: [0, 0, 100, 100], FontMatrix: [1, 0, 0, 1, 0, 0],
12
+ FontBBox: [0, 100, 100, 0], FontMatrix: [0.002, 0, 0, 0.002, 0, 0],
13
13
  CharProcs: {}})
14
14
  end
15
15
 
16
+ describe "bounding_box" do
17
+ it "returns the font's bounding box" do
18
+ assert_equal([0, 0, 100, 100], @font.bounding_box)
19
+ end
20
+
21
+ it "inverts the y-values if necessary based on /FontMatrix" do
22
+ @font[:FontMatrix][3] *= -1
23
+ assert_equal([0, -100, 100, 0], @font.bounding_box)
24
+ end
25
+ end
26
+
27
+ it "returns the glyph scaling factor" do
28
+ assert_equal(0.002, @font.glyph_scaling_factor)
29
+ end
30
+
16
31
  describe "validation" do
17
32
  it "works for valid objects" do
18
33
  assert(@font.validate)