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
@@ -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