hexapdf 0.43.0 → 0.44.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/examples/030-pdfa.rb +1 -0
  4. data/lib/hexapdf/composer.rb +1 -0
  5. data/lib/hexapdf/document/files.rb +7 -2
  6. data/lib/hexapdf/document/metadata.rb +12 -1
  7. data/lib/hexapdf/layout/box.rb +160 -61
  8. data/lib/hexapdf/layout/box_fitter.rb +1 -0
  9. data/lib/hexapdf/layout/column_box.rb +22 -24
  10. data/lib/hexapdf/layout/container_box.rb +2 -2
  11. data/lib/hexapdf/layout/frame.rb +13 -95
  12. data/lib/hexapdf/layout/image_box.rb +4 -4
  13. data/lib/hexapdf/layout/list_box.rb +11 -19
  14. data/lib/hexapdf/layout/style.rb +5 -1
  15. data/lib/hexapdf/layout/table_box.rb +48 -55
  16. data/lib/hexapdf/layout/text_box.rb +27 -42
  17. data/lib/hexapdf/parser.rb +5 -2
  18. data/lib/hexapdf/type/file_specification.rb +9 -5
  19. data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
  20. data/lib/hexapdf/version.rb +1 -1
  21. data/test/hexapdf/document/test_files.rb +5 -0
  22. data/test/hexapdf/document/test_metadata.rb +21 -0
  23. data/test/hexapdf/layout/test_box.rb +82 -37
  24. data/test/hexapdf/layout/test_box_fitter.rb +7 -0
  25. data/test/hexapdf/layout/test_column_box.rb +7 -13
  26. data/test/hexapdf/layout/test_container_box.rb +1 -1
  27. data/test/hexapdf/layout/test_frame.rb +0 -48
  28. data/test/hexapdf/layout/test_image_box.rb +14 -6
  29. data/test/hexapdf/layout/test_list_box.rb +25 -26
  30. data/test/hexapdf/layout/test_table_box.rb +39 -53
  31. data/test/hexapdf/layout/test_text_box.rb +38 -66
  32. data/test/hexapdf/test_composer.rb +6 -0
  33. data/test/hexapdf/test_parser.rb +8 -0
  34. data/test/hexapdf/type/test_file_specification.rb +2 -1
  35. metadata +2 -2
@@ -45,7 +45,7 @@ module HexaPDF
45
45
  #
46
46
  # == Usage
47
47
  #
48
- # After a Frame object is initialized, it is ready for drawing boxes on it.
48
+ # After a Frame object is initialized, it is ready for fitting boxes in it and drawing them.
49
49
  #
50
50
  # The explicit way of drawing a box follows these steps:
51
51
  #
@@ -54,7 +54,7 @@ module HexaPDF
54
54
  #
55
55
  # The method #fit is also called for absolutely positioned boxes but since these boxes are not
56
56
  # subject to the normal constraints, the provided available width and height are the width and
57
- # height inside the frame to the right and top of the bottom-left corner of the box.
57
+ # height inside the frame to the right and top of the bottom left corner of the box.
58
58
  #
59
59
  # * If the box didn't fit, call #find_next_region to determine the next region for placing the
60
60
  # box. If a new region was found, start over with #fit. Otherwise the frame has no more space
@@ -65,10 +65,6 @@ module HexaPDF
65
65
  # splitting is successful, the first box can be drawn (Make sure that the second box is
66
66
  # handled correctly). Otherwise, start over with #find_next_region.
67
67
  #
68
- # For applications where splitting is not necessary, an easier way is to just use #draw and
69
- # #find_next_region together, as #draw calls #fit if the box was not fit into the current
70
- # region.
71
- #
72
68
  # == Used Box Properties
73
69
  #
74
70
  # The style properties 'position', 'align', 'valign', 'margin' and 'mask_mode' are taken into
@@ -88,84 +84,10 @@ module HexaPDF
88
84
 
89
85
  include HexaPDF::Utils
90
86
 
91
- # Stores the result of fitting a box in a Frame.
92
- class FitResult
93
-
94
- # The frame into which the box was fitted.
95
- attr_accessor :frame
96
-
97
- # The box that was fitted into the frame.
98
- attr_accessor :box
99
-
100
- # The horizontal position where the box will be drawn.
101
- attr_accessor :x
102
-
103
- # The vertical position where the box will be drawn.
104
- attr_accessor :y
105
-
106
- # The available width in the frame for this particular box.
107
- attr_accessor :available_width
108
-
109
- # The available height in the frame for this particular box.
110
- attr_accessor :available_height
111
-
112
- # The rectangle (a Geom2D::Rectangle object) that will be removed from the frame when
113
- # drawing the box.
114
- attr_accessor :mask
115
-
116
- # Initialize the result object for the given frame and box.
117
- def initialize(frame, box)
118
- @frame = frame
119
- @box = box
120
- @available_width = 0
121
- @available_height = 0
122
- @success = false
123
- end
124
-
125
- # Marks the fitting status as success.
126
- def success!
127
- @success = true
128
- end
129
-
130
- # Returns +true+ if fitting was successful.
131
- def success?
132
- @success
133
- end
134
-
135
- # Draws the #box onto the canvas at (#x + *dx*, #y + *dy*).
136
- #
137
- # The relative offset (dx, dy) is useful when rendering results that were accumulated and
138
- # then need to be moved because the container holding them changes its position.
139
- #
140
- # The configuration option "debug" can be used to add visual debug output with respect to
141
- # box placement.
142
- def draw(canvas, dx: 0, dy: 0)
143
- doc = canvas.context.document
144
- if doc.config['debug']
145
- name = (frame.parent_boxes + [box]).map do |box|
146
- box.class.to_s.sub(/.*::/, '')
147
- end.join('-') << "##{box.object_id}"
148
- name = "#{name} (#{(x + dx).to_i},#{(y + dy).to_i}-#{mask.width.to_i}x#{mask.height.to_i})"
149
- ocg = doc.optional_content.ocg(name)
150
- canvas.optional_content(ocg) do
151
- canvas.translate(dx, dy) do
152
- canvas.fill_color("green").stroke_color("darkgreen").
153
- opacity(fill_alpha: 0.1, stroke_alpha: 0.2).
154
- draw(:geom2d, object: mask, path_only: true).fill_stroke
155
- end
156
- end
157
- page = "Page #{canvas.context.index + 1}" rescue "XObject"
158
- doc.optional_content.default_configuration.add_ocg_to_ui(ocg, path: ['Debug', page])
159
- end
160
- box.draw(canvas, x + dx, y + dy)
161
- end
162
-
163
- end
164
-
165
- # The x-coordinate of the bottom-left corner.
87
+ # The x-coordinate of the bottom left corner.
166
88
  attr_reader :left
167
89
 
168
- # The y-coordinate of the bottom-left corner.
90
+ # The y-coordinate of the bottom left corner.
169
91
  attr_reader :bottom
170
92
 
171
93
  # The width of the frame.
@@ -253,16 +175,15 @@ module HexaPDF
253
175
  @context&.document
254
176
  end
255
177
 
256
- # Fits the given box into the current region of available space and returns a FitResult
257
- # object.
178
+ # Fits the given box into the current region of available space and returns the associated
179
+ # Box::FitResult object.
258
180
  #
259
181
  # Fitting a box takes the style properties 'position', 'align', 'valign', 'margin', and
260
182
  # 'mask_mode' into account.
261
183
  #
262
- # Use the FitResult#success? method to determine whether fitting was successful.
184
+ # Use the Box::FitResult#success? method to determine whether fitting was successful.
263
185
  def fit(box)
264
- fit_result = FitResult.new(self, box)
265
- return fit_result if full?
186
+ return Box::FitResult.new(box, frame: self) if full?
266
187
 
267
188
  margin = box.style.margin if box.style.margin?
268
189
 
@@ -277,7 +198,7 @@ module HexaPDF
277
198
 
278
199
  aw = width - x
279
200
  ah = height - y
280
- box.fit(aw, ah, self)
201
+ fit_result = box.fit(aw, ah, self)
281
202
  fit_result.success!
282
203
 
283
204
  x += left
@@ -294,7 +215,7 @@ module HexaPDF
294
215
  ah -= margin_top = margin.top unless float_equal(@y, @bottom + @height)
295
216
  end
296
217
 
297
- fit_result.success! if box.fit(aw, ah, self)
218
+ fit_result = box.fit(aw, ah, self)
298
219
 
299
220
  width = box.width
300
221
  height = box.height
@@ -372,27 +293,24 @@ module HexaPDF
372
293
  create_rectangle(@x, @y - available_height, @x + available_width, @y)
373
294
  end
374
295
 
375
- fit_result.available_width = aw
376
- fit_result.available_height = ah
377
296
  fit_result.x = x
378
297
  fit_result.y = y
379
298
  fit_result.mask = rectangle
380
299
  fit_result
381
300
  end
382
301
 
383
- # Tries to split the box of the given FitResult into two parts and returns both parts.
302
+ # Tries to split the box of the given Box::FitResult into two parts and returns both parts.
384
303
  #
385
304
  # See Box#split for further details.
386
305
  def split(fit_result)
387
- fit_result.box.split(fit_result.available_width, fit_result.available_height, self)
306
+ fit_result.box.split
388
307
  end
389
308
 
390
- # Draws the box of the given FitResult onto the canvas at the fitted position.
309
+ # Draws the box of the given Box::FitResult onto the canvas at the fitted position.
391
310
  #
392
311
  # After a box is successfully drawn, the frame's shape is adjusted to remove the occupied
393
312
  # area.
394
313
  def draw(canvas, fit_result)
395
- return if fit_result.box.height == 0 || fit_result.box.width == 0
396
314
  fit_result.draw(canvas)
397
315
  remove_area(fit_result.mask)
398
316
  end
@@ -79,9 +79,11 @@ module HexaPDF
79
79
  false
80
80
  end
81
81
 
82
+ private
83
+
82
84
  # Fits the image into the current region of the frame, taking the initially set width and
83
85
  # height into account (see the class description for details).
84
- def fit(available_width, available_height, _frame)
86
+ def fit_content(available_width, available_height, _frame)
85
87
  image_width = @image.width.to_f
86
88
  image_height = @image.height.to_f
87
89
  image_ratio = image_width / image_height
@@ -103,12 +105,10 @@ module HexaPDF
103
105
  @height = image_height * ratio + rh
104
106
  end
105
107
 
106
- @fit_successful = float_compare(@width, available_width) <= 0 &&
108
+ fit_result.success! if float_compare(@width, available_width) <= 0 &&
107
109
  float_compare(@height, available_height) <= 0
108
110
  end
109
111
 
110
- private
111
-
112
112
  # Draws the image onto the canvas at position [x, y].
113
113
  def draw_content(canvas, x, y)
114
114
  canvas.image(@image, at: [x, y], width: content_width, height: content_height)
@@ -186,8 +186,10 @@ module HexaPDF
186
186
  super && (!@results || @results.all? {|result| result.box_fitter.fit_results.empty? })
187
187
  end
188
188
 
189
+ private
190
+
189
191
  # Fits the list box into the current region of the frame.
190
- def fit(available_width, available_height, frame)
192
+ def fit_content(available_width, available_height, frame)
191
193
  @width = if @initial_width > 0
192
194
  @initial_width
193
195
  else
@@ -253,15 +255,13 @@ module HexaPDF
253
255
 
254
256
  @height = @results.sum(&:height) + (@results.count - 1) * item_spacing + reserved_height
255
257
 
256
- @draw_pos_x = frame.x + reserved_width_left
257
- @draw_pos_y = frame.y - @height + reserved_height_bottom
258
- @all_items_fitted = @results.all? {|r| r.box_fitter.success? } &&
259
- @results.size == @children.size
260
- @fit_successful = @all_items_fitted || (@initial_height > 0 && style.overflow == :truncate)
258
+ if @results.size == @children.size && @results.all? {|r| r.box_fitter.success? }
259
+ fit_result.success!
260
+ elsif !@results.empty? && !@results[0].box_fitter.fit_results.empty?
261
+ fit_result.overflow!
262
+ end
261
263
  end
262
264
 
263
- private
264
-
265
265
  # Removes the +content_indentation+ from the left side of the given shape (a Geom2D::PolygonSet).
266
266
  def remove_indent_from_frame_shape(shape)
267
267
  polygon_index = 0
@@ -307,7 +307,7 @@ module HexaPDF
307
307
  end
308
308
 
309
309
  # Splits the content of the list box. This method is called from Box#split.
310
- def split_content(_available_width, _available_height, _frame)
310
+ def split_content
311
311
  remaining_boxes = @results[-1].box_fitter.remaining_boxes
312
312
  first_is_split_box = !remaining_boxes.empty?
313
313
  children = (remaining_boxes.empty? ? [] : [remaining_boxes]) + @children[@results.size..-1]
@@ -361,17 +361,9 @@ module HexaPDF
361
361
 
362
362
  # Draws the list items onto the canvas at position [x, y].
363
363
  def draw_content(canvas, x, y)
364
- if !@all_items_fitted && (@initial_height > 0 && style.overflow == :error)
365
- raise HexaPDF::Error, "Some items don't fit into box with limited height and " \
366
- "style property overflow is set to :error"
367
- end
364
+ translate = style.position != :flow && (x != @fit_x || y != @fit_y)
368
365
 
369
- translate = style.position != :flow && (x != @draw_pos_x || y != @draw_pos_y)
370
-
371
- if translate
372
- canvas.save_graphics_state
373
- canvas.translate(x - @draw_pos_x, y - @draw_pos_y)
374
- end
366
+ canvas.save_graphics_state.translate(x - @fit_x, y - @fit_y) if translate
375
367
 
376
368
  @results.each do |item_result|
377
369
  box_fitter = item_result.box_fitter
@@ -1254,7 +1254,11 @@ module HexaPDF
1254
1254
  # doesn't. If a box doesn't support this value, it is positioned as if the value :default
1255
1255
  # was set.
1256
1256
  #
1257
- # Note that the properties #align and #valign are not used with this value!
1257
+ # Notes:
1258
+ #
1259
+ # * The properties #align and #valign are not used with this value.
1260
+ # * The rectangular area of the box is the rectangle containing all the flowed content.
1261
+ # That rectangle is used for drawing the border, background and so on.
1258
1262
  #
1259
1263
  # Examples:
1260
1264
  #
@@ -211,21 +211,27 @@ module HexaPDF
211
211
  @height = height
212
212
  end
213
213
 
214
+ # :nodoc:
215
+ def inspect
216
+ "<Cell (#{row},#{column}) #{row_span}x#{col_span} #{Array(children).map(&:class)}>"
217
+ end
218
+
219
+ private
220
+
214
221
  # Fits the children of the table cell into the given rectangular area.
215
- def fit(available_width, available_height, frame)
216
- @width = available_width
222
+ def fit_content(available_width, available_height, frame)
217
223
  width = available_width - reserved_width
218
- height = available_height - reserved_height
219
- return false if width <= 0 || height <= 0
224
+ height = @used_height = available_height - reserved_height
225
+ return if width <= 0 || height <= 0
220
226
 
221
227
  frame = frame.child_frame(0, 0, width, height, box: self)
222
228
  case children
223
229
  when Box
224
- fit_result = frame.fit(children)
225
- @preferred_width = fit_result.x + fit_result.box.width + reserved_width
226
- @height = @preferred_height = fit_result.box.height + reserved_height
227
- @fit_results = [fit_result]
228
- @fit_successful = fit_result.success?
230
+ child_result = frame.fit(children)
231
+ @preferred_width = child_result.x + child_result.box.width + reserved_width
232
+ @height = @preferred_height = child_result.box.height + reserved_height
233
+ @fit_results = [child_result]
234
+ fit_result.success! if child_result.success?
229
235
  when Array
230
236
  box_fitter = BoxFitter.new([frame])
231
237
  children.each {|box| box_fitter.fit(box) }
@@ -233,34 +239,23 @@ module HexaPDF
233
239
  @preferred_width = max_x_result.x + max_x_result.box.width + reserved_width
234
240
  @height = @preferred_height = box_fitter.content_heights[0] + reserved_height
235
241
  @fit_results = box_fitter.fit_results
236
- @fit_successful = box_fitter.success?
242
+ fit_result.success! if box_fitter.success?
237
243
  else
238
244
  @preferred_width = reserved_width
239
245
  @height = @preferred_height = reserved_height
240
246
  @fit_results = []
241
- @fit_successful = true
247
+ fit_result.success!
242
248
  end
243
249
  end
244
250
 
245
- # :nodoc:
246
- def inspect
247
- "<Cell (#{row},#{column}) #{row_span}x#{col_span} #{Array(children).map(&:class)}>"
248
- end
249
-
250
- private
251
-
252
251
  # Draws the content of the cell.
253
252
  def draw_content(canvas, x, y)
254
253
  return if @fit_results.empty?
255
254
 
256
255
  # available_width is always equal to content_width but we need to adjust for the
257
256
  # difference in the y direction between fitting and drawing
258
- y -= (@fit_results[0].available_height - content_height)
259
- @fit_results.each do |fit_result|
260
- #fit_result.x += x
261
- #fit_result.y += y
262
- fit_result.draw(canvas, dx: x, dy: y)
263
- end
257
+ y -= (@used_height - content_height)
258
+ @fit_results.each {|fit_result| fit_result.draw(canvas, dx: x, dy: y) }
264
259
  end
265
260
 
266
261
  end
@@ -393,7 +388,7 @@ module HexaPDF
393
388
  else
394
389
  column_info[cell.column].last
395
390
  end
396
- unless cell.fit(available_cell_width, available_height, frame)
391
+ unless cell.fit(available_cell_width, available_height, frame).success?
397
392
  row_fit = false
398
393
  break
399
394
  end
@@ -589,24 +584,23 @@ module HexaPDF
589
584
  super && (!@last_fitted_row_index || @last_fitted_row_index < 0)
590
585
  end
591
586
 
592
- # Fits the table into the current region of the frame.
593
- def fit(available_width, available_height, frame)
594
- return false if (@initial_width > 0 && @initial_width > available_width) ||
595
- (@initial_height > 0 && @initial_height > available_height)
587
+ private
596
588
 
589
+ # Fits the table into the current region of the frame.
590
+ def fit_content(_available_width, _available_height, frame)
597
591
  # Adjust reserved width/height to include space used by the edge cells for their border
598
592
  # since cell borders are drawn on the bounds and not inside.
599
- # This uses the top-left and bottom-right cells and so might not be correct in all cases.
593
+ # This uses the top left and bottom right cells and so might not be correct in all cases.
600
594
  @cell_tl_border_width = @cells[0, 0].style.border.width
601
595
  cell_br_border_width = @cells[-1, -1].style.border.width
602
- rw = reserved_width + (@cell_tl_border_width.left + cell_br_border_width.right) / 2.0
603
- rh = reserved_height + (@cell_tl_border_width.top + cell_br_border_width.bottom) / 2.0
596
+ rw = (@cell_tl_border_width.left + cell_br_border_width.right) / 2.0
597
+ rh = (@cell_tl_border_width.top + cell_br_border_width.bottom) / 2.0
604
598
 
605
- width = (@initial_width > 0 ? @initial_width : available_width) - rw
606
- height = (@initial_height > 0 ? @initial_height : available_height) - rh
599
+ width = @width - reserved_width - rw
600
+ height = @height - reserved_height - rh
607
601
  used_height = 0
608
602
  columns = calculate_column_widths(width)
609
- return false if columns.empty?
603
+ return if columns.empty?
610
604
 
611
605
  frame = frame.child_frame(box: self)
612
606
  @special_cells_fit_not_successful = false
@@ -616,18 +610,21 @@ module HexaPDF
616
610
  height -= special_used_height
617
611
  used_height += special_used_height
618
612
  @special_cells_fit_not_successful = (last_fitted_row_index != special_cells.number_of_rows - 1)
619
- return false if @special_cells_fit_not_successful
613
+ return nil if @special_cells_fit_not_successful
620
614
  end
621
615
 
622
616
  main_used_height, @last_fitted_row_index = @cells.fit_rows(@start_row_index, height, columns, frame)
623
617
  used_height += main_used_height
624
618
 
625
- @width = (@initial_width > 0 ? @initial_width : columns[-1].sum + rw)
626
- @height = (@initial_height > 0 ? @initial_height : used_height + rh)
627
- @fit_successful = (@last_fitted_row_index == @cells.number_of_rows - 1)
628
- end
619
+ update_content_width { columns[-1].sum + rw }
620
+ update_content_height { used_height + rh }
629
621
 
630
- private
622
+ if @last_fitted_row_index == @cells.number_of_rows - 1
623
+ fit_result.success!
624
+ elsif @last_fitted_row_index >= 0
625
+ fit_result.overflow!
626
+ end
627
+ end
631
628
 
632
629
  # Calculates and returns the x-coordinates and widths of all columns based on the given total
633
630
  # available width.
@@ -649,20 +646,16 @@ module HexaPDF
649
646
  end
650
647
 
651
648
  # Splits the content of the table box. This method is called from Box#split.
652
- def split_content(_available_width, _available_height, _frame)
653
- if @special_cells_fit_not_successful || @last_fitted_row_index < 0
654
- [nil, self]
655
- else
656
- box = create_split_box
657
- box.instance_variable_set(:@start_row_index, @last_fitted_row_index + 1)
658
- box.instance_variable_set(:@last_fitted_row_index, -1)
659
- box.instance_variable_set(:@special_cells_fit_not_successful, nil)
660
- header_cells = @header ? Cells.new(@header.call(self), cell_style: @cell_style) : nil
661
- box.instance_variable_set(:@header_cells, header_cells)
662
- footer_cells = @footer ? Cells.new(@footer.call(self), cell_style: @cell_style) : nil
663
- box.instance_variable_set(:@footer_cells, footer_cells)
664
- [self, box]
665
- end
649
+ def split_content
650
+ box = create_split_box
651
+ box.instance_variable_set(:@start_row_index, @last_fitted_row_index + 1)
652
+ box.instance_variable_set(:@last_fitted_row_index, -1)
653
+ box.instance_variable_set(:@special_cells_fit_not_successful, nil)
654
+ header_cells = @header ? Cells.new(@header.call(self), cell_style: @cell_style) : nil
655
+ box.instance_variable_set(:@header_cells, header_cells)
656
+ footer_cells = @footer ? Cells.new(@footer.call(self), cell_style: @cell_style) : nil
657
+ box.instance_variable_set(:@footer_cells, footer_cells)
658
+ [self, box]
666
659
  end
667
660
 
668
661
  # Draws the child boxes onto the canvas at position [x, y].
@@ -43,6 +43,11 @@ module HexaPDF
43
43
  # objects of a Frame.
44
44
  #
45
45
  # This class uses TextLayouter behind the scenes to do the hard work.
46
+ #
47
+ # == Used Box Properties
48
+ #
49
+ # The spacing after the last line can be controlled via the style property +last_line_gap+. Also
50
+ # see TextLayouter#style for other style properties taken into account.
46
51
  class TextBox < Box
47
52
 
48
53
  # Creates a new TextBox object with the given inline items (e.g. TextFragment and InlineBox
@@ -67,21 +72,27 @@ module HexaPDF
67
72
  true
68
73
  end
69
74
 
75
+ # :nodoc:
76
+ def draw(canvas, x, y)
77
+ super(canvas, x + @x_offset, y)
78
+ end
79
+
80
+ # :nodoc:
81
+ def empty?
82
+ super && (!@result || @result.lines.empty?)
83
+ end
84
+
85
+ private
86
+
70
87
  # Fits the text box into the Frame.
71
88
  #
72
89
  # Depending on the 'position' style property, the text is either fit into the current region
73
90
  # of the frame using +available_width+ and +available_height+, or fit to the shape of the
74
91
  # frame starting from the top (when 'position' is set to :flow).
75
- #
76
- # The spacing after the last line can be controlled via the style property +last_line_gap+.
77
- #
78
- # Also see TextLayouter#style for other style properties taken into account.
79
- def fit(available_width, available_height, frame)
80
- return false if (@initial_width > 0 && @initial_width > available_width) ||
81
- (@initial_height > 0 && @initial_height > available_height)
82
-
92
+ def fit_content(available_width, available_height, frame)
83
93
  frame = frame.child_frame(box: self)
84
94
  @width = @x_offset = @height = 0
95
+
85
96
  @result = if style.position == :flow
86
97
  @tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
87
98
  apply_first_text_indent: !split_box?, frame: frame)
@@ -92,6 +103,7 @@ module HexaPDF
92
103
  height = (@initial_height > 0 ? @initial_height : available_height) - @height
93
104
  @tl.fit(@items, width, height, apply_first_text_indent: !split_box?, frame: frame)
94
105
  end
106
+
95
107
  @width += if @initial_width > 0 || style.text_align == :center || style.text_align == :right
96
108
  width
97
109
  elsif style.position == :flow
@@ -114,47 +126,20 @@ module HexaPDF
114
126
  @height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
115
127
  end
116
128
 
117
- @result.status == :success ||
118
- (@result.status == :height && @initial_height > 0 && style.overflow == :truncate)
119
- end
120
-
121
- # Splits the text box into two boxes if necessary and possible.
122
- def split(available_width, available_height, frame)
123
- fit(available_width, available_height, frame) unless @result
124
-
125
- if style.position != :flow && (float_compare(@width, available_width) > 0 ||
126
- float_compare(@height, available_height) > 0)
127
- [nil, self]
128
- elsif @result.remaining_items.empty?
129
- [self]
130
- elsif @result.lines.empty?
131
- [nil, self]
132
- else
133
- [self, create_box_for_remaining_items]
129
+ if @result.status == :success
130
+ fit_result.success!
131
+ elsif @result.status == :height && !@result.lines.empty?
132
+ fit_result.overflow!
134
133
  end
135
134
  end
136
135
 
137
- # :nodoc:
138
- def draw(canvas, x, y)
139
- super(canvas, x + @x_offset, y)
136
+ # Splits the text box into two.
137
+ def split_content
138
+ [self, create_box_for_remaining_items]
140
139
  end
141
140
 
142
- # :nodoc:
143
- def empty?
144
- super && (!@result || @result.lines.empty?)
145
- end
146
-
147
- private
148
-
149
141
  # Draws the text into the box.
150
142
  def draw_content(canvas, x, y)
151
- return unless @result
152
-
153
- if @result.status == :height && @initial_height > 0 && style.overflow == :error
154
- raise HexaPDF::Error, "Text doesn't fit into box with limited height and " \
155
- "style property overflow is set to :error"
156
- end
157
-
158
143
  return if @result.lines.empty?
159
144
  @result.draw(canvas, x - @x_offset, y + content_height)
160
145
  end
@@ -365,11 +365,14 @@ module HexaPDF
365
365
  # Need to iterate through the whole lines array in case there are multiple %%EOF to try
366
366
  eof_index = 0
367
367
  while (eof_index = lines[0..(eof_index - 1)].rindex {|l| l.strip == '%%EOF' })
368
- if lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
368
+ if eof_index > 0 && lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
369
369
  startxref_offset = $1.to_i
370
370
  startxref_mangled = true
371
371
  break # we found it even if it the syntax is not entirely correct
372
- elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
372
+ elsif eof_index < 2
373
+ startxref_missing = true
374
+ break
375
+ elsif lines[eof_index - 2].strip != "startxref"
373
376
  startxref_missing = true
374
377
  else
375
378
  startxref_offset = lines[eof_index - 1].to_i
@@ -158,11 +158,11 @@ module HexaPDF
158
158
  end
159
159
 
160
160
  # :call-seq:
161
- # file_spec.embed(filename, name: File.basename(filename), register: true) -> ef_stream
162
- # file_spec.embed(io, name:, register: true) -> ef_stream
161
+ # file_spec.embed(filename, name: File.basename(filename), mime_type: nil, register: true) -> ef_stream
162
+ # file_spec.embed(io, name:, mime_type: nil, register: true) -> ef_stream
163
163
  #
164
- # Embeds the given file or IO stream into the PDF file, sets the path accordingly and returns
165
- # the created stream object.
164
+ # Embeds the given file or IO stream into the PDF file, sets the path and MIME type
165
+ # accordingly and returns the created stream object.
166
166
  #
167
167
  # If a file is given, the +name+ option defaults to the basename of the file. However, if an
168
168
  # IO object is given, the +name+ argument is mandatory.
@@ -177,13 +177,16 @@ module HexaPDF
177
177
  # name::
178
178
  # The name that should be used as path value and when registering.
179
179
  #
180
+ # mime_type::
181
+ # Optionally specifies the MIME type of the file.
182
+ #
180
183
  # register::
181
184
  # Specifies whether the embedded file will be added to the EmbeddedFiles name tree under
182
185
  # the +name+. If the name is already taken, it's value is overwritten.
183
186
  #
184
187
  # The file has to be available until the PDF document gets written because reading and
185
188
  # writing is done lazily.
186
- def embed(file_or_io, name: nil, register: true)
189
+ def embed(file_or_io, name: nil, mime_type: nil, register: true)
187
190
  name ||= File.basename(file_or_io) if file_or_io.kind_of?(String)
188
191
  if name.nil?
189
192
  raise ArgumentError, "The name argument is mandatory when given an IO object"
@@ -194,6 +197,7 @@ module HexaPDF
194
197
 
195
198
  self[:EF] ||= {}
196
199
  ef_stream = self[:EF][:UF] = self[:EF][:F] = document.add({Type: :EmbeddedFile})
200
+ ef_stream[:Subtype] = mime_type.to_sym if mime_type
197
201
  stat = if file_or_io.kind_of?(String)
198
202
  File.stat(file_or_io)
199
203
  elsif file_or_io.respond_to?(:stat)
@@ -51,7 +51,7 @@ module HexaPDF
51
51
 
52
52
  define_type :ExtGState
53
53
 
54
- define_field :Type, type: Symbol, required: true, default: type
54
+ define_field :Type, type: Symbol, default: type
55
55
  define_field :LW, type: Numeric, version: "1.3"
56
56
  define_field :LC, type: Integer, version: "1.3"
57
57
  define_field :LJ, type: Integer, version: "1.3"
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.43.0'
40
+ VERSION = '0.44.0'
41
41
 
42
42
  end
@@ -43,6 +43,11 @@ describe HexaPDF::Document::Files do
43
43
  assert_equal('Some file', spec[:Desc])
44
44
  end
45
45
 
46
+ it "optionally sets the MIME type of an embedded file" do
47
+ spec = @doc.files.add(@file.path, mime_type: 'application/pdf')
48
+ assert_equal(:'application/pdf', spec.embedded_file_stream[:Subtype])
49
+ end
50
+
46
51
  it "requires the name argument when given an IO object" do
47
52
  assert_raises(ArgumentError) { @doc.files.add(StringIO.new) }
48
53
  end