hexapdf 0.20.4 → 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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -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/info.rb +5 -1
  8. data/lib/hexapdf/cli/inspect.rb +59 -0
  9. data/lib/hexapdf/cli/split.rb +1 -1
  10. data/lib/hexapdf/composer.rb +147 -53
  11. data/lib/hexapdf/configuration.rb +7 -3
  12. data/lib/hexapdf/content/canvas.rb +1 -1
  13. data/lib/hexapdf/content/color_space.rb +1 -1
  14. data/lib/hexapdf/content/operator.rb +7 -7
  15. data/lib/hexapdf/content/parser.rb +3 -3
  16. data/lib/hexapdf/content/processor.rb +9 -9
  17. data/lib/hexapdf/document/signatures.rb +5 -4
  18. data/lib/hexapdf/document.rb +7 -0
  19. data/lib/hexapdf/font/true_type/font.rb +7 -7
  20. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  21. data/lib/hexapdf/font/true_type/subsetter.rb +1 -1
  22. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +3 -3
  23. data/lib/hexapdf/font/true_type_wrapper.rb +9 -14
  24. data/lib/hexapdf/font/type1/font.rb +10 -12
  25. data/lib/hexapdf/font/type1_wrapper.rb +1 -2
  26. data/lib/hexapdf/layout/box.rb +12 -9
  27. data/lib/hexapdf/layout/image_box.rb +1 -1
  28. data/lib/hexapdf/layout/style.rb +28 -8
  29. data/lib/hexapdf/layout/text_fragment.rb +10 -9
  30. data/lib/hexapdf/parser.rb +5 -0
  31. data/lib/hexapdf/tokenizer.rb +3 -3
  32. data/lib/hexapdf/type/acro_form/appearance_generator.rb +6 -4
  33. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -2
  34. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  35. data/lib/hexapdf/type/font_type0.rb +1 -1
  36. data/lib/hexapdf/type/font_type3.rb +1 -1
  37. data/lib/hexapdf/type/resources.rb +4 -4
  38. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +1 -1
  39. data/lib/hexapdf/type/signature.rb +1 -1
  40. data/lib/hexapdf/type/trailer.rb +3 -3
  41. data/lib/hexapdf/version.rb +1 -1
  42. data/lib/hexapdf/xref_section.rb +1 -1
  43. data/test/hexapdf/common_tokenizer_tests.rb +5 -5
  44. data/test/hexapdf/content/test_graphics_state.rb +1 -0
  45. data/test/hexapdf/content/test_operator.rb +2 -2
  46. data/test/hexapdf/content/test_processor.rb +1 -1
  47. data/test/hexapdf/encryption/test_standard_security_handler.rb +23 -29
  48. data/test/hexapdf/filter/test_predictor.rb +16 -20
  49. data/test/hexapdf/font/test_type1_wrapper.rb +1 -1
  50. data/test/hexapdf/font/true_type/table/common.rb +1 -1
  51. data/test/hexapdf/font/true_type/table/test_cmap.rb +1 -1
  52. data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +1 -1
  53. data/test/hexapdf/image_loader/test_pdf.rb +6 -8
  54. data/test/hexapdf/image_loader/test_png.rb +2 -2
  55. data/test/hexapdf/layout/test_box.rb +11 -1
  56. data/test/hexapdf/layout/test_style.rb +23 -0
  57. data/test/hexapdf/layout/test_text_fragment.rb +21 -21
  58. data/test/hexapdf/test_composer.rb +115 -52
  59. data/test/hexapdf/test_dictionary.rb +2 -2
  60. data/test/hexapdf/test_document.rb +11 -9
  61. data/test/hexapdf/test_object.rb +1 -1
  62. data/test/hexapdf/test_parser.rb +13 -7
  63. data/test/hexapdf/test_serializer.rb +20 -22
  64. data/test/hexapdf/test_stream.rb +7 -9
  65. data/test/hexapdf/test_writer.rb +2 -2
  66. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +1 -2
  67. data/test/hexapdf/type/acro_form/test_choice_field.rb +1 -1
  68. data/test/hexapdf/type/signature/common.rb +1 -1
  69. data/test/hexapdf/type/test_font_type0.rb +1 -1
  70. data/test/hexapdf/type/test_font_type1.rb +7 -7
  71. data/test/hexapdf/type/test_image.rb +13 -17
  72. metadata +2 -2
@@ -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
@@ -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)))
@@ -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
 
@@ -19,7 +19,7 @@ describe HexaPDF::Composer do
19
19
  assert_equal(36, @composer.frame.bottom)
20
20
  assert_equal(523, @composer.frame.width)
21
21
  assert_equal(770, @composer.frame.height)
22
- assert_equal("Times", @composer.base_style.font)
22
+ assert_kind_of(HexaPDF::Layout::Style, @composer.style(:base))
23
23
  end
24
24
 
25
25
  it "allows the customization of the page size" do
@@ -86,83 +86,144 @@ describe HexaPDF::Composer do
86
86
  assert_equal(806, @composer.y)
87
87
  end
88
88
 
89
+ describe "style" do
90
+ it "creates a new style if it does not exist based on the base argument" do
91
+ @composer.style(:base, font_size: 20)
92
+ assert_equal(20, @composer.style(:newstyle, subscript: true).font_size)
93
+ refute( @composer.style(:base).subscript)
94
+ assert_equal(10, @composer.style(:another_new, base: nil).font_size)
95
+ assert(@composer.style(:yet_another_new, base: :newstyle).subscript)
96
+ end
97
+
98
+ it "returns the named style" do
99
+ assert_kind_of(HexaPDF::Layout::Style, @composer.style(:base))
100
+ end
101
+
102
+ it "updates the style with the given properties" do
103
+ assert_equal(20, @composer.style(:base, font_size: 20).font_size)
104
+ end
105
+ end
106
+
89
107
  describe "text" do
90
- it "creates a text box and draws it on the canvas" do
91
- box = nil
92
- @composer.define_singleton_method(:draw_box) {|arg| box = arg }
108
+ before do
109
+ test_self = self
110
+ @composer.define_singleton_method(:draw_box) do |arg|
111
+ test_self.instance_variable_set(:@box, arg)
112
+ end
113
+ end
93
114
 
115
+ it "creates a text box and draws it on the canvas" do
94
116
  @composer.text("Test", width: 10, height: 15)
95
- assert_equal(10, box.width)
96
- assert_equal(15, box.height)
97
- assert_same(@composer.document.fonts.add("Times"), box.style.font)
98
- items = box.instance_variable_get(:@items)
117
+ assert_equal(10, @box.width)
118
+ assert_equal(15, @box.height)
119
+ assert_same(@composer.document.fonts.add("Times"), @box.style.font)
120
+ items = @box.instance_variable_get(:@items)
99
121
  assert_equal(1, items.length)
100
- assert_same(box.style, items.first.style)
122
+ assert_same(@box.style, items.first.style)
101
123
  end
102
124
 
103
125
  it "allows setting of a custom style" do
104
- box = nil
105
- @composer.define_singleton_method(:draw_box) {|arg| box = arg }
106
-
107
- @composer.text("Test", style: HexaPDF::Layout::Style.new(font_size: 20))
108
- assert_same(@composer.document.fonts.add("Times"), box.style.font)
109
- assert_equal(20, box.style.font_size)
126
+ style = HexaPDF::Layout::Style.new(font_size: 20, font: ['Times', {variant: :bold}])
127
+ @composer.text("Test", style: style)
128
+ assert_same(@box.style, style)
129
+ assert_same(@composer.document.fonts.add("Times", variant: :bold), @box.style.font)
130
+ assert_equal(20, @box.style.font_size)
131
+
132
+ @composer.text("Test", style: {font_size: 20})
133
+ assert_equal(20, @box.style.font_size)
134
+
135
+ @composer.style(:named, font_size: 20)
136
+ @composer.text("Test", style: :named)
137
+ assert_equal(20, @box.style.font_size)
110
138
  end
111
139
 
112
140
  it "updates the used style with the provided options" do
113
- box = nil
114
- @composer.define_singleton_method(:draw_box) {|arg| box = arg }
141
+ @composer.text("Test", style: {subscript: true}, font_size: 20)
142
+ assert_equal(20, @box.style.font_size)
143
+ end
115
144
 
116
- @composer.text("Test", style: HexaPDF::Layout::Style.new, font_size: 20)
117
- assert_equal(20, box.style.font_size)
145
+ it "allows using a box style different from the text style" do
146
+ style = HexaPDF::Layout::Style.new(font_size: 20)
147
+ @composer.text("Test", box_style: style)
148
+ refute_same(@box.instance_variable_get(:@items).first.style, style)
149
+ assert_same(@box.style, style)
150
+
151
+ @composer.style(:named, font_size: 20)
152
+ @composer.text("Test", box_style: :named)
153
+ assert_equal(20, @box.style.font_size)
118
154
  end
119
155
  end
120
156
 
121
157
  describe "formatted_text" do
122
- it "creates a text box with the formatted text and draws it on the canvas" do
123
- box = nil
124
- @composer.define_singleton_method(:draw_box) {|arg| box = arg }
158
+ before do
159
+ test_self = self
160
+ @composer.define_singleton_method(:draw_box) do |arg|
161
+ test_self.instance_variable_set(:@box, arg)
162
+ end
163
+ end
125
164
 
165
+ it "creates a text box with the given text and draws it on the canvas" do
126
166
  @composer.formatted_text(["Test"], width: 10, height: 15)
127
- assert_equal(10, box.width)
128
- assert_equal(15, box.height)
129
- assert_equal(1, box.instance_variable_get(:@items).length)
167
+ assert_equal(10, @box.width)
168
+ assert_equal(15, @box.height)
169
+ assert_equal(1, @box.instance_variable_get(:@items).length)
130
170
  end
131
171
 
132
- it "a hash can be used for custom style properties" do
133
- box = nil
134
- @composer.define_singleton_method(:draw_box) {|arg| box = arg }
172
+ it "allows using a hash with :text key instead of a simple string" do
173
+ @composer.formatted_text([{text: "Test"}])
174
+ items = @box.instance_variable_get(:@items)
175
+ assert_equal(4, items[0].items.length)
176
+ end
135
177
 
136
- @composer.formatted_text([{text: "Test", font_size: 20}], align: :center)
137
- items = box.instance_variable_get(:@items)
138
- assert_equal(1, items.length)
139
- assert_equal(20, items.first.style.font_size)
140
- assert_equal(:center, items.first.style.align)
141
- assert_equal(10, box.style.font_size)
178
+ it "uses an empty string if the :text key for a hash is not specified" do
179
+ @composer.formatted_text([{font_size: "Test"}])
180
+ items = @box.instance_variable_get(:@items)
181
+ assert_equal(0, items[0].items.length)
142
182
  end
143
183
 
144
- it "a hash can be used to provide a custom style" do
145
- box = nil
146
- @composer.define_singleton_method(:draw_box) {|arg| box = arg }
184
+ it "allows setting a custom base style for all parts" do
185
+ @composer.formatted_text(["Test", "other"], font_size: 20)
186
+ items = @box.instance_variable_get(:@items)
187
+ assert_equal(20, @box.style.font_size)
188
+ assert_equal(20, items[0].style.font_size)
189
+ assert_equal(20, items[1].style.font_size)
190
+ end
191
+
192
+ it "allows using custom style properties for a single part" do
193
+ @composer.formatted_text([{text: "Test", font_size: 20}, "test"], align: :center)
194
+ items = @box.instance_variable_get(:@items)
195
+ assert_equal(10, @box.style.font_size)
147
196
 
148
- @composer.formatted_text([{text: "Test", style: HexaPDF::Layout::Style.new(fill_color: 128),
149
- font_size: 20}], align: :center)
150
- items = box.instance_variable_get(:@items)
151
- assert_equal(20, items.first.style.font_size)
152
- assert_equal(128, items.first.style.fill_color)
153
- assert_equal(:center, items.first.style.align)
197
+ assert_equal(20, items[0].style.font_size)
198
+ assert_equal(:center, items[0].style.align)
199
+
200
+ assert_equal(10, items[1].style.font_size)
201
+ assert_equal(:center, items[1].style.align)
154
202
  end
155
203
 
156
- it "a hash can be used to link to an URL" do
157
- box = nil
158
- @composer.define_singleton_method(:draw_box) {|arg| box = arg }
204
+ it "allows using a custom style as basis for a single part" do
205
+ @composer.formatted_text([{text: "Test", style: {font_size: 20}, subscript: true}, "test"],
206
+ align: :center)
207
+ items = @box.instance_variable_get(:@items)
208
+ assert_equal(10, @box.style.font_size)
159
209
 
160
- @composer.formatted_text([{text: "Test", link: "URI"}, {link: "URI"}])
161
- items = box.instance_variable_get(:@items)
162
- assert_equal(2, items.length)
163
- assert_equal(4, items[0].items.length)
164
- assert_equal(3, items[1].items.length)
210
+ assert_equal(20, items[0].style.font_size)
211
+ assert_equal(:left, items[0].style.align)
212
+ assert(items[0].style.subscript)
213
+
214
+ assert_equal(10, items[1].style.font_size)
215
+ assert_equal(:center, items[1].style.align)
216
+ refute(items[1].style.subscript)
217
+ end
218
+
219
+ it "allows specifying a link to an URL via the :link key" do
220
+ @composer.formatted_text([{text: "Test", link: "URI"}, {link: "URI"}, "test"])
221
+ items = @box.instance_variable_get(:@items)
222
+ assert_equal(3, items.length)
223
+ assert_equal(4, items[0].items.length, "text should be Test")
224
+ assert_equal(3, items[1].items.length, "text should be URI")
165
225
  assert_equal([:link, {uri: 'URI'}], items[0].style.overlays.instance_variable_get(:@layers)[0])
226
+ refute(items[2].style.overlays?)
166
227
  end
167
228
  end
168
229
 
@@ -172,9 +233,11 @@ describe HexaPDF::Composer do
172
233
  @composer.define_singleton_method(:draw_box) {|arg| box = arg }
173
234
  image_path = File.join(TEST_DATA_DIR, 'images', 'gray.jpg')
174
235
 
175
- @composer.image(image_path, width: 10, height: 15)
236
+ @composer.image(image_path, width: 10, height: 15, style: {font_size: 20}, subscript: true)
176
237
  assert_equal(10, box.width)
177
238
  assert_equal(15, box.height)
239
+ assert_equal(20, box.style.font_size)
240
+ assert(box.style.subscript)
178
241
  assert_same(@composer.document.images.add(image_path), box.image)
179
242
  end
180
243
  end
@@ -19,8 +19,8 @@ describe HexaPDF::Dictionary do
19
19
  klass.new(obj, oid: 1)
20
20
  end
21
21
 
22
- def delete(_obj)
23
- _obj.data.value = nil
22
+ def delete(obj)
23
+ obj.data.value = nil
24
24
  end
25
25
 
26
26
  def wrap(obj, type:)