hexapdf 0.20.2 → 0.21.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +5 -3
  4. data/Rakefile +10 -1
  5. data/examples/018-composer.rb +10 -10
  6. data/lib/hexapdf/cli/batch.rb +4 -6
  7. data/lib/hexapdf/cli/form.rb +9 -1
  8. data/lib/hexapdf/cli/info.rb +5 -1
  9. data/lib/hexapdf/cli/inspect.rb +59 -0
  10. data/lib/hexapdf/cli/split.rb +1 -1
  11. data/lib/hexapdf/composer.rb +147 -53
  12. data/lib/hexapdf/configuration.rb +7 -3
  13. data/lib/hexapdf/content/canvas.rb +1 -1
  14. data/lib/hexapdf/content/color_space.rb +1 -1
  15. data/lib/hexapdf/content/operator.rb +7 -7
  16. data/lib/hexapdf/content/parser.rb +3 -3
  17. data/lib/hexapdf/content/processor.rb +9 -9
  18. data/lib/hexapdf/document/signatures.rb +5 -4
  19. data/lib/hexapdf/document.rb +7 -0
  20. data/lib/hexapdf/encryption/security_handler.rb +5 -1
  21. data/lib/hexapdf/font/true_type/font.rb +7 -7
  22. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  23. data/lib/hexapdf/font/true_type/subsetter.rb +1 -1
  24. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +3 -3
  25. data/lib/hexapdf/font/true_type_wrapper.rb +9 -14
  26. data/lib/hexapdf/font/type1/font.rb +10 -12
  27. data/lib/hexapdf/font/type1_wrapper.rb +15 -17
  28. data/lib/hexapdf/layout/box.rb +12 -9
  29. data/lib/hexapdf/layout/image_box.rb +1 -1
  30. data/lib/hexapdf/layout/style.rb +28 -8
  31. data/lib/hexapdf/layout/text_fragment.rb +10 -9
  32. data/lib/hexapdf/parser.rb +5 -0
  33. data/lib/hexapdf/tokenizer.rb +3 -3
  34. data/lib/hexapdf/type/acro_form/appearance_generator.rb +6 -4
  35. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -2
  36. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  37. data/lib/hexapdf/type/annotation.rb +1 -1
  38. data/lib/hexapdf/type/font_type0.rb +1 -1
  39. data/lib/hexapdf/type/font_type3.rb +1 -1
  40. data/lib/hexapdf/type/resources.rb +4 -4
  41. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +1 -1
  42. data/lib/hexapdf/type/signature.rb +1 -1
  43. data/lib/hexapdf/type/trailer.rb +3 -3
  44. data/lib/hexapdf/version.rb +1 -1
  45. data/lib/hexapdf/writer.rb +11 -3
  46. data/lib/hexapdf/xref_section.rb +1 -1
  47. data/test/hexapdf/common_tokenizer_tests.rb +5 -5
  48. data/test/hexapdf/content/test_graphics_state.rb +1 -0
  49. data/test/hexapdf/content/test_operator.rb +2 -2
  50. data/test/hexapdf/content/test_processor.rb +1 -1
  51. data/test/hexapdf/encryption/test_security_handler.rb +11 -3
  52. data/test/hexapdf/encryption/test_standard_security_handler.rb +23 -29
  53. data/test/hexapdf/filter/test_predictor.rb +16 -20
  54. data/test/hexapdf/font/test_type1_wrapper.rb +5 -1
  55. data/test/hexapdf/font/true_type/table/common.rb +1 -1
  56. data/test/hexapdf/font/true_type/table/test_cmap.rb +1 -1
  57. data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +1 -1
  58. data/test/hexapdf/image_loader/test_pdf.rb +6 -8
  59. data/test/hexapdf/image_loader/test_png.rb +2 -2
  60. data/test/hexapdf/layout/test_box.rb +11 -1
  61. data/test/hexapdf/layout/test_style.rb +23 -0
  62. data/test/hexapdf/layout/test_text_fragment.rb +21 -21
  63. data/test/hexapdf/test_composer.rb +115 -52
  64. data/test/hexapdf/test_dictionary.rb +2 -2
  65. data/test/hexapdf/test_document.rb +11 -9
  66. data/test/hexapdf/test_object.rb +1 -1
  67. data/test/hexapdf/test_parser.rb +13 -7
  68. data/test/hexapdf/test_serializer.rb +20 -22
  69. data/test/hexapdf/test_stream.rb +7 -9
  70. data/test/hexapdf/test_writer.rb +13 -2
  71. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +1 -2
  72. data/test/hexapdf/type/acro_form/test_choice_field.rb +1 -1
  73. data/test/hexapdf/type/signature/common.rb +1 -1
  74. data/test/hexapdf/type/test_annotation.rb +7 -1
  75. data/test/hexapdf/type/test_font_type0.rb +1 -1
  76. data/test/hexapdf/type/test_font_type1.rb +7 -7
  77. data/test/hexapdf/type/test_image.rb +13 -17
  78. metadata +2 -2
@@ -160,7 +160,7 @@ module HexaPDF
160
160
  end
161
161
  return unless entry.kind_of?(HexaPDF::Stream)
162
162
 
163
- if entry.type == :XObject && entry[:Subtype] == :Form
163
+ if entry.type == :XObject && entry[:Subtype] == :Form && !entry.instance_of?(HexaPDF::Stream)
164
164
  entry
165
165
  elsif (entry[:Type].nil? || entry[:Type] == :XObject) &&
166
166
  (entry[:Subtype].nil? || entry[:Subtype] == :Form) && entry[:BBox]
@@ -141,7 +141,7 @@ module HexaPDF
141
141
  registry = system_info[:Registry]
142
142
  ordering = system_info[:Ordering]
143
143
  if (encoding.kind_of?(Symbol) && HexaPDF::Font::CMap.predefined?(encoding.to_s) &&
144
- encoding != :"Identity-H" && encoding != :"Identity-V") ||
144
+ encoding != :'Identity-H' && encoding != :'Identity-V') ||
145
145
  (registry == "Adobe" && ['GB1', 'CNS1', 'Japan1', 'Korea1'].include?(ordering))
146
146
  HexaPDF::Font::CMap.for_name("#{registry}-#{ordering}-UCS2")
147
147
  end
@@ -59,7 +59,7 @@ module HexaPDF
59
59
  def bounding_box
60
60
  matrix = self[:FontMatrix]
61
61
  bbox = self[:FontBBox].value
62
- if matrix[3] < 0 # Some writers invert the y-axis
62
+ if matrix[3] < 0 # Some writers invert the y-axis
63
63
  bbox = bbox.dup
64
64
  bbox[1], bbox[3] = -bbox[3], -bbox[1]
65
65
  end
@@ -218,10 +218,7 @@ module HexaPDF
218
218
  def perform_validation
219
219
  super
220
220
  val = self[:ProcSet]
221
- if !val
222
- yield("No procedure set specified", true)
223
- self[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI]
224
- else
221
+ if val
225
222
  if val.kind_of?(Symbol)
226
223
  yield("Procedure set is a single value instead of an Array", true)
227
224
  val = value[:ProcSet] = [val]
@@ -235,6 +232,9 @@ module HexaPDF
235
232
  true
236
233
  end
237
234
  end
235
+ else
236
+ yield("No procedure set specified", true)
237
+ self[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI]
238
238
  end
239
239
  end
240
240
 
@@ -100,7 +100,7 @@ module HexaPDF
100
100
  end
101
101
 
102
102
  key_usage = signer_certificate.extensions.find {|ext| ext.oid == 'keyUsage' }
103
- unless key_usage.value.split(', ').include?("Digital Signature")
103
+ unless key_usage && key_usage.value.split(', ').include?("Digital Signature")
104
104
  result.log(:error, "Certificate key usage is missing 'Digital Signature'")
105
105
  end
106
106
 
@@ -75,7 +75,7 @@ module HexaPDF
75
75
 
76
76
  # For DocMDP, also used by UR
77
77
  define_field :P, type: [Integer, Boolean]
78
- define_field :V, type: Symbol, allowed_values: [:"1.2", :"2.2"]
78
+ define_field :V, type: Symbol, allowed_values: [:'1.2', :'2.2']
79
79
 
80
80
  # For UR
81
81
  define_field :Document, type: PDFArray
@@ -87,10 +87,10 @@ module HexaPDF
87
87
  # Updates the second part of the /ID field (the first part should always be the same for a
88
88
  # PDF file, the second part should change with each write).
89
89
  def update_id
90
- if !self[:ID].kind_of?(PDFArray)
91
- set_random_id
92
- else
90
+ if self[:ID].kind_of?(PDFArray)
93
91
  value[:ID][1] = Digest::MD5.digest(rand.to_s)
92
+ else
93
+ set_random_id
94
94
  end
95
95
  end
96
96
 
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.20.2'
40
+ VERSION = '0.21.0'
41
41
 
42
42
  end
@@ -90,12 +90,20 @@ module HexaPDF
90
90
  #
91
91
  # For this method to work the document must have been created from an existing file.
92
92
  def write_incremental
93
- @document.revisions.parser.io.seek(0, IO::SEEK_SET)
94
- IO.copy_stream(@document.revisions.parser.io, @io)
93
+ parser = @document.revisions.parser
94
+
95
+ _, orig_trailer = parser.load_revision(parser.startxref_offset)
96
+ orig_trailer = @document.wrap(orig_trailer, type: :XXTrailer)
97
+ if @document.revisions.current.trailer[:Encrypt]&.value != orig_trailer[:Encrypt]&.value
98
+ raise HexaPDF::Error, "Used encryption cannot be modified when doing incremental writing"
99
+ end
100
+
101
+ parser.io.seek(0, IO::SEEK_SET)
102
+ IO.copy_stream(parser.io, @io)
95
103
  @io << "\n"
96
104
 
97
105
  @rev_size = @document.revisions.current.next_free_oid
98
- @use_xref_streams = @document.revisions.parser.contains_xref_streams?
106
+ @use_xref_streams = parser.contains_xref_streams?
99
107
 
100
108
  revision = Revision.new(@document.revisions.current.trailer)
101
109
  @document.revisions.each do |rev|
@@ -109,7 +109,7 @@ module HexaPDF
109
109
 
110
110
  # Make the assignment method private so that only the provided convenience methods can be
111
111
  # used.
112
- private :"[]="
112
+ private :'[]='
113
113
 
114
114
  # Adds an in-use entry to the cross-reference section.
115
115
  #
@@ -60,9 +60,9 @@ module CommonTokenizerTests
60
60
  'obj', 'endobj', 'f*', '*f', '{', '}',
61
61
  "parentheses ( ) and (\nspecial \0053++characters\n (*!&}^% and so on).\n", '',
62
62
  "Nov shmoz ka pop.", "\x90\x1F\xA3", "\x90\x1F\xA0",
63
- :Name1, :ASomewhatLongerName, :"A;Name_With-Various***Characters?",
64
- :"1.2", :"$$", :"@pattern", :".notdef", :"lime Green", :"paired()parentheses",
65
- :"The_Key_of_F#_Minor", :AB, :"",
63
+ :Name1, :ASomewhatLongerName, :'A;Name_With-Various***Characters?',
64
+ :'1.2', :$$, :@pattern, :'.notdef', :'lime Green', :'paired()parentheses',
65
+ :'The_Key_of_F#_Minor', :AB, :"",
66
66
  '[', 5, 6, :Name, ']', '[', 5, 6, :Name, ']',
67
67
  '<<', :Name, 5, '>>'
68
68
  ].each {|t| t.force_encoding('BINARY') if t.respond_to?(:force_encoding) }
@@ -85,12 +85,12 @@ module CommonTokenizerTests
85
85
 
86
86
  create_tokenizer("/Hößgang")
87
87
  token = @tokenizer.next_token
88
- assert_equal(:"Hößgang", token)
88
+ assert_equal(:Hößgang, token)
89
89
  assert_equal(Encoding::UTF_8, token.encoding)
90
90
 
91
91
  create_tokenizer('/H#c3#b6#c3#9fgang')
92
92
  token = @tokenizer.next_token
93
- assert_equal(:"Hößgang", token)
93
+ assert_equal(:Hößgang, token)
94
94
  assert_equal(Encoding::UTF_8, token.encoding)
95
95
 
96
96
  create_tokenizer('/H#E8lp')
@@ -156,4 +156,5 @@ class GraphicsStateWrapper < Minitest::Spec
156
156
  assert_equal(0.02, @gs.scaled_font_size)
157
157
  end
158
158
  end
159
+
159
160
  end
@@ -391,7 +391,7 @@ describe_operator :CurveToNoSecondControlPoint, :y do
391
391
  end
392
392
  end
393
393
 
394
- [:S, :s, :f, :F, 'f*'.intern, :B, 'B*'.intern, :b, 'b*'.intern, :n].each do |sym|
394
+ [:S, :s, :f, :F, :'f*', :B, :'B*', :b, :'b*', :n].each do |sym|
395
395
  describe_operator :EndPath, sym do
396
396
  it "changes the graphics object to none" do
397
397
  @processor.graphics_object = :path
@@ -401,7 +401,7 @@ end
401
401
  end
402
402
  end
403
403
 
404
- [:W, 'W*'.intern].each do |sym|
404
+ [:W, :'W*'].each do |sym|
405
405
  describe_operator :ClipPath, sym do
406
406
  it "changes the graphics object to clipping_path for clip path operations" do
407
407
  invoke
@@ -119,7 +119,7 @@ describe HexaPDF::Content::Processor do
119
119
  @processor.process(:BT)
120
120
  @processor.graphics_state.font = @font = @doc.add({Type: :Font, Subtype: :Type1,
121
121
  Encoding: :WinAnsiEncoding,
122
- BaseFont: :"Times-Roman"})
122
+ BaseFont: :'Times-Roman'})
123
123
  @processor.graphics_state.font_size = 10
124
124
  @processor.graphics_state.text_rise = 10
125
125
  @processor.graphics_state.character_spacing = 1
@@ -294,9 +294,17 @@ describe HexaPDF::Encryption::SecurityHandler do
294
294
  assert_equal('string', obj.stream)
295
295
  end
296
296
 
297
- it "doesn't decrypt a document's Encrypt dictionary" do
298
- @document.trailer[:Encrypt] = @obj
299
- assert_equal(@encrypted, @handler.decrypt(@obj)[:Key])
297
+ it "doesn't decrypt a document's Encrypt dictionaries" do
298
+ @document = HexaPDF::Document.new
299
+ @document.trailer[:Encrypt] = @document.add({Key: "Something"})
300
+ @document.revisions.add
301
+ @document.trailer[:Encrypt] = @document.add({Key: "Otherthing"})
302
+ @handler = TestHandler.new(@document)
303
+
304
+ assert_equal("Something",
305
+ @handler.decrypt(@document.revisions[0].trailer[:Encrypt])[:Key])
306
+ assert_equal("Otherthing",
307
+ @handler.decrypt(@document.revisions[1].trailer[:Encrypt])[:Key])
300
308
  end
301
309
 
302
310
  it "defers handling encryption to a Crypt filter is specified" do
@@ -62,29 +62,25 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
62
62
  test_files.each do |file|
63
63
  basename = File.basename(file)
64
64
  it "can decrypt, encrypt and decrypt the encrypted file #{basename} with the user password" do
65
- begin
66
- doc = HexaPDF::Document.new(io: StringIO.new(File.binread(file)),
67
- decryption_opts: {password: user_password})
68
- assert_equal(minimal_doc.trailer[:Info][:ModDate], doc.trailer[:Info][:ModDate])
69
-
70
- out = StringIO.new(''.b)
71
- HexaPDF::Writer.new(doc, out).write
72
- doc = HexaPDF::Document.new(io: out, decryption_opts: {password: user_password})
73
- assert_equal(minimal_doc.trailer[:Info][:ModDate], doc.trailer[:Info][:ModDate])
74
- rescue HexaPDF::EncryptionError => e
75
- flunk("Error processing #{basename}: #{e}")
76
- end
65
+ doc = HexaPDF::Document.new(io: StringIO.new(File.binread(file)),
66
+ decryption_opts: {password: user_password})
67
+ assert_equal(minimal_doc.trailer[:Info][:ModDate], doc.trailer[:Info][:ModDate])
68
+
69
+ out = StringIO.new(''.b)
70
+ HexaPDF::Writer.new(doc, out).write
71
+ doc = HexaPDF::Document.new(io: out, decryption_opts: {password: user_password})
72
+ assert_equal(minimal_doc.trailer[:Info][:ModDate], doc.trailer[:Info][:ModDate])
73
+ rescue HexaPDF::EncryptionError => e
74
+ flunk("Error processing #{basename}: #{e}")
77
75
  end
78
76
 
79
77
  unless basename.start_with?("userpwd")
80
78
  it "can decrypt the encrypted file #{basename} with the owner password" do
81
- begin
82
- doc = HexaPDF::Document.new(io: StringIO.new(File.binread(file)),
83
- decryption_opts: {password: owner_password})
84
- assert_equal(minimal_doc.trailer[:Info][:ModDate], doc.trailer[:Info][:ModDate])
85
- rescue HexaPDF::EncryptionError => e
86
- flunk("Error processing #{basename}: #{e}")
87
- end
79
+ doc = HexaPDF::Document.new(io: StringIO.new(File.binread(file)),
80
+ decryption_opts: {password: owner_password})
81
+ assert_equal(minimal_doc.trailer[:Info][:ModDate], doc.trailer[:Info][:ModDate])
82
+ rescue HexaPDF::EncryptionError => e
83
+ flunk("Error processing #{basename}: #{e}")
88
84
  end
89
85
  end
90
86
  end
@@ -97,16 +93,14 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
97
93
  it "can encrypt and then decrypt with all encryption variations" do
98
94
  {arc4: [40, 48, 128], aes: [128, 256]}.each do |algorithm, key_lengths|
99
95
  key_lengths.each do |key_length|
100
- begin
101
- doc = HexaPDF::Document.new
102
- doc.encrypt(algorithm: algorithm, key_length: key_length)
103
- sio = StringIO.new
104
- doc.write(sio)
105
- doc = HexaPDF::Document.new(io: sio)
106
- assert_kind_of(Time, doc.trailer.info[:ModDate], "alg: #{algorithm} #{key_length} bits")
107
- rescue HexaPDF::Error => e
108
- flunk("Error using variation: #{algorithm} #{key_length} bits\n" << e.message)
109
- end
96
+ doc = HexaPDF::Document.new
97
+ doc.encrypt(algorithm: algorithm, key_length: key_length)
98
+ sio = StringIO.new
99
+ doc.write(sio)
100
+ doc = HexaPDF::Document.new(io: sio)
101
+ assert_kind_of(Time, doc.trailer.info[:ModDate], "alg: #{algorithm} #{key_length} bits")
102
+ rescue HexaPDF::Error => e
103
+ flunk("Error using variation: #{algorithm} #{key_length} bits\n" << e.message)
110
104
  end
111
105
  end
112
106
  end
@@ -106,17 +106,15 @@ describe HexaPDF::Filter::Predictor do
106
106
  end
107
107
 
108
108
  it "fails if the last row is missing data and 'filter.predictor.strict' is true " do
109
- begin
110
- HexaPDF::GlobalConfiguration['filter.predictor.strict'] = true
111
- assert_raises(HexaPDF::FilterError) do
112
- data = @testcases['up']
113
- encoder = @obj.png_execute(:encoder, feeder(data[:source][0..-2], 1), data[:Predictor],
114
- data[:Colors], data[:BitsPerComponent], data[:Columns])
115
- collector(encoder)
116
- end
117
- ensure
118
- HexaPDF::GlobalConfiguration['filter.predictor.strict'] = false
109
+ HexaPDF::GlobalConfiguration['filter.predictor.strict'] = true
110
+ assert_raises(HexaPDF::FilterError) do
111
+ data = @testcases['up']
112
+ encoder = @obj.png_execute(:encoder, feeder(data[:source][0..-2], 1), data[:Predictor],
113
+ data[:Colors], data[:BitsPerComponent], data[:Columns])
114
+ collector(encoder)
119
115
  end
116
+ ensure
117
+ HexaPDF::GlobalConfiguration['filter.predictor.strict'] = false
120
118
  end
121
119
  end
122
120
 
@@ -139,17 +137,15 @@ describe HexaPDF::Filter::Predictor do
139
137
  end
140
138
 
141
139
  it "fails if the last row is missing data and 'filter.predictor.strict' is true " do
142
- begin
143
- HexaPDF::GlobalConfiguration['filter.predictor.strict'] = true
144
- assert_raises(HexaPDF::FilterError) do
145
- data = @testcases['up']
146
- encoder = @obj.png_execute(:decoder, feeder(data[:result][0..-2], 1), data[:Predictor],
147
- data[:Colors], data[:BitsPerComponent], data[:Columns])
148
- collector(encoder)
149
- end
150
- ensure
151
- HexaPDF::GlobalConfiguration['filter.predictor.strict'] = false
140
+ HexaPDF::GlobalConfiguration['filter.predictor.strict'] = true
141
+ assert_raises(HexaPDF::FilterError) do
142
+ data = @testcases['up']
143
+ encoder = @obj.png_execute(:decoder, feeder(data[:result][0..-2], 1), data[:Predictor],
144
+ data[:Colors], data[:BitsPerComponent], data[:Columns])
145
+ collector(encoder)
152
146
  end
147
+ ensure
148
+ HexaPDF::GlobalConfiguration['filter.predictor.strict'] = false
153
149
  end
154
150
  end
155
151
  end
@@ -14,7 +14,7 @@ describe HexaPDF::Font::Type1Wrapper do
14
14
 
15
15
  it "can be used with an existing PDF object" do
16
16
  font = @doc.add({Type: :Font, Subtype: :Type1, Encoding: {Differences: [65, :B]},
17
- BaseFont: :"Times-Roman"})
17
+ BaseFont: :'Times-Roman'})
18
18
  wrapper = HexaPDF::Font::Type1Wrapper.new(@doc, FONT_TIMES, pdf_object: font)
19
19
  assert_equal([:B, :E, :A, :S, :T], wrapper.decode_utf8("BEAST").map(&:name))
20
20
  assert_equal("A", wrapper.encode(wrapper.glyph(:A)))
@@ -103,5 +103,9 @@ describe HexaPDF::Font::Type1Wrapper do
103
103
  it "sets the circular reference" do
104
104
  assert_same(@times_wrapper, @times_wrapper.pdf_object.font_wrapper)
105
105
  end
106
+
107
+ it "makes sure that the PDF dictionaries are indirect" do
108
+ assert(@times_wrapper.pdf_object.indirect?)
109
+ end
106
110
  end
107
111
  end
@@ -16,7 +16,7 @@ module TestHelper
16
16
  end
17
17
 
18
18
  def create_table(name, data = nil, standalone: false)
19
- font, entry = !standalone ? [@font, @entry] : set_up_stub_true_type_font(register_vars: false)
19
+ font, entry = standalone ? set_up_stub_true_type_font(register_vars: false) : [@font, @entry]
20
20
  if data
21
21
  font.io.string = data
22
22
  entry.length = font.io.length
@@ -11,7 +11,7 @@ describe HexaPDF::Font::TrueType::Table::Cmap do
11
11
  [0, 1, 28],
12
12
  [3, 1, 28 + f0.length],
13
13
  [1, 0, 28],
14
- ].map {|a| a.pack('n2N') }.join('') << f0 << [10, 22, 0, 0, 2, 10, 13].pack('nN2N2n2')
14
+ ].map {|a| a.pack('n2N') }.join << f0 << [10, 22, 0, 0, 2, 10, 13].pack('nN2N2n2')
15
15
  set_up_stub_true_type_font(data)
16
16
  end
17
17
 
@@ -55,7 +55,7 @@ describe HexaPDF::Font::TrueType::Table::CmapSubtable do
55
55
 
56
56
  it "works for format 2" do
57
57
  f2 = ([0, 8] + [0] * 254).pack('n*') + \
58
- [[0, 256, 0, 2 + 8], [0x33, 3, 5, 2 + 256 * 2]].map {|a| a.pack('n2s>n') }.join('') + \
58
+ [[0, 256, 0, 2 + 8], [0x33, 3, 5, 2 + 256 * 2]].map {|a| a.pack('n2s>n') }.join + \
59
59
  ((0..255).to_a + [35, 65534, 0]).pack('n*')
60
60
  t = table([2, f2.length + 6, 0].pack('n3') << f2)
61
61
  assert_nil(t[0x0132])
@@ -33,14 +33,12 @@ describe HexaPDF::ImageLoader::PDF do
33
33
  end
34
34
 
35
35
  it "works for PDF files using a string object and use_stringio=false" do
36
- begin
37
- @doc.config['image_loader.pdf.use_stringio'] = false
38
- form = @loader.load(@doc, @pdf)
39
- assert_equal(:Form, form[:Subtype])
40
- ensure
41
- ObjectSpace.each_object(File) do |file|
42
- file.close if file.path == @pdf && !file.closed?
43
- end
36
+ @doc.config['image_loader.pdf.use_stringio'] = false
37
+ form = @loader.load(@doc, @pdf)
38
+ assert_equal(:Form, form[:Subtype])
39
+ ensure
40
+ ObjectSpace.each_object(File) do |file|
41
+ file.close if file.path == @pdf && !file.closed?
44
42
  end
45
43
  end
46
44
  end
@@ -30,11 +30,11 @@ describe HexaPDF::ImageLoader::PNG do
30
30
  assert_equal(height, image[:Height])
31
31
  assert_equal(bpc, image[:BitsPerComponent])
32
32
  assert_equal(color_space, @doc.unwrap(image[:ColorSpace])) if color_space
33
- data = stream.map {|row| [row.map {|i| i.to_s(2).rjust(bpc, '0') }.join("")].pack('B*') }.join("")
33
+ data = stream.map {|row| [row.map {|i| i.to_s(2).rjust(bpc, '0') }.join].pack('B*') }.join
34
34
  assert_equal(data, image.stream)
35
35
  end
36
36
 
37
- # Note: colors and image data for comparisons were extracted using GIMP and its color tools
37
+ # NOTE: colors and image data for comparisons were extracted using GIMP and its color tools
38
38
  describe "load" do
39
39
  before do
40
40
  @greyscale_1bit_data = [[1, 1, 0, 0, 0],
@@ -17,11 +17,21 @@ describe HexaPDF::Layout::Box do
17
17
  assert_same(block, box.instance_eval { @draw_block })
18
18
  end
19
19
 
20
- it "allows specifying style options" do
20
+ it "allows specifying a style object" do
21
+ box = HexaPDF::Layout::Box.create(style: {background_color: 20})
22
+ assert_equal(20, box.style.background_color)
23
+ end
24
+
25
+ it "allows specifying style properties" do
21
26
  box = HexaPDF::Layout::Box.create(background_color: 20)
22
27
  assert_equal(20, box.style.background_color)
23
28
  end
24
29
 
30
+ it "applies the additional style properties to the style object" do
31
+ box = HexaPDF::Layout::Box.create(style: {background_color: 20}, background_color: 15)
32
+ assert_equal(15, box.style.background_color)
33
+ end
34
+
25
35
  it "takes content width and height" do
26
36
  box = HexaPDF::Layout::Box.create(width: 100, height: 200, content_box: true,
27
37
  padding: [10, 8, 6, 4],
@@ -604,6 +604,26 @@ describe HexaPDF::Layout::Style do
604
604
  end
605
605
  end
606
606
 
607
+ describe "self.create" do
608
+ it "returns the provided style argument" do
609
+ assert_same(@style, HexaPDF::Layout::Style.create(@style))
610
+ end
611
+
612
+ it "creates a new Style object based on the passed hash" do
613
+ style = HexaPDF::Layout::Style.create(font_size: 10, fill_color: 'green')
614
+ assert_equal(10, style.font_size)
615
+ assert_equal('green', style.fill_color)
616
+ end
617
+
618
+ it "creates an empty Style object if nil is passed" do
619
+ assert_kind_of(HexaPDF::Layout::Style, HexaPDF::Layout::Style.create(nil))
620
+ end
621
+
622
+ it "raises an error if an invalid object is provided" do
623
+ assert_raises(ArgumentError) { HexaPDF::Layout::Style.create(5) }
624
+ end
625
+ end
626
+
607
627
  it "can assign values on initialization" do
608
628
  style = HexaPDF::Layout::Style.new(font_size: 10)
609
629
  assert_equal(10, style.font_size)
@@ -692,6 +712,9 @@ describe HexaPDF::Layout::Style do
692
712
 
693
713
  @style.stroke_dash_pattern(5, 2)
694
714
  assert_equal([[5], 2], @style.stroke_dash_pattern.to_operands)
715
+
716
+ @style.line_spacing(1.2)
717
+ assert_equal([:proportional, 1.2], [@style.line_spacing.type, @style.line_spacing.value])
695
718
  end
696
719
 
697
720
  it "allows checking for valid values" do
@@ -20,7 +20,7 @@ describe HexaPDF::Layout::TextFragment do
20
20
  end
21
21
 
22
22
  describe "create" do
23
- it "creates a TextFragment from text and options" do
23
+ it "creates a TextFragment from text and style" do
24
24
  frag = HexaPDF::Layout::TextFragment.create("Tom", font: @font, font_size: 20,
25
25
  font_features: {kern: true})
26
26
  assert_equal(4, frag.items.length)
@@ -180,16 +180,16 @@ describe HexaPDF::Layout::TextFragment do
180
180
  [:set_line_width, [5]],
181
181
  [:set_line_cap_style, [1]],
182
182
  [:set_line_dash_pattern, [[5], 0]]],
183
- back: [[:end_text],
184
- [:save_graphics_state],
185
- [:set_device_gray_stroking_color, [0]],
186
- [:set_line_width, [@fragment.style.calculated_underline_thickness]],
187
- [:set_line_cap_style, [0]],
188
- [:set_line_dash_pattern, [[], 0]],
189
- [:move_to, [10, 15]],
190
- [:line_to, [40.88, 15]],
191
- [:stroke_path],
192
- [:restore_graphics_state]])
183
+ back: [[:end_text],
184
+ [:save_graphics_state],
185
+ [:set_device_gray_stroking_color, [0]],
186
+ [:set_line_width, [@fragment.style.calculated_underline_thickness]],
187
+ [:set_line_cap_style, [0]],
188
+ [:set_line_dash_pattern, [[], 0]],
189
+ [:move_to, [10, 15]],
190
+ [:line_to, [40.88, 15]],
191
+ [:stroke_path],
192
+ [:restore_graphics_state]])
193
193
  end
194
194
 
195
195
  it "draws the strikeout line" do
@@ -201,16 +201,16 @@ describe HexaPDF::Layout::TextFragment do
201
201
  [:set_line_width, [5]],
202
202
  [:set_line_cap_style, [1]],
203
203
  [:set_line_dash_pattern, [[5], 0]]],
204
- back: [[:end_text],
205
- [:save_graphics_state],
206
- [:set_device_gray_stroking_color, [0]],
207
- [:set_line_width, [@fragment.style.calculated_strikeout_thickness]],
208
- [:set_line_cap_style, [0]],
209
- [:set_line_dash_pattern, [[], 0]],
210
- [:move_to, [10, 21.01]],
211
- [:line_to, [40.88, 21.01]],
212
- [:stroke_path],
213
- [:restore_graphics_state]])
204
+ back: [[:end_text],
205
+ [:save_graphics_state],
206
+ [:set_device_gray_stroking_color, [0]],
207
+ [:set_line_width, [@fragment.style.calculated_strikeout_thickness]],
208
+ [:set_line_cap_style, [0]],
209
+ [:set_line_dash_pattern, [[], 0]],
210
+ [:move_to, [10, 21.01]],
211
+ [:line_to, [40.88, 21.01]],
212
+ [:stroke_path],
213
+ [:restore_graphics_state]])
214
214
  end
215
215
  end
216
216