hexapdf 0.43.0 → 0.44.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 (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