hexapdf 0.8.0 → 0.9.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 (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
@@ -443,7 +443,7 @@ module HexaPDF
443
443
  XXViewerPreferences: 'HexaPDF::Type::ViewerPreferences',
444
444
  Action: 'HexaPDF::Type::Action',
445
445
  XXLaunchActionWinParameters: 'HexaPDF::Type::Actions::Launch::WinParameters',
446
- Annotation: 'HexaPDF::Type::Annotation',
446
+ Annot: 'HexaPDF::Type::Annotation',
447
447
  },
448
448
  'object.subtype_map' => {
449
449
  nil => {
@@ -479,7 +479,7 @@ module HexaPDF
479
479
  Launch: 'HexaPDF::Type::Actions::Launch',
480
480
  URI: 'HexaPDF::Type::Actions::URI',
481
481
  },
482
- Annotation: {
482
+ Annot: {
483
483
  Text: 'HexaPDF::Type::Annotations::Text',
484
484
  Link: 'HexaPDF::Type::Annotations::Link',
485
485
  },
@@ -1258,12 +1258,9 @@ module HexaPDF
1258
1258
  obj = context.document.images.add(obj)
1259
1259
  end
1260
1260
 
1261
- if obj[:Subtype] == :Image
1262
- width, height = calculate_dimensions(obj[:Width], obj[:Height],
1263
- rwidth: width, rheight: height)
1264
- else
1265
- width, height = calculate_dimensions(obj.box.width, obj.box.height,
1266
- rwidth: width, rheight: height)
1261
+ width, height = calculate_dimensions(obj.width, obj.height,
1262
+ rwidth: width, rheight: height)
1263
+ if obj[:Subtype] != :Image
1267
1264
  width /= obj.box.width.to_f
1268
1265
  height /= obj.box.height.to_f
1269
1266
  at[0] -= obj.box.left
@@ -261,9 +261,6 @@ module HexaPDF
261
261
  each_set_key_or_required_field do |name, field|
262
262
  obj = key?(name) ? self[name] : nil
263
263
 
264
- # Validate nested objects
265
- validate_nested(obj, &block)
266
-
267
264
  # The checks below need a valid field definition
268
265
  next if field.nil?
269
266
 
@@ -57,6 +57,8 @@ require 'hexapdf/layout'
57
57
  # * HexaPDF::Content::Canvas provides the canvas API for drawing/writing on a page or form XObject
58
58
  module HexaPDF
59
59
 
60
+ autoload(:Composer, 'hexapdf/composer')
61
+
60
62
  # Represents one PDF document.
61
63
  #
62
64
  # A PDF document consists of (indirect) objects, so the main job of this class is to provide
@@ -367,12 +369,13 @@ module HexaPDF
367
369
  end
368
370
 
369
371
  # :call-seq:
370
- # doc.each(current: true) {|obj| block } -> doc
371
- # doc.each(current: true) {|obj, rev| block } -> doc
372
- # doc.each(current: true) -> Enumerator
372
+ # doc.each(only_current: true, only_loaded: false) {|obj| block } -> doc
373
+ # doc.each(only_current: true, only_loaded: false) {|obj, rev| block } -> doc
374
+ # doc.each(only_current: true, only_loaded: false) -> Enumerator
373
375
  #
374
- # Calls the given block once for every object in the PDF document. The block may either accept
375
- # only the object or the object and the revision it is in.
376
+ # Calls the given block once for every object, or, if +only_loaded+ is +true+, for every loaded
377
+ # object in the PDF document. The block may either accept only the object or the object and the
378
+ # revision it is in.
376
379
  #
377
380
  # By default, only the current version of each object is returned which implies that each
378
381
  # object number is yielded exactly once. If the +current+ option is +false+, all stored
@@ -387,14 +390,16 @@ module HexaPDF
387
390
  # * Additionally, there may also be objects with the same object number but different
388
391
  # generation numbers in different revisions, e.g. one object with oid/gen [3,0] and one with
389
392
  # oid/gen [3,1].
390
- def each(current: true, &block)
391
- return to_enum(__method__, current: current) unless block_given?
393
+ def each(only_current: true, only_loaded: false, &block)
394
+ unless block_given?
395
+ return to_enum(__method__, only_current: only_current, only_loaded: only_loaded)
396
+ end
392
397
 
393
398
  yield_rev = (block.arity == 2)
394
399
  oids = {}
395
400
  @revisions.reverse_each do |rev|
396
- rev.each do |obj|
397
- next if current && oids.include?(obj.oid)
401
+ rev.each(only_loaded: only_loaded) do |obj|
402
+ next if only_current && oids.include?(obj.oid)
398
403
  (yield_rev ? yield(obj, rev) : yield(obj))
399
404
  oids[obj.oid] = true
400
405
  end
@@ -553,22 +558,18 @@ module HexaPDF
553
558
  @security_handler
554
559
  end
555
560
 
556
- # :call-seq:
557
- # doc.validate(auto_correct: true) -> true or false
558
- # doc.validate(auto_correct: true) {|object, msg, correctable| block } -> true or false
559
- #
560
- # Validates all objects of the document, with optional auto-correction, and returns +true+ if
561
- # everything is fine.
561
+ # Validates all objects, or, if +only_loaded+ is +true+, only loaded objects, with optional
562
+ # auto-correction, and returns +true+ if everything is fine.
562
563
  #
563
564
  # If a block is given, it is called on validation problems.
564
565
  #
565
566
  # See HexaPDF::Object#validate for more information.
566
- def validate(auto_correct: true)
567
+ def validate(auto_correct: true, only_loaded: false) #:yield: object, msg, correctable
567
568
  cur_obj = trailer
568
569
  block = (block_given? ? lambda {|msg, correctable| yield(cur_obj, msg, correctable) } : nil)
569
570
 
570
571
  result = trailer.validate(auto_correct: auto_correct, &block)
571
- each(current: false) do |obj|
572
+ each(only_current: false, only_loaded: only_loaded) do |obj|
572
573
  cur_obj = obj
573
574
  result &&= obj.validate(auto_correct: auto_correct, &block)
574
575
  end
@@ -576,8 +577,8 @@ module HexaPDF
576
577
  end
577
578
 
578
579
  # :call-seq:
579
- # doc.write(filename, validate: true, update_fields: true, optimize: false)
580
- # doc.write(io, validate: true, update_fields: true, optimize: false)
580
+ # doc.write(filename, incremental: false, validate: true, update_fields: true, optimize: false)
581
+ # doc.write(io, incremental: false, validate: true, update_fields: true, optimize: false)
581
582
  #
582
583
  # Writes the document to the given file (in case +io+ is a String) or IO stream.
583
584
  #
@@ -586,6 +587,13 @@ module HexaPDF
586
587
  #
587
588
  # Options:
588
589
  #
590
+ # incremental::
591
+ # Use the incremental writing mode which just adds a new revision to an existing document.
592
+ # This is needed, for example, when modifying a signed PDF and the original signature should
593
+ # stay valid.
594
+ #
595
+ # See: PDF1.7 s7.5.6
596
+ #
589
597
  # validate::
590
598
  # Validates the document and raises an error if an uncorrectable problem is found.
591
599
  #
@@ -596,7 +604,7 @@ module HexaPDF
596
604
  # optimize::
597
605
  # Optimize the file size by using object and cross-reference streams. This will raise the PDF
598
606
  # version to at least 1.5.
599
- def write(file_or_io, validate: true, update_fields: true, optimize: false)
607
+ def write(file_or_io, incremental: false, validate: true, update_fields: true, optimize: false)
600
608
  dispatch_message(:complete_objects)
601
609
 
602
610
  if update_fields
@@ -619,9 +627,9 @@ module HexaPDF
619
627
  dispatch_message(:before_write)
620
628
 
621
629
  if file_or_io.kind_of?(String)
622
- File.open(file_or_io, 'w+') {|file| Writer.write(self, file) }
630
+ File.open(file_or_io, 'w+') {|file| Writer.write(self, file, incremental: incremental) }
623
631
  else
624
- Writer.write(self, file_or_io)
632
+ Writer.write(self, file_or_io, incremental: incremental)
625
633
  end
626
634
  end
627
635
 
@@ -101,7 +101,7 @@ module HexaPDF
101
101
  return to_enum(__method__, search: search) unless block_given?
102
102
 
103
103
  if search
104
- @document.each(current: false) do |obj|
104
+ @document.each(only_current: false) do |obj|
105
105
  yield(obj) if obj.type == :Filespec
106
106
  end
107
107
  else
@@ -85,7 +85,7 @@ module HexaPDF
85
85
  # Note that only real images are yielded which means, for example, that images used as soft
86
86
  # mask are not.
87
87
  def each(&block)
88
- images = @document.each(current: false).select do |obj|
88
+ images = @document.each(only_current: false).select do |obj|
89
89
  next unless obj.kind_of?(HexaPDF::Dictionary)
90
90
  obj[:Subtype] == :Image && !obj[:ImageMask]
91
91
  end
@@ -163,16 +163,16 @@ module HexaPDF
163
163
  case data.getbyte(pos)
164
164
  when PREDICTOR_PNG_SUB
165
165
  bytes_per_pixel.upto(bytes_per_row - 2) do |i|
166
- line.setbyte(i, line.getbyte(i) + line.getbyte(i - bytes_per_pixel))
166
+ line.setbyte(i, (line.getbyte(i) + line.getbyte(i - bytes_per_pixel)) % 256)
167
167
  end
168
168
  when PREDICTOR_PNG_UP
169
169
  0.upto(bytes_per_row - 2) do |i|
170
- line.setbyte(i, line.getbyte(i) + last_line.getbyte(i))
170
+ line.setbyte(i, (line.getbyte(i) + last_line.getbyte(i)) % 256)
171
171
  end
172
172
  when PREDICTOR_PNG_AVERAGE
173
173
  0.upto(bytes_per_row - 2) do |i|
174
174
  a = i < bytes_per_pixel ? 0 : line.getbyte(i - bytes_per_pixel)
175
- line.setbyte(i, line.getbyte(i) + ((a + last_line.getbyte(i)) >> 1))
175
+ line.setbyte(i, (line.getbyte(i) + ((a + last_line.getbyte(i)) >> 1)) % 256)
176
176
  end
177
177
  when PREDICTOR_PNG_PAETH
178
178
  0.upto(bytes_per_row - 2) do |i|
@@ -187,7 +187,7 @@ module HexaPDF
187
187
 
188
188
  point = ((pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c))
189
189
 
190
- line.setbyte(i, line.getbyte(i) + point)
190
+ line.setbyte(i, (line.getbyte(i) + point) % 256)
191
191
  end
192
192
  end
193
193
 
@@ -202,16 +202,16 @@ module HexaPDF
202
202
  case predictor
203
203
  when PREDICTOR_PNG_SUB
204
204
  bytes_per_row.downto(bytes_per_pixel + 1) do |i|
205
- line.setbyte(i, line.getbyte(i) - line.getbyte(i - bytes_per_pixel))
205
+ line.setbyte(i, (line.getbyte(i) - line.getbyte(i - bytes_per_pixel)) % 256)
206
206
  end
207
207
  when PREDICTOR_PNG_UP
208
208
  bytes_per_row.downto(1) do |i|
209
- line.setbyte(i, line.getbyte(i) - last_line.getbyte(i))
209
+ line.setbyte(i, (line.getbyte(i) - last_line.getbyte(i)) % 256)
210
210
  end
211
211
  when PREDICTOR_PNG_AVERAGE
212
212
  bytes_per_row.downto(1) do |i|
213
213
  a = i <= bytes_per_pixel ? 0 : line.getbyte(i - bytes_per_pixel)
214
- line.setbyte(i, line.getbyte(i) - ((a + last_line.getbyte(i)) >> 1))
214
+ line.setbyte(i, (line.getbyte(i) - ((a + last_line.getbyte(i)) >> 1)) % 256)
215
215
  end
216
216
  when PREDICTOR_PNG_PAETH
217
217
  bytes_per_row.downto(1) do |i|
@@ -226,7 +226,7 @@ module HexaPDF
226
226
 
227
227
  point = ((pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c))
228
228
 
229
- line.setbyte(i, line.getbyte(i) - point)
229
+ line.setbyte(i, (line.getbyte(i) - point) % 256)
230
230
  end
231
231
  end
232
232
 
@@ -49,6 +49,7 @@ module HexaPDF
49
49
  autoload(:Frame, 'hexapdf/layout/frame')
50
50
  autoload(:WidthFromPolygon, 'hexapdf/layout/width_from_polygon')
51
51
  autoload(:TextBox, 'hexapdf/layout/text_box')
52
+ autoload(:ImageBox, 'hexapdf/layout/image_box')
52
53
 
53
54
  end
54
55
 
@@ -95,31 +95,49 @@ module HexaPDF
95
95
  @height = @initial_height = height
96
96
  @style = (style.kind_of?(Style) ? style : Style.new(style))
97
97
  @draw_block = block
98
- @outline = nil
99
98
  end
100
99
 
101
100
  # The width of the content box, i.e. without padding and/or borders.
102
101
  def content_width
103
- [0, width - (@style.padding.left + @style.padding.right +
104
- @style.border.width.left + @style.border.width.right)].max
102
+ width = @width - reserved_width
103
+ width < 0 ? 0 : width
105
104
  end
106
105
 
107
106
  # The height of the content box, i.e. without padding and/or borders.
108
107
  def content_height
109
- [0, height - (@style.padding.top + @style.padding.bottom +
110
- @style.border.width.top + @style.border.width.bottom)].max
108
+ height = @height - reserved_height
109
+ height < 0 ? 0 : height
111
110
  end
112
111
 
113
112
  # Fits the box into the Frame and returns +true+ if fitting was successful.
114
113
  #
115
114
  # The default implementation uses the whole available space for width and height if they were
116
115
  # initially set to 0. Otherwise the specified dimensions are used.
117
- def fit(available_width, available_height, frame)
116
+ def fit(available_width, available_height, _frame)
118
117
  @width = (@initial_width > 0 ? @initial_width : available_width)
119
118
  @height = (@initial_height > 0 ? @initial_height : available_height)
120
119
  @width <= available_width && @height <= available_height
121
120
  end
122
121
 
122
+ # Tries to split the box into two, the first of which needs to fit into the available space,
123
+ # and returns the parts as array.
124
+ #
125
+ # In many cases the first box in the list will be this box, meaning that even when #fit fails,
126
+ # a part of the box may still fit. Note that #fit may not be called if the first box is this
127
+ # box since it is assumed that it is already fitted. If not even a part of this box fits into
128
+ # the available space, +nil+ should be returned as the first array element.
129
+ #
130
+ # Possible return values:
131
+ #
132
+ # [self]:: The box fully fits into the available space.
133
+ # [nil, self]:: The box can't be split or no part of the box fits into the available space.
134
+ # [self, new_box]:: A part of the box fits and a new box is returned for the rest.
135
+ #
136
+ # This default implementation provides no splitting functionality.
137
+ def split(_available_width, _available_height, _frame)
138
+ [nil, self]
139
+ end
140
+
123
141
  # Draws the content of the box onto the canvas at the position (x, y).
124
142
  #
125
143
  # The coordinate system is translated so that the origin is at the bottom left corner of the
@@ -139,12 +157,11 @@ module HexaPDF
139
157
  style.underlays.draw(canvas, x, y, self) if style.underlays?
140
158
  style.border.draw(canvas, x, y, width, height) if style.border?
141
159
 
142
- if @draw_block
143
- canvas.translate(x + style.padding.left + style.border.width.left,
144
- y + style.padding.bottom + style.border.width.bottom) do
145
- @draw_block.call(canvas, self)
146
- end
147
- end
160
+ cx = x
161
+ cy = y
162
+ (cx += style.padding.left; cy += style.padding.bottom) if style.padding?
163
+ (cx += style.border.width.left; cy += style.border.width.bottom) if style.border?
164
+ draw_content(canvas, cx, cy)
148
165
 
149
166
  style.overlays.draw(canvas, x, y, self) if style.overlays?
150
167
  end
@@ -158,6 +175,32 @@ module HexaPDF
158
175
  (style.overlays? && !style.overlays.none?))
159
176
  end
160
177
 
178
+ private
179
+
180
+ # Returns the width that is reserved by the padding and border style properties.
181
+ def reserved_width
182
+ result = 0
183
+ result += style.padding.left + style.padding.right if style.padding?
184
+ result += style.border.width.left + style.border.width.right if style.border?
185
+ result
186
+ end
187
+
188
+ # Returns the height that is reserved by the padding and border style properties.
189
+ def reserved_height
190
+ result = 0
191
+ result += style.padding.top + style.padding.bottom if style.padding?
192
+ result += style.border.width.top + style.border.width.bottom if style.border?
193
+ result
194
+ end
195
+
196
+ # Draws the content of the box at position [x, y] which is the bottom-left corner of the
197
+ # content box.
198
+ def draw_content(canvas, x, y)
199
+ if @draw_block
200
+ canvas.translate(x, y) { @draw_block.call(canvas, self) }
201
+ end
202
+ end
203
+
161
204
  end
162
205
 
163
206
  end
@@ -42,11 +42,35 @@ module HexaPDF
42
42
  #
43
43
  # == Usage
44
44
  #
45
- # After a Frame object is initialized, the #draw method can be used to draw a box onto frame. If
46
- # drawing is successful, the next box can be drawn. Otherwise, #find_next_region should be
47
- # called to determine the next region for placing the box. If the call returns +true+, a region
48
- # was found and #draw can be tried again. Once #find_next_region returns +false+ the frame has
49
- # no more space for placing boxes.
45
+ # After a Frame object is initialized, it is ready for drawing boxes on it.
46
+ #
47
+ # The explicit way of drawing a box follows these steps:
48
+ #
49
+ # * Call #fit with the box to see if the box can fit into the currently selected region of
50
+ # available space. If fitting is successful, the box can be drawn using #draw.
51
+ #
52
+ # The method #fit is also called for absolutely positioned boxes but since these boxes are not
53
+ # subject to the normal constraints, the available space used is the width and height inside
54
+ # the frame to the right and top of the bottom-left corner of the box.
55
+ #
56
+ # * If the box didn't fit, call #find_next_region to determine the next region for placing the
57
+ # box. If a new region was found, start over with #fit. Otherwise the frame has no more space
58
+ # for placing boxes.
59
+ #
60
+ # * Alternatively to calling #find_next_region it is also possible to call #split. This method
61
+ # tries to split the box into two so that the first part fits into the current region. If
62
+ # splitting is successful, the first box can be drawn (Make sure that the second box is
63
+ # handled correctly). Otherwise, start over with #find_next_region.
64
+ #
65
+ # For applications where splitting is not necessary, an easier way is to just use #draw and
66
+ # #find_next_region together, as #draw calls #fit if the box was not fit into the current
67
+ # region.
68
+ #
69
+ # == Used Box Properties
70
+ #
71
+ # The style properties "position", "position_hint" and "margin" are taken into account when
72
+ # fitting, splitting or drawing a box. Note that the margin is ignored if a box's side coincides
73
+ # with the frame's original boundary.
50
74
  #
51
75
  # == Frame Shape and Contour Line
52
76
  #
@@ -63,6 +87,42 @@ module HexaPDF
63
87
 
64
88
  include Geom2D::Utils
65
89
 
90
+ # Internal class for storing data of a fitted box.
91
+ class FitData
92
+
93
+ # The box that was fitted into the frame.
94
+ attr_accessor :box
95
+
96
+ # The available width for this particular box.
97
+ attr_accessor :available_width
98
+
99
+ # The available height for this particular box.
100
+ attr_accessor :available_height
101
+
102
+ # The left margin to use instead of +box.style.margin.left+.
103
+ attr_accessor :margin_left
104
+
105
+ # The right margin to use instead of +box.style.margin.right+.
106
+ attr_accessor :margin_right
107
+
108
+ # The top margin to use instead of +box.style.margin.top+.
109
+ attr_accessor :margin_top
110
+
111
+ # Initialize the object by calling #reset.
112
+ def initialize
113
+ reset
114
+ end
115
+
116
+ # Resets the object.
117
+ def reset(box = nil, available_width = 0, available_height = 0)
118
+ @box = box
119
+ @available_width = available_width
120
+ @available_height = available_height
121
+ @margin_left = @margin_right = @margin_top = 0
122
+ end
123
+
124
+ end
125
+
66
126
  # The x-coordinate of the bottom-left corner.
67
127
  attr_reader :left
68
128
 
@@ -78,12 +138,9 @@ module HexaPDF
78
138
  # The shape of the frame, a Geom2D::PolygonSet consisting of rectilinear polygons.
79
139
  attr_reader :shape
80
140
 
81
- # The contour line of the frame, a Geom2D::PolygonSet consisting of arbitrary polygons.
82
- attr_reader :contour_line
83
-
84
141
  # The x-coordinate where the next box will be placed.
85
142
  #
86
- # Note: Since the algorithm for #draw takes the margin of a box into account, the actual
143
+ # Note: Since the algorithm for drawing takes the margin of a box into account, the actual
87
144
  # x-coordinate (and y-coordinate, available width and available height) might be different.
88
145
  attr_reader :x
89
146
 
@@ -103,18 +160,12 @@ module HexaPDF
103
160
  attr_reader :available_height
104
161
 
105
162
  # Creates a new Frame object for the given rectangular area.
106
- #
107
- # If the contour line of the frame is not specified, then the rectangular area is used as
108
- # contour line.
109
163
  def initialize(left, bottom, width, height, contour_line: nil)
110
164
  @left = left
111
165
  @bottom = bottom
112
166
  @width = width
113
167
  @height = height
114
- @contour_line = contour_line || Geom2D::PolygonSet.new(
115
- [create_rectangle(left, bottom, left + width, bottom + height)]
116
- )
117
-
168
+ @contour_line = contour_line
118
169
  @shape = Geom2D::PolygonSet.new(
119
170
  [create_rectangle(left, bottom, left + width, bottom + height)]
120
171
  )
@@ -123,36 +174,69 @@ module HexaPDF
123
174
  @available_width = width
124
175
  @available_height = height
125
176
  @region_selection = :max_height
177
+ @fit_data = FitData.new
126
178
  end
127
179
 
128
- # Draws the given box onto the canvas at the frame's current position. Returns +true+ if
129
- # drawing was possible, +false+ otherwise.
130
- #
131
- # When positioning the box, the style properties "position", "position_hint" and "margin" are
132
- # taken into account. Note that the margin is ignored if a box's side coincides with the
133
- # frame's original boundary.
134
- #
135
- # After a box is successfully drawn, the frame's shape and contour line are adjusted to remove
136
- # the occupied area.
137
- def draw(canvas, box)
180
+ # Fits the given box into the current region of available space.
181
+ def fit(box)
138
182
  aw = available_width
139
183
  ah = available_height
140
- used_margin_left = used_margin_right = used_margin_top = 0
184
+ @fit_data.reset(box, aw, ah)
141
185
 
142
- if box.style.position != :absolute
186
+ if full?
187
+ false
188
+ elsif box.style.position == :absolute
189
+ x, y = box.style.position_hint
190
+ box.fit(width - x, height - y, self)
191
+ true
192
+ else
143
193
  if box.style.margin?
144
194
  margin = box.style.margin
145
195
  ah -= margin.bottom unless float_equal(@y - ah, @bottom)
146
- ah -= used_margin_top = margin.top unless float_equal(@y, @bottom + @height)
147
- aw -= used_margin_right = margin.right unless float_equal(@x + aw, @left + @width)
148
- aw -= used_margin_left = margin.left unless float_equal(@x, @left)
196
+ ah -= @fit_data.margin_top = margin.top unless float_equal(@y, @bottom + @height)
197
+ aw -= @fit_data.margin_right = margin.right unless float_equal(@x + aw, @left + @width)
198
+ aw -= @fit_data.margin_left = margin.left unless float_equal(@x, @left)
199
+ @fit_data.available_width = aw
200
+ @fit_data.available_height = ah
149
201
  end
150
202
 
151
- return false unless box.fit(aw, ah, self)
203
+ box.fit(aw, ah, self)
204
+ end
205
+ end
206
+
207
+ # Tries to split the (fitted) box into two parts, where the first part needs to fit into the
208
+ # available space, and returns both parts.
209
+ #
210
+ # If the given box is not the last fitted box, #fit is called before splitting the box.
211
+ #
212
+ # See Box#split for further details.
213
+ def split(box)
214
+ fit(box) unless box == @fit_data.box
215
+ boxes = box.split(@fit_data.available_width, @fit_data.available_height, self)
216
+ @fit_data.reset unless boxes[0] == @fit_data.box
217
+ boxes
218
+ end
219
+
220
+ # Draws the given (fitted) box onto the canvas at the frame's current position. Returns +true+
221
+ # if drawing was possible, +false+ otherwise.
222
+ #
223
+ # If the given box is not the last fitted box, #fit is called before drawing the box.
224
+ #
225
+ # After a box is successfully drawn, the frame's shape and contour line are adjusted to remove
226
+ # the occupied area.
227
+ def draw(canvas, box)
228
+ unless box == @fit_data.box
229
+ fit(box) || return
152
230
  end
153
231
 
154
232
  width = box.width
155
233
  height = box.height
234
+ margin = box.style.margin if box.style.margin?
235
+
236
+ if height == 0
237
+ @fit_data.reset
238
+ return true
239
+ end
156
240
 
157
241
  case box.style.position
158
242
  when :absolute
@@ -160,18 +244,17 @@ module HexaPDF
160
244
  x += left
161
245
  y += bottom
162
246
  rectangle = if box.style.margin?
163
- margin = box.style.margin
164
247
  create_rectangle(x - margin.left, y - margin.bottom,
165
248
  x + width + margin.right, y + height + margin.top)
166
249
  else
167
250
  create_rectangle(x, y, x + width, y + height)
168
251
  end
169
252
  when :float
170
- x = @x + used_margin_left
171
- x += aw - width if box.style.position_hint == :right
172
- y = @y - height - used_margin_top
173
- # We can use the real margins from the box because they either have the desired effect or
174
- # just extend the rectangle outside the frame.
253
+ x = @x + @fit_data.margin_left
254
+ x += @fit_data.available_width - width if box.style.position_hint == :right
255
+ y = @y - height - @fit_data.margin_top
256
+ # We use the real margins from the box because they either have the desired effect or just
257
+ # extend the rectangle outside the frame.
175
258
  rectangle = create_rectangle(x - (margin&.left || 0), y - (margin&.bottom || 0),
176
259
  x + width + (margin&.right || 0), @y)
177
260
  when :flow
@@ -181,24 +264,25 @@ module HexaPDF
181
264
  else
182
265
  x = case box.style.position_hint
183
266
  when :right
184
- @x + used_margin_left + aw - width
267
+ @x + @fit_data.margin_left + @fit_data.available_width - width
185
268
  when :center
186
- max_margin = [used_margin_left, used_margin_right].max
269
+ max_margin = [@fit_data.margin_left, @fit_data.margin_right].max
187
270
  # If we have enough space left for equal margins, we center perfectly
188
271
  if available_width - width >= 2 * max_margin
189
272
  @x + (available_width - width) / 2.0
190
273
  else
191
- @x + used_margin_left + (aw - width) / 2.0
274
+ @x + @fit_data.margin_left + (@fit_data.available_width - width) / 2.0
192
275
  end
193
276
  else
194
- @x + used_margin_left
277
+ @x + @fit_data.margin_left
195
278
  end
196
- y = @y - height - used_margin_top
279
+ y = @y - height - @fit_data.margin_top
197
280
  rectangle = create_rectangle(left, y - (margin&.bottom || 0), left + self.width, @y)
198
281
  end
199
282
 
200
283
  box.draw(canvas, x, y)
201
284
  remove_area(rectangle)
285
+ @fit_data.reset
202
286
 
203
287
  true
204
288
  end
@@ -229,6 +313,7 @@ module HexaPDF
229
313
  trim_shape
230
314
  end
231
315
 
316
+ @fit_data.reset
232
317
  available_width != 0
233
318
  end
234
319
 
@@ -236,12 +321,24 @@ module HexaPDF
236
321
  # line.
237
322
  def remove_area(polygon)
238
323
  @shape = Geom2D::Algorithms::PolygonOperation.run(@shape, polygon, :difference)
239
- @contour_line = Geom2D::Algorithms::PolygonOperation.run(@contour_line, polygon,
240
- :difference)
324
+ if @contour_line
325
+ @contour_line = Geom2D::Algorithms::PolygonOperation.run(@contour_line, polygon,
326
+ :difference)
327
+ end
241
328
  @region_selection = :max_width
242
329
  find_next_region
243
330
  end
244
331
 
332
+ # Returns +true+ if the frame has no more space left.
333
+ def full?
334
+ available_width == 0
335
+ end
336
+
337
+ # The contour line of the frame, a Geom2D::PolygonSet consisting of arbitrary polygons.
338
+ def contour_line
339
+ @contour_line || @shape
340
+ end
341
+
245
342
  # Returns a width specification for the frame's contour line that can be used, for example,
246
343
  # with TextLayouter.
247
344
  #
@@ -255,7 +352,7 @@ module HexaPDF
255
352
  # Depending on the complexity of the frame, the result may be any of the allowed width
256
353
  # specifications of TextLayouter#fit.
257
354
  def width_specification(offset = 0)
258
- WidthFromPolygon.new(@contour_line, offset)
355
+ WidthFromPolygon.new(contour_line, offset)
259
356
  end
260
357
 
261
358
  private