hexapdf 0.17.2 → 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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/lib/hexapdf/cli/command.rb +7 -1
  4. data/lib/hexapdf/content/canvas.rb +2 -2
  5. data/lib/hexapdf/content/color_space.rb +19 -0
  6. data/lib/hexapdf/content/graphic_object/arc.rb +2 -2
  7. data/lib/hexapdf/content/graphic_object/endpoint_arc.rb +4 -4
  8. data/lib/hexapdf/content/graphic_object/solid_arc.rb +111 -10
  9. data/lib/hexapdf/content/graphics_state.rb +167 -25
  10. data/lib/hexapdf/dictionary.rb +1 -1
  11. data/lib/hexapdf/dictionary_fields.rb +1 -1
  12. data/lib/hexapdf/document/signatures.rb +221 -0
  13. data/lib/hexapdf/encryption/security_handler.rb +3 -1
  14. data/lib/hexapdf/layout/style.rb +2 -1
  15. data/lib/hexapdf/object.rb +18 -0
  16. data/lib/hexapdf/parser.rb +3 -0
  17. data/lib/hexapdf/serializer.rb +2 -0
  18. data/lib/hexapdf/task/optimize.rb +46 -3
  19. data/lib/hexapdf/type/acro_form/appearance_generator.rb +4 -4
  20. data/lib/hexapdf/type/acro_form/form.rb +39 -28
  21. data/lib/hexapdf/type/acro_form/variable_text_field.rb +56 -18
  22. data/lib/hexapdf/type/annotations/widget.rb +3 -15
  23. data/lib/hexapdf/type/font.rb +5 -0
  24. data/lib/hexapdf/type/font_type3.rb +20 -0
  25. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +125 -0
  26. data/lib/hexapdf/version.rb +1 -1
  27. data/test/hexapdf/content/graphic_object/test_endpoint_arc.rb +16 -8
  28. data/test/hexapdf/content/test_color_space.rb +26 -0
  29. data/test/hexapdf/content/test_graphics_state.rb +9 -1
  30. data/test/hexapdf/content/test_operator.rb +8 -3
  31. data/test/hexapdf/encryption/test_security_handler.rb +3 -1
  32. data/test/hexapdf/layout/test_style.rb +11 -0
  33. data/test/hexapdf/task/test_optimize.rb +26 -0
  34. data/test/hexapdf/test_dictionary.rb +1 -0
  35. data/test/hexapdf/test_dictionary_fields.rb +3 -2
  36. data/test/hexapdf/test_object.rb +28 -0
  37. data/test/hexapdf/test_parser.rb +11 -0
  38. data/test/hexapdf/test_writer.rb +2 -2
  39. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +8 -1
  40. data/test/hexapdf/type/acro_form/test_form.rb +15 -8
  41. data/test/hexapdf/type/acro_form/test_variable_text_field.rb +18 -8
  42. data/test/hexapdf/type/test_font.rb +4 -0
  43. data/test/hexapdf/type/test_font_type3.rb +16 -1
  44. metadata +4 -2
@@ -37,7 +37,7 @@
37
37
  require 'hexapdf/dictionary'
38
38
  require 'hexapdf/stream'
39
39
  require 'hexapdf/error'
40
- require 'hexapdf/content/parser'
40
+ require 'hexapdf/content'
41
41
 
42
42
  module HexaPDF
43
43
  module Type
@@ -61,14 +61,53 @@ module HexaPDF
61
61
 
62
62
  UNSET_ARG = ::Object.new # :nodoc:
63
63
 
64
- # Parses the given appearance string. If no block is given, the appearance string is
65
- # searched for font name and font size both of which are returend. Otherwise the block is
66
- # called with each found content stream operator and has to handle them themselves.
64
+ # Creates an AcroForm appearance string for the HexaPDF +document+ from the given arguments
65
+ # and returns it.
66
+ #
67
+ # +font+::
68
+ # The name of the font.
69
+ #
70
+ # +font_options+::
71
+ # Additional font options like :variant used when loading the font. See
72
+ # HexaPDF::Document::Fonts#add
73
+ #
74
+ # +font_size+::
75
+ # The font size. If this is set to 0, the font size is calculated using the height/width
76
+ # of the field.
77
+ #
78
+ # +font_color+::
79
+ # The font color. See HexaPDF::Content::ColorSpace.device_color_from_specification for
80
+ # allowed values.
81
+ def self.create_appearance_string(document, font: 'Helvetica', font_options: {},
82
+ font_size: 0, font_color: 0)
83
+ name = document.acro_form(create: true).default_resources.
84
+ add_font(document.fonts.add(font, **font_options).pdf_object)
85
+ font_color = HexaPDF::Content::ColorSpace.device_color_from_specification(font_color)
86
+ color_string = HexaPDF::Content::ColorSpace.serialize_device_color(font_color)
87
+ "#{color_string.chomp} /#{name} #{font_size} Tf"
88
+ end
89
+
90
+ # :call-seq:
91
+ # VariableTextField.parse_appearance_string(string) -> [font_name, font_size, font_color]
92
+ # VariableTextField.parse_appearance_string(string) {|obj, params| block } -> nil
93
+ #
94
+ # Parses the given appearance string.
95
+ #
96
+ # If no block is given, the appearance string is searched for font name, font size and font
97
+ # color all of which are returned. Otherwise the block is called with each found content
98
+ # stream operator and has to handle them itself.
67
99
  def self.parse_appearance_string(appearance_string, &block) # :yield: obj, params
68
- font_params = nil
69
- block ||= lambda {|obj, params| font_params = params.dup if obj == :Tf }
100
+ font_params = [nil, nil, nil]
101
+ block ||= lambda do |obj, params|
102
+ case obj
103
+ when :Tf
104
+ font_params[0, 2] = params
105
+ when :rg, :g, :k
106
+ font_params[2] = HexaPDF::Content::ColorSpace.prenormalized_device_color(params)
107
+ end
108
+ end
70
109
  HexaPDF::Content::Parser.parse(appearance_string.sub(/\/\//, '/'), &block)
71
- font_params
110
+ block_given? ? nil : font_params
72
111
  end
73
112
 
74
113
  # :call-seq:
@@ -99,21 +138,20 @@ module HexaPDF
99
138
  end
100
139
  end
101
140
 
102
- # Sets the default appearance string using the provided values.
103
- #
104
- # The default argument values are a sane default. If +font_size+ is set to 0, the font size
105
- # is calculated using the height/width of the field.
141
+ # Sets the default appearance string using the provided values or the default values which
142
+ # provide a sane default.
106
143
  #
107
- # Use the +font_options+ hash to provide font options like :variant, see
108
- # HexaPDF::Document::Fonts#add.
109
- def set_default_appearance_string(font: 'Helvetica', font_options: {}, font_size: 0)
110
- name = document.acro_form(create: true).default_resources.
111
- add_font(document.fonts.add(font, **font_options).pdf_object)
112
- self[:DA] = "0 g /#{name} #{font_size} Tf"
144
+ # See ::create_appearance_string for information on the arguments.
145
+ def set_default_appearance_string(font: 'Helvetica', font_options: {}, font_size: 0,
146
+ font_color: 0)
147
+ self[:DA] = self.class.create_appearance_string(document, font: font,
148
+ font_options: font_options,
149
+ font_size: font_size,
150
+ font_color: font_color)
113
151
  end
114
152
 
115
153
  # Parses the default appearance string and returns an array containing [font_name,
116
- # font_size].
154
+ # font_size, font_color].
117
155
  #
118
156
  # The default appearance string is taken from the field or, if not set, the default
119
157
  # appearance string of the form.
@@ -268,6 +268,7 @@ module HexaPDF
268
268
  style ||= (field.check_box? ? :check : :cicrle)
269
269
  size ||= 0
270
270
  color = Content::ColorSpace.device_color_from_specification(color || 0)
271
+ serialized_color = Content::ColorSpace.serialize_device_color(color)
271
272
 
272
273
  self[:MK] ||= {}
273
274
  self[:MK][:CA] = case style
@@ -281,13 +282,6 @@ module HexaPDF
281
282
  else
282
283
  raise ArgumentError, "Unknown value #{style} for argument 'style'"
283
284
  end
284
- operator = case color.color_space.family
285
- when :DeviceRGB then :rg
286
- when :DeviceGray then :g
287
- when :DeviceCMYK then :k
288
- end
289
- serialized_color = Content::Operator::DEFAULT_OPERATORS[operator].
290
- serialize(HexaPDF::Serializer.new, *color.components)
291
285
  self[:DA] = "/ZaDb #{size} Tf #{serialized_color}".strip
292
286
  else
293
287
  style = case self[:MK]&.[](:CA)
@@ -306,16 +300,10 @@ module HexaPDF
306
300
  end
307
301
  end
308
302
  size = 0
309
- color = [0]
303
+ color = HexaPDF::Content::ColorSpace.prenormalized_device_color([0])
310
304
  if (da = self[:DA] || field[:DA])
311
- HexaPDF::Type::AcroForm::VariableTextField.parse_appearance_string(da) do |obj, params|
312
- case obj
313
- when :rg, :g, :k then color = params.dup
314
- when :Tf then size = params[1]
315
- end
316
- end
305
+ _, size, color = HexaPDF::Type::AcroForm::VariableTextField.parse_appearance_string(da)
317
306
  end
318
- color = HexaPDF::Content::ColorSpace.prenormalized_device_color(color)
319
307
 
320
308
  MarkerStyle.new(style, size, color)
321
309
  end
@@ -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.17.2'
40
+ VERSION = '0.19.1'
41
41
 
42
42
  end
@@ -55,21 +55,29 @@ describe HexaPDF::Content::GraphicObject::EndpointArc do
55
55
 
56
56
  it "draws the arc onto the canvas" do
57
57
  {
58
- [false, false] => {cx: 100, cy: 50, start_angle: 180, end_angle: 270, clockwise: false},
59
- [false, true] => {cx: 50, cy: 25, start_angle: 90, end_angle: 0, clockwise: true},
60
- [true, false] => {cx: 50, cy: 25, start_angle: 90, end_angle: 360, clockwise: false},
61
- [true, true] => {cx: 100, cy: 50, start_angle: 180, end_angle: -90, clockwise: true},
58
+ [false, false] => {cx: 100, cy: 50, a: 50, b: 25, start_angle: 180, end_angle: 270, clockwise: false},
59
+ [false, true] => {cx: 50, cy: 25, a: 50, b: 25, start_angle: 90, end_angle: 0, clockwise: true},
60
+ [true, false] => {cx: 50, cy: 25, a: 50, b: 25, start_angle: 90, end_angle: 360, clockwise: false},
61
+ [true, true] => {cx: 0, cy: 0, a: 40, b: 30, start_angle: 60, end_angle: 120},
62
62
  }.each do |(large_arc, clockwise), data|
63
63
  @page.delete(:Contents)
64
64
  canvas = @page.canvas
65
- canvas.draw(:arc, a: 50, b: 25, inclination: 0, **data)
65
+ arc = canvas.graphic_object(:arc, **data)
66
+ canvas.draw(arc)
66
67
  arc_data = @page.contents
67
68
 
68
69
  canvas.contents.clear
69
70
  assert(@page.contents.empty?)
70
- canvas.move_to(50.0, 50.0)
71
- canvas.draw(:endpoint_arc, x: 100, y: 25, a: 50, b: 25, inclination: 0,
72
- large_arc: large_arc, clockwise: clockwise)
71
+ canvas.move_to(*arc.start_point)
72
+ earc = canvas.graphic_object(:endpoint_arc, x: arc.end_point[0], y: arc.end_point[1],
73
+ a: data[:a], b: data[:b], inclination: data[:inclination] || 0,
74
+ large_arc: large_arc, clockwise: clockwise)
75
+ canvas.draw(earc)
76
+ narc = canvas.graphic_object(:arc, **earc.send(:compute_arc_values, *arc.start_point))
77
+ assert_in_delta(arc.start_point[0], narc.start_point[0], 0.0001)
78
+ assert_in_delta(arc.start_point[1], narc.start_point[1], 0.0001)
79
+ assert_in_delta(arc.end_point[0], narc.end_point[0], 0.0001)
80
+ assert_in_delta(arc.end_point[1], narc.end_point[1], 0.0001)
73
81
  assert_equal(arc_data, @page.contents)
74
82
  end
75
83
  end
@@ -99,6 +99,32 @@ describe HexaPDF::Content::ColorSpace do
99
99
  end
100
100
  end
101
101
 
102
+ describe "self.serialize_device_color" do
103
+ it "works for device gray colors" do
104
+ color = @class.device_color_from_specification(0.5)
105
+ assert_equal("0.5 g\n", @class.serialize_device_color(color))
106
+ assert_equal("0.5 G\n", @class.serialize_device_color(color, type: :stroke))
107
+ end
108
+
109
+ it "works for device RGB colors" do
110
+ color = @class.device_color_from_specification("red")
111
+ assert_equal("1.0 0.0 0.0 rg\n", @class.serialize_device_color(color))
112
+ assert_equal("1.0 0.0 0.0 RG\n", @class.serialize_device_color(color, type: :stroke))
113
+ end
114
+
115
+ it "works for device CMYK colors" do
116
+ color = @class.device_color_from_specification([100, 100, 100, 0])
117
+ assert_equal("1.0 1.0 1.0 0.0 k\n", @class.serialize_device_color(color))
118
+ assert_equal("1.0 1.0 1.0 0.0 K\n", @class.serialize_device_color(color, type: :stroke))
119
+ end
120
+
121
+ it "fails if no device color is provided" do
122
+ assert_raises(ArgumentError) do
123
+ @class.serialize_device_color(@class::Universal.new([]).default_color)
124
+ end
125
+ end
126
+ end
127
+
102
128
  it "returns a device color object for prenormalized color values" do
103
129
  assert_equal([5, 6, 7], @class.prenormalized_device_color([5, 6, 7]).components)
104
130
  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)
@@ -107,9 +107,11 @@ describe HexaPDF::Encryption::SecurityHandler do
107
107
  end
108
108
 
109
109
  it "updates the trailer's /Encrypt entry to be wrapped by an encryption dictionary" do
110
- @document.trailer[:Encrypt] = {Filter: :Test, V: 1}
110
+ @document.trailer[:Encrypt] = {Filter: :Test,
111
+ V: HexaPDF::Object.new(1, oid: 1, document: @document)}
111
112
  HexaPDF::Encryption::SecurityHandler.set_up_decryption(@document)
112
113
  assert_kind_of(HexaPDF::Encryption::EncryptionDictionary, @document.trailer[:Encrypt])
114
+ assert_equal({Filter: :Test, V: 1}, @document.trailer[:Encrypt].value)
113
115
  end
114
116
 
115
117
  it "returns the frozen security handler" do
@@ -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
 
@@ -159,4 +159,30 @@ describe HexaPDF::Task::Optimize do
159
159
  assert_equal("10 10 m\nq\nQ\nBI\n/Name 5 ID\ndataEI\n", page.contents)
160
160
  end
161
161
  end
162
+
163
+ describe "prune_page_resources" do
164
+ it "removes all unused XObject references" do
165
+ [false, true].each do |compress_pages|
166
+ page1 = @doc.pages.add
167
+ page1.resources[:XObject] = {}
168
+ page1.resources[:XObject][:test] = @doc.add({})
169
+ page1.resources[:XObject][:used_on_page2] = @doc.add({})
170
+ page1.resources[:XObject][:unused] = @doc.add({})
171
+ page1.contents = "/test Do"
172
+ page2 = @doc.pages.add
173
+ page2.resources[:XObject] = {}
174
+ page2.resources[:XObject][:used_on2] = page1.resources[:XObject][:used_on_page2]
175
+ page2.resources[:XObject][:also_unused] = page1.resources[:XObject][:unused]
176
+ page2.contents = "/used_on2 Do"
177
+
178
+ @doc.task(:optimize, prune_page_resources: true, compress_pages: compress_pages)
179
+
180
+ assert(page1.resources[:XObject].key?(:test))
181
+ assert(page1.resources[:XObject].key?(:used_on_page2))
182
+ refute(page1.resources[:XObject].key?(:unused))
183
+ assert(page2.resources[:XObject].key?(:used_on2))
184
+ refute(page2.resources[:XObject].key?(:also_unused))
185
+ end
186
+ end
187
+ end
162
188
  end
@@ -20,6 +20,7 @@ describe HexaPDF::Dictionary do
20
20
  end
21
21
 
22
22
  def delete(_obj)
23
+ _obj.data.value = nil
23
24
  end
24
25
 
25
26
  def wrap(obj, type:)
@@ -135,12 +135,12 @@ describe HexaPDF::DictionaryFields do
135
135
  end
136
136
 
137
137
  def configuration
138
- {'document.on_invalid_string' => proc {|str| str }}
138
+ HexaPDF::Configuration.with_defaults
139
139
  end
140
140
 
141
141
  it "calls document.on_invalid_string if the provided string is invalid" do
142
142
  str = "\xfe\xff\xD8\x00\x00s\x00t".b
143
- assert_equal("\xD8\x00\x00s\x00t".force_encoding("UTF-16BE"), @field.convert(str, self))
143
+ assert_equal("st", @field.convert(str, self))
144
144
  end
145
145
  end
146
146
 
@@ -173,6 +173,7 @@ describe HexaPDF::DictionaryFields do
173
173
 
174
174
  it "allows conversion to a Time object from a binary string" do
175
175
  refute(@field.convert('test'.b, self))
176
+ refute(@field.convert('D:01211016165909+00\'64'.b, self))
176
177
 
177
178
  [
178
179
  ["D:1998", [1998, 01, 01, 00, 00, 00, "-00:00"]],
@@ -45,6 +45,34 @@ describe HexaPDF::Object do
45
45
  end
46
46
  end
47
47
 
48
+ describe "class.make_direct" do
49
+ before do
50
+ @doc = HexaPDF::Document.new
51
+ end
52
+
53
+ it "doesn't touch wrapped direct objects" do
54
+ obj = HexaPDF::Object.new(5)
55
+ assert_same(obj, HexaPDF::Object.make_direct(obj))
56
+ end
57
+
58
+ it "works for simple values" do
59
+ obj = HexaPDF::Object.new(5, oid: 1, document: @doc)
60
+ assert_same(5, HexaPDF::Object.make_direct(obj))
61
+ end
62
+
63
+ it "works for hashes" do
64
+ obj = HexaPDF::Dictionary.new({a: 5, b: HexaPDF::Object.new(:a, oid: 3, document: @doc)},
65
+ oid: 1, document: @doc)
66
+ assert_equal({a: 5, b: :a}, HexaPDF::Object.make_direct(obj))
67
+ end
68
+
69
+ it "works for arrays" do
70
+ obj = HexaPDF::PDFArray.new([:b, HexaPDF::Object.new(:a, oid: 3, document: @doc)],
71
+ oid: 1, document: @doc)
72
+ assert_equal([:b, :a], HexaPDF::Object.make_direct(obj))
73
+ end
74
+ end
75
+
48
76
  describe "initialize" do
49
77
  it "uses a simple value as is" do
50
78
  obj = HexaPDF::Object.new(5)
@@ -607,11 +607,22 @@ describe HexaPDF::Parser do
607
607
  assert_equal(4, @parser.load_object(@xref).value)
608
608
  end
609
609
 
610
+ it "handles an invalid object as first object" do
611
+ create_parser("2 0 obj\n(a(b\nendobj\n1 0 obj\n6\nendobj #)(\ntrailer\n<</Size 1>>")
612
+ assert_equal(6, @parser.load_object(@xref).value)
613
+ end
614
+
610
615
  it "ignores invalid lines" do
611
616
  create_parser("1 0 obj\n5\nendobj\nhello there\n1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
612
617
  assert_equal(6, @parser.load_object(@xref).value)
613
618
  end
614
619
 
620
+ it "resets the header offset" do
621
+ create_parser("1 0 obj\n5\nendobj\ntrailer\n<</Size 1>>")
622
+ @parser.instance_variable_set(:@header_offset, 5)
623
+ assert_equal(5, @parser.load_object(@xref).value)
624
+ end
625
+
615
626
  it "uses the last trailer" do
616
627
  create_parser("trailer <</Size 1>>\ntrailer <</Size 2/Prev 342>>")
617
628
  assert_equal({Size: 2}, @parser.reconstructed_revision.trailer.value)
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.17.2)>>
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.17.2)>>
75
+ <</Producer(HexaPDF version 0.19.1)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream