hexapdf 0.19.0 → 0.19.1

Sign up to get free protection for your applications and to get access to all the features.
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)