hexapdf 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -1
  3. data/CONTRIBUTERS +1 -1
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -1
  6. data/examples/018-composer.rb +44 -0
  7. data/lib/hexapdf/cli.rb +2 -0
  8. data/lib/hexapdf/cli/command.rb +2 -2
  9. data/lib/hexapdf/cli/optimize.rb +1 -1
  10. data/lib/hexapdf/cli/split.rb +82 -0
  11. data/lib/hexapdf/composer.rb +303 -0
  12. data/lib/hexapdf/configuration.rb +2 -2
  13. data/lib/hexapdf/content/canvas.rb +3 -6
  14. data/lib/hexapdf/dictionary.rb +0 -3
  15. data/lib/hexapdf/document.rb +30 -22
  16. data/lib/hexapdf/document/files.rb +1 -1
  17. data/lib/hexapdf/document/images.rb +1 -1
  18. data/lib/hexapdf/filter/predictor.rb +8 -8
  19. data/lib/hexapdf/layout.rb +1 -0
  20. data/lib/hexapdf/layout/box.rb +55 -12
  21. data/lib/hexapdf/layout/frame.rb +143 -46
  22. data/lib/hexapdf/layout/image_box.rb +96 -0
  23. data/lib/hexapdf/layout/inline_box.rb +10 -0
  24. data/lib/hexapdf/layout/line.rb +1 -1
  25. data/lib/hexapdf/layout/style.rb +55 -3
  26. data/lib/hexapdf/layout/text_box.rb +38 -8
  27. data/lib/hexapdf/layout/text_layouter.rb +66 -52
  28. data/lib/hexapdf/object.rb +3 -2
  29. data/lib/hexapdf/parser.rb +6 -1
  30. data/lib/hexapdf/rectangle.rb +6 -0
  31. data/lib/hexapdf/reference.rb +4 -6
  32. data/lib/hexapdf/revision.rb +34 -8
  33. data/lib/hexapdf/revisions.rb +12 -2
  34. data/lib/hexapdf/stream.rb +18 -0
  35. data/lib/hexapdf/task.rb +1 -1
  36. data/lib/hexapdf/task/dereference.rb +1 -1
  37. data/lib/hexapdf/task/optimize.rb +2 -2
  38. data/lib/hexapdf/type/form.rb +10 -0
  39. data/lib/hexapdf/version.rb +1 -1
  40. data/lib/hexapdf/writer.rb +25 -5
  41. data/man/man1/hexapdf.1 +17 -0
  42. data/test/hexapdf/layout/test_box.rb +7 -1
  43. data/test/hexapdf/layout/test_frame.rb +38 -1
  44. data/test/hexapdf/layout/test_image_box.rb +73 -0
  45. data/test/hexapdf/layout/test_inline_box.rb +8 -0
  46. data/test/hexapdf/layout/test_line.rb +1 -1
  47. data/test/hexapdf/layout/test_style.rb +58 -1
  48. data/test/hexapdf/layout/test_text_box.rb +57 -12
  49. data/test/hexapdf/layout/test_text_layouter.rb +14 -4
  50. data/test/hexapdf/task/test_optimize.rb +3 -3
  51. data/test/hexapdf/test_composer.rb +258 -0
  52. data/test/hexapdf/test_document.rb +29 -3
  53. data/test/hexapdf/test_importer.rb +8 -2
  54. data/test/hexapdf/test_object.rb +3 -1
  55. data/test/hexapdf/test_parser.rb +6 -0
  56. data/test/hexapdf/test_rectangle.rb +7 -0
  57. data/test/hexapdf/test_reference.rb +3 -1
  58. data/test/hexapdf/test_revision.rb +13 -0
  59. data/test/hexapdf/test_revisions.rb +1 -0
  60. data/test/hexapdf/test_stream.rb +13 -0
  61. data/test/hexapdf/test_writer.rb +13 -2
  62. data/test/hexapdf/type/test_annotation.rb +1 -1
  63. data/test/hexapdf/type/test_form.rb +13 -2
  64. metadata +10 -4
@@ -293,9 +293,10 @@ module HexaPDF
293
293
  (oid == other.oid ? gen <=> other.gen : oid <=> other.oid)
294
294
  end
295
295
 
296
- # Returns +true+ if the other object is an Object and wraps the same #data structure.
296
+ # Returns +true+ if the other object is an Object and wraps the same #data structure, or if the
297
+ # other object is a Reference with the same oid/gen.
297
298
  def ==(other)
298
- other.kind_of?(Object) && data == other.data
299
+ (other.kind_of?(Object) && data == other.data) || (other.kind_of?(Reference) && other == self)
299
300
  end
300
301
 
301
302
  # Returns +true+ if the other object references the same PDF object as this object.
@@ -45,6 +45,9 @@ module HexaPDF
45
45
  # See: PDF1.7 s7
46
46
  class Parser
47
47
 
48
+ # The IO stream which is parsed.
49
+ attr_reader :io
50
+
48
51
  # Creates a new parser for the given IO object.
49
52
  #
50
53
  # PDF references are resolved using the associated Document object.
@@ -270,6 +273,8 @@ module HexaPDF
270
273
  #
271
274
  # See: PDF1.7 s7.5.5, ADB1.7 sH.3-3.4.4
272
275
  def startxref_offset
276
+ return @startxref_offset if defined?(@startxref_offset)
277
+
273
278
  @io.seek(0, IO::SEEK_END)
274
279
  step_size = 1024
275
280
  pos = @io.pos
@@ -301,7 +306,7 @@ module HexaPDF
301
306
  force: eof_index < 2 || lines[eof_index - 2].strip != "startxref")
302
307
  end
303
308
 
304
- lines[eof_index - 1].to_i
309
+ @startxref_offset = lines[eof_index - 1].to_i
305
310
  end
306
311
 
307
312
  # Returns the PDF version number that is stored in the file header.
@@ -81,6 +81,12 @@ module HexaPDF
81
81
  value[3] - value[1]
82
82
  end
83
83
 
84
+ # Compares this rectangle to +other+ like in Object#== but also allows comparison to simple
85
+ # arrays if the rectangle is a direct object.
86
+ def ==(other)
87
+ super || (other.kind_of?(Array) && !indirect? && other == data.value)
88
+ end
89
+
84
90
  private
85
91
 
86
92
  # Ensures that the value is an array containing four numbers that specify the bottom left and
@@ -73,16 +73,14 @@ module HexaPDF
73
73
  (oid == other.oid ? gen <=> other.gen : oid <=> other.oid)
74
74
  end
75
75
 
76
- # Returns +true+ if the other object is a Reference and has the same object and generation
77
- # numbers.
78
- def ==(other)
79
- other.kind_of?(Reference) && oid == other.oid && gen == other.gen
80
- end
81
-
82
76
  # Returns +true+ if the other object references the same PDF object as this reference object.
77
+ #
78
+ # This is necessary so that Object and Reference objects can be used as interchangable hash
79
+ # keys and can be compared.
83
80
  def eql?(other)
84
81
  other.respond_to?(:oid) && oid == other.oid && other.respond_to?(:gen) && gen == other.gen
85
82
  end
83
+ alias == eql?
86
84
 
87
85
  # Computes the hash value based on the object and generation numbers.
88
86
  def hash
@@ -164,17 +164,18 @@ module HexaPDF
164
164
  end
165
165
 
166
166
  # :call-seq:
167
- # revision.each {|obj| block } -> revision
168
- # revision.each -> Enumerator
167
+ # revision.each(only_loaded: false) {|obj| block } -> revision
168
+ # revision.each(only_loaded: false) -> Enumerator
169
169
  #
170
- # Calls the given block once for every object of the revision.
170
+ # Calls the given block for every object of the revision, or, if +only_loaded+ is +true+, for
171
+ # every already loaded object.
171
172
  #
172
- # Objects that are loadable via an associated cross-reference section but are currently not,
173
- # are loaded automatically.
174
- def each
175
- return to_enum(__method__) unless block_given?
173
+ # Objects that are loadable via an associated cross-reference section but are currently not
174
+ # loaded, are loaded automatically if +only_loaded+ is +false+.
175
+ def each(only_loaded: false)
176
+ return to_enum(__method__, only_loaded: only_loaded) unless block_given?
176
177
 
177
- if defined?(@all_objects_loaded)
178
+ if defined?(@all_objects_loaded) || only_loaded
178
179
  @objects.each {|_oid, _gen, data| yield(data) }
179
180
  else
180
181
  seen = {}
@@ -189,6 +190,31 @@ module HexaPDF
189
190
  self
190
191
  end
191
192
 
193
+ # :call-seq:
194
+ # revision.each_modified_object {|obj| block } -> revision
195
+ # revision.each_modified_object -> Enumerator
196
+ #
197
+ # Calls the given block once for each object that has been modified since it was loaded.
198
+ #
199
+ # Note that this also means that for revisions without an associated cross-reference section all
200
+ # loaded objects will be yielded.
201
+ def each_modified_object
202
+ return to_enum(__method__) unless block_given?
203
+
204
+ @objects.each do |oid, gen, obj|
205
+ if @xref_section.entry?(oid, gen)
206
+ stored_obj = @loader.call(@xref_section[oid, gen])
207
+ if obj.data.value != stored_obj.data.value || obj.data.stream != stored_obj.data.stream
208
+ yield(obj)
209
+ end
210
+ else
211
+ yield(obj)
212
+ end
213
+ end
214
+
215
+ self
216
+ end
217
+
192
218
  private
193
219
 
194
220
  # Loads a single object from the associated cross-reference section.
@@ -88,13 +88,16 @@ module HexaPDF
88
88
  end
89
89
 
90
90
  document.version = parser.file_header_version
91
- new(document, initial_revisions: revisions)
91
+ new(document, initial_revisions: revisions, parser: parser)
92
92
  end
93
93
 
94
94
  end
95
95
 
96
96
  include Enumerable
97
97
 
98
+ # The Parser instance used for reading the initial revisions.
99
+ attr_reader :parser
100
+
98
101
  # Creates a new revisions object for the given PDF document.
99
102
  #
100
103
  # Options:
@@ -102,8 +105,15 @@ module HexaPDF
102
105
  # initial_revisions::
103
106
  # An array of revisions that should initially be used. If this option is not specified, a
104
107
  # single empty revision is added.
105
- def initialize(document, initial_revisions: nil)
108
+ #
109
+ # parser::
110
+ # The parser with which the initial revisions were read. If this option is not specified
111
+ # even though the document was read from an IO stream, some parts may not work, like
112
+ # incremental writing.
113
+ def initialize(document, initial_revisions: nil, parser: nil)
106
114
  @document = document
115
+ @parser = parser
116
+
107
117
  @revisions = []
108
118
  if initial_revisions
109
119
  @revisions += initial_revisions
@@ -96,6 +96,24 @@ module HexaPDF
96
96
  end
97
97
  end
98
98
 
99
+ # Returns whether this stream data object is equal to the other stream data object.
100
+ def ==(other)
101
+ other.kind_of?(StreamData) &&
102
+ source == other.source && offset == other.offset && length == other.length &&
103
+ filter == other.filter && decode_parms == other.decode_parms
104
+ end
105
+
106
+ protected
107
+
108
+ # The source.
109
+ attr_reader :source
110
+
111
+ # The optional offset into the bytes provided by source.
112
+ attr_reader :offset
113
+
114
+ # The optional number of bytes to use starting from offset.
115
+ attr_reader :length
116
+
99
117
  end
100
118
 
101
119
  # Implements Stream objects of the PDF object system.
@@ -55,7 +55,7 @@ module HexaPDF
55
55
  #
56
56
  # doc = HexaPDF::Document.new
57
57
  # doc.config['task.map'][:validate] = lambda do |doc|
58
- # doc.each(current: false) {|obj| obj.validate || raise "Invalid object #{obj}"}
58
+ # doc.each(only_current: false) {|obj| obj.validate || raise "Invalid object #{obj}"}
59
59
  # end
60
60
  module Task
61
61
 
@@ -69,7 +69,7 @@ module HexaPDF
69
69
  else
70
70
  dereference(@doc.trailer)
71
71
  @result = []
72
- @doc.each(current: false) do |obj|
72
+ @doc.each(only_current: false) do |obj|
73
73
  if !@seen.key?(obj.data) && obj.type != :ObjStm && obj.type != :XRef
74
74
  @result << obj
75
75
  elsif obj.kind_of?(HexaPDF::Stream) && (val = obj.value[:Length]) &&
@@ -78,7 +78,7 @@ module HexaPDF
78
78
  elsif xref_streams != :preserve
79
79
  process_xref_streams(doc, xref_streams)
80
80
  else
81
- doc.each(current: false, &method(:delete_fields_with_defaults))
81
+ doc.each(only_current: false, &method(:delete_fields_with_defaults))
82
82
  end
83
83
 
84
84
  compress_pages(doc) if compress_pages
@@ -169,7 +169,7 @@ module HexaPDF
169
169
  def self.process_xref_streams(doc, method)
170
170
  case method
171
171
  when :delete
172
- doc.each(current: false) do |obj, rev|
172
+ doc.each(only_current: false) do |obj, rev|
173
173
  if obj.type == :XRef
174
174
  rev.delete(obj)
175
175
  else
@@ -72,6 +72,16 @@ module HexaPDF
72
72
  self[:BBox]
73
73
  end
74
74
 
75
+ # Returns the width of the bounding box (see #box).
76
+ def width
77
+ box.width
78
+ end
79
+
80
+ # Returns the height of the bounding box (see #box).
81
+ def height
82
+ box.height
83
+ end
84
+
75
85
  # Returns the contents of the form XObject.
76
86
  #
77
87
  # Note: This is the same as #stream but here for interface compatibility with Page.
@@ -34,6 +34,6 @@
34
34
  module HexaPDF
35
35
 
36
36
  # The version of HexaPDF.
37
- VERSION = '0.8.0'
37
+ VERSION = '0.9.0'
38
38
 
39
39
  end
@@ -41,9 +41,14 @@ module HexaPDF
41
41
  # Writes the contents of a PDF document to an IO stream.
42
42
  class Writer
43
43
 
44
- # Writes the document to the IO object.
45
- def self.write(document, io)
46
- new(document, io).write
44
+ # Writes the document to the IO object. If +incremental+ is +true+ and the document was created
45
+ # from an existing PDF file, the changes are appended to a full copy of the source document.
46
+ def self.write(document, io, incremental: false)
47
+ if incremental && document.revisions.parser
48
+ new(document, io).write_incremental
49
+ else
50
+ new(document, io).write
51
+ end
47
52
  end
48
53
 
49
54
  # Creates a new writer object for the given HexaPDF document that gets written to the IO
@@ -56,13 +61,12 @@ module HexaPDF
56
61
  @io.seek(0, IO::SEEK_SET) # TODO: incremental update!
57
62
 
58
63
  @serializer = Serializer.new
64
+ @serializer.encrypter = @document.encrypted? ? @document.security_handler : nil
59
65
  @rev_size = 0
60
66
  end
61
67
 
62
68
  # Writes the document to the IO object.
63
69
  def write
64
- @serializer.encrypter = @document.encrypted? ? @document.security_handler : nil
65
-
66
70
  write_file_header
67
71
 
68
72
  pos = nil
@@ -72,6 +76,22 @@ module HexaPDF
72
76
  end
73
77
  end
74
78
 
79
+ # Writes the complete source document and one revision containing all changes to the IO.
80
+ #
81
+ # For this method to work the document must have been created from an existing file.
82
+ def write_incremental
83
+ @document.revisions.parser.io.seek(0, IO::SEEK_SET)
84
+ IO.copy_stream(@document.revisions.parser.io, @io)
85
+
86
+ @rev_size = @document.revisions.current.next_free_oid
87
+
88
+ revision = Revision.new(@document.revisions.current.trailer)
89
+ @document.revisions.each do |rev|
90
+ rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
91
+ end
92
+ write_revision(revision, @document.revisions.parser.startxref_offset)
93
+ end
94
+
75
95
  private
76
96
 
77
97
  # Writes the PDF file header.
@@ -29,6 +29,8 @@ Modifying an existing PDF file (see the \fBmodify\fP command)
29
29
  .IP \(bu 4
30
30
  Optimizing the file size of a PDF file (see the \fBoptimize\fP command)
31
31
  .IP \(bu 4
32
+ Splitting a PDF file into individual pages (see the \fBsplit\fP command)
33
+ .IP \(bu 4
32
34
  Batch execution of a command on multiple PDF files (see the \fBbatch\fP command)
33
35
  .PD
34
36
  .P
@@ -316,6 +318,17 @@ By default, all strategies except page compression are used since page compressi
316
318
  The password to decrypt the \fIINPUT\fP\&\. Use \fB\-\fP for \fIPASSWORD\fP for reading it from standard input\.
317
319
  .P
318
320
  The \fBOptimization Options\fP can be used with this command\. Note that the defaults are changed to provide good compression out of the box\.
321
+ .SS "split"
322
+ Synopsis: \fBsplit\fP [\fBOPTIONS\fP] \fIINPUT\fP [\fIOUTPUT_SPEC\fP]
323
+ .P
324
+ This command splits the input file into multiple output files, each containing one page\.
325
+ .P
326
+ If no \fIOUTPUT_SPEC\fP is given, files of the form \fIINPUT_0001\.pdf\fP, \fIINPUT_0002\.pdf\fP, \.\.\. and so on are created (only the name \fIINPUT\fP without the file extension is used)\. Otherwise \fIOUTPUT_SPEC\fP determines the file names of the created files, with a printf\-style format string like \[u2018]%04d\[u2019] being replaced by the page number\. For example, if the files should be named \fIpage_01\.pdf\fP, \fIpage_02\.pdf\fP and so on, use \fIpage_%02d\.pdf\fP for the \fIOUTPUT_SPEC\fP\&\.
327
+ .TP
328
+ \fB\-p\fP \fIPASSWORD\fP, \fB\-\-password\fP \fIPASSWORD\fP
329
+ The password to decrypt the \fIINPUT\fP\&\. Use \fB\-\fP for \fIPASSWORD\fP for reading it from standard input\.
330
+ .P
331
+ Additionally, the \fBOptimization Options\fP and \fBEncryption Options\fP can be used\.
319
332
  .SS "version"
320
333
  This command shows the version of the hexapdf application\. It is an alternative to using the global \fB\-\-version\fP option\.
321
334
  .SH "PAGES SPECIFICATION"
@@ -392,6 +405,10 @@ Encryption removal: Create the \fBoutput\.pdf\fP as copy of \fBinput\.pdf\fP but
392
405
  \fBhexapdf optimize input\.pdf output\.pdf\fP
393
406
  .P
394
407
  Optimization: Compress the \fBinput\.pdf\fP to get a smaller file size\.
408
+ .SS "split"
409
+ \fBhexapdf split input\.pdf out_%02d\.pdf\fP
410
+ .P
411
+ Split the \fBinput\.pdf\fP into individual pages, naming the output files \fBout_01\.pdf\fP, \fBout_02\.pdf\fP, and so on\.
395
412
  .SS "files"
396
413
  \fBhexapdf files input\.pdf\fP
397
414
  .br
@@ -24,7 +24,8 @@ describe HexaPDF::Layout::Box do
24
24
 
25
25
  it "takes content width and height" do
26
26
  box = HexaPDF::Layout::Box.create(width: 100, height: 200, content_box: true,
27
- padding: 10, border: {width: 10})
27
+ padding: [10, 8, 6, 4],
28
+ border: {width: [10, 8, 6, 4]})
28
29
  assert_equal(100, box.content_width)
29
30
  assert_equal(200, box.content_height)
30
31
  end
@@ -78,6 +79,11 @@ describe HexaPDF::Layout::Box do
78
79
  end
79
80
  end
80
81
 
82
+ it "can't be split into two parts" do
83
+ box = create_box(width: 100, height: 100)
84
+ assert_equal([nil, box], box.split(50, 50, nil))
85
+ end
86
+
81
87
  describe "draw" do
82
88
  it "draws the box onto the canvas" do
83
89
  box = create_box(width: 150, height: 130) do |canvas, _|
@@ -40,7 +40,7 @@ describe HexaPDF::Layout::Frame do
40
40
  assert_kind_of(HexaPDF::Layout::WidthFromPolygon, ws)
41
41
  end
42
42
 
43
- describe "draw" do
43
+ describe "fit and draw" do
44
44
  before do
45
45
  @frame = HexaPDF::Layout::Frame.new(10, 10, 100, 100)
46
46
  @canvas = Minitest::Mock.new
@@ -80,6 +80,14 @@ describe HexaPDF::Layout::Frame do
80
80
  )
81
81
  end
82
82
 
83
+ it "determines the available space for #fit by using the space to the right and above" do
84
+ check_box(
85
+ {position: :absolute, position_hint: [10, 10]},
86
+ [20, 20],
87
+ [[[10, 10], [110, 10], [110, 20], [20, 20], [20, 110], [10, 110]]]
88
+ )
89
+ end
90
+
83
91
  it "always removes the whole margin box from the frame" do
84
92
  check_box(
85
93
  {width: 50, height: 50, position: :absolute, position_hint: [10, 10],
@@ -238,6 +246,26 @@ describe HexaPDF::Layout::Frame do
238
246
  box = HexaPDF::Layout::Box.create(width: 150, height: 50)
239
247
  refute(@frame.draw(@canvas, box))
240
248
  end
249
+
250
+ it "can't fit the box if there is no available space" do
251
+ @frame.remove_area(Geom2D::Polygon([0, 0], [110, 0], [110, 110], [0, 110]))
252
+ box = HexaPDF::Layout::Box.create
253
+ refute(@frame.fit(box))
254
+ end
255
+
256
+ it "draws the box even if the box's height is zero" do
257
+ box = HexaPDF::Layout::Box.create
258
+ box.define_singleton_method(:height) { 0 }
259
+ assert(@frame.draw(@canvas, box))
260
+ end
261
+ end
262
+
263
+ describe "split" do
264
+ it "splits the box if necessary" do
265
+ box = HexaPDF::Layout::Box.create(width: 10, height: 10)
266
+ assert_equal([nil, box], @frame.split(box))
267
+ assert_nil(@frame.instance_variable_get(:@fit_data).box)
268
+ end
241
269
  end
242
270
 
243
271
  describe "find_next_region" do
@@ -310,4 +338,13 @@ describe HexaPDF::Layout::Frame do
310
338
  end
311
339
  end
312
340
 
341
+ describe "remove_area" do
342
+ it "recalculates the contour line only if necessary" do
343
+ contour = Geom2D::Polygon([10, 10], [10, 90], [90, 90], [90, 10])
344
+ frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100, contour_line: contour)
345
+ frame.remove_area(Geom2D::Polygon([0, 0], [20, 0], [20, 100], [0, 100]))
346
+ assert_equal([[[20, 10], [90, 10], [90, 90], [20, 90]]],
347
+ frame.contour_line.polygons.map(&:to_a))
348
+ end
349
+ end
313
350
  end
@@ -0,0 +1,73 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require_relative '../content/common'
5
+ require 'hexapdf/document'
6
+ require 'hexapdf/layout/image_box'
7
+
8
+ describe HexaPDF::Layout::ImageBox do
9
+ before do
10
+ @image = HexaPDF::Stream.new({Subtype: :Image}, stream: '')
11
+ @image.define_singleton_method(:width) { 40 }
12
+ @image.define_singleton_method(:height) { 20 }
13
+ end
14
+
15
+ def create_box(**kwargs)
16
+ HexaPDF::Layout::ImageBox.new(@image, kwargs)
17
+ end
18
+
19
+ describe "initialize" do
20
+ it "takes the image to be displayed" do
21
+ box = create_box
22
+ assert_equal(@image, box.image)
23
+ end
24
+ end
25
+
26
+ describe "fit" do
27
+ it "fits with fixed dimensions" do
28
+ box = create_box(width: 50, height: 30, style: {padding: [10, 4, 6, 2]})
29
+ assert(box.fit(100, 100, nil))
30
+ assert_equal(50, box.width)
31
+ assert_equal(30, box.height)
32
+ end
33
+
34
+ it "fits with a fixed width" do
35
+ box = create_box(width: 60, style: {padding: [10, 4, 6, 2]})
36
+ assert(box.fit(100, 100, nil))
37
+ assert_equal(60, box.width)
38
+ assert_equal(43, box.height)
39
+ end
40
+
41
+ it "fits with a fixed height" do
42
+ box = create_box(height: 40, style: {padding: [10, 4, 6, 2]})
43
+ assert(box.fit(100, 100, nil))
44
+ assert_equal(54, box.width)
45
+ assert_equal(40, box.height)
46
+ end
47
+
48
+ it "fits with auto-scaling to available space" do
49
+ box = create_box(style: {padding: [10, 4, 6, 2]})
50
+ assert(box.fit(100, 100, nil))
51
+ assert_equal(100, box.width)
52
+ assert_equal(63, box.height)
53
+
54
+ assert(box.fit(100, 30, nil))
55
+ assert_equal(34, box.width)
56
+ assert_equal(30, box.height)
57
+ end
58
+ end
59
+
60
+ describe "draw" do
61
+ it "draws the image" do
62
+ box = create_box(height: 40, style: {padding: [10, 4, 6, 2]})
63
+ box.fit(100, 100, nil)
64
+
65
+ @canvas = HexaPDF::Document.new.pages.add.canvas
66
+ box.draw(@canvas, 0, 0)
67
+ assert_operators(@canvas.contents, [[:save_graphics_state],
68
+ [:concatenate_matrix, [48, 0, 0, 24, 2, 6]],
69
+ [:paint_xobject, [:XO1]],
70
+ [:restore_graphics_state]])
71
+ end
72
+ end
73
+ end