hexapdf 0.44.0 → 0.46.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 +106 -47
  3. data/examples/019-acro_form.rb +5 -0
  4. data/examples/027-composer_optional_content.rb +6 -4
  5. data/examples/030-pdfa.rb +12 -11
  6. data/lib/hexapdf/cli/inspect.rb +5 -0
  7. data/lib/hexapdf/composer.rb +23 -1
  8. data/lib/hexapdf/configuration.rb +8 -0
  9. data/lib/hexapdf/content/canvas.rb +3 -3
  10. data/lib/hexapdf/content/canvas_composer.rb +1 -0
  11. data/lib/hexapdf/digital_signature/cms_handler.rb +31 -3
  12. data/lib/hexapdf/digital_signature/signing/default_handler.rb +9 -1
  13. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +5 -1
  14. data/lib/hexapdf/document/layout.rb +63 -30
  15. data/lib/hexapdf/document.rb +24 -2
  16. data/lib/hexapdf/font/type1/character_metrics.rb +1 -1
  17. data/lib/hexapdf/font/type1/font_metrics.rb +1 -1
  18. data/lib/hexapdf/importer.rb +15 -5
  19. data/lib/hexapdf/layout/box.rb +48 -36
  20. data/lib/hexapdf/layout/column_box.rb +3 -11
  21. data/lib/hexapdf/layout/container_box.rb +4 -4
  22. data/lib/hexapdf/layout/frame.rb +7 -6
  23. data/lib/hexapdf/layout/inline_box.rb +17 -23
  24. data/lib/hexapdf/layout/list_box.rb +27 -42
  25. data/lib/hexapdf/layout/page_style.rb +23 -16
  26. data/lib/hexapdf/layout/style.rb +5 -5
  27. data/lib/hexapdf/layout/table_box.rb +14 -10
  28. data/lib/hexapdf/layout/text_box.rb +60 -36
  29. data/lib/hexapdf/layout/text_fragment.rb +1 -1
  30. data/lib/hexapdf/layout/text_layouter.rb +7 -8
  31. data/lib/hexapdf/parser.rb +5 -1
  32. data/lib/hexapdf/rectangle.rb +4 -4
  33. data/lib/hexapdf/revisions.rb +1 -1
  34. data/lib/hexapdf/stream.rb +3 -3
  35. data/lib/hexapdf/tokenizer.rb +3 -2
  36. data/lib/hexapdf/type/acro_form/button_field.rb +2 -0
  37. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  38. data/lib/hexapdf/type/acro_form/field.rb +8 -0
  39. data/lib/hexapdf/type/acro_form/form.rb +2 -1
  40. data/lib/hexapdf/type/acro_form/text_field.rb +2 -0
  41. data/lib/hexapdf/type/form.rb +2 -2
  42. data/lib/hexapdf/version.rb +1 -1
  43. data/test/hexapdf/content/test_canvas_composer.rb +13 -8
  44. data/test/hexapdf/digital_signature/common.rb +66 -84
  45. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +7 -0
  46. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +9 -0
  47. data/test/hexapdf/digital_signature/test_cms_handler.rb +41 -1
  48. data/test/hexapdf/digital_signature/test_handler.rb +2 -1
  49. data/test/hexapdf/document/test_layout.rb +44 -5
  50. data/test/hexapdf/layout/test_box.rb +23 -5
  51. data/test/hexapdf/layout/test_frame.rb +21 -2
  52. data/test/hexapdf/layout/test_inline_box.rb +17 -28
  53. data/test/hexapdf/layout/test_list_box.rb +8 -8
  54. data/test/hexapdf/layout/test_page_style.rb +7 -2
  55. data/test/hexapdf/layout/test_table_box.rb +8 -1
  56. data/test/hexapdf/layout/test_text_box.rb +51 -29
  57. data/test/hexapdf/layout/test_text_layouter.rb +0 -3
  58. data/test/hexapdf/test_composer.rb +14 -5
  59. data/test/hexapdf/test_document.rb +27 -0
  60. data/test/hexapdf/test_importer.rb +17 -0
  61. data/test/hexapdf/test_revisions.rb +54 -41
  62. data/test/hexapdf/test_serializer.rb +1 -0
  63. data/test/hexapdf/type/acro_form/test_form.rb +9 -0
  64. metadata +2 -2
@@ -43,14 +43,12 @@ module HexaPDF
43
43
  # An InlineBox wraps a regular Box so that it can be used as an item for a Line. This enables
44
44
  # inline graphics.
45
45
  #
46
- # Complete box auto-sizing is not possible since the available space cannot be determined
47
- # beforehand! This means the box *must* have at least its width set. The height may either also
48
- # be set or determined during fitting.
46
+ # When an inline box gets placed on a line, the method #fit_wrapped_box is called to fit the
47
+ # wrapped box. This allows the wrapped box to correctly set its width and height which are
48
+ # needed by the TextLayouter algorithm.
49
49
  #
50
- # Fitting of the wrapped box via #fit_wrapped_box needs to be done before accessing any other
51
- # method that uses the wrapped box. For fitting, a frame is used that has the width of the
52
- # wrapped box and its height, or if not set, a practically infinite height. In the latter case
53
- # the height *must* be set during fitting.
50
+ # Note: It is *mandatory* that the wrapped box sets its width and height without relying on the
51
+ # dimensions of the frame's current region.
54
52
  class InlineBox
55
53
 
56
54
  # Creates an InlineBox that wraps a basic Box. All arguments (except +valign+) and the block
@@ -74,7 +72,6 @@ module HexaPDF
74
72
  # The +valign+ argument can be used to specify the vertical alignment of the box relative to
75
73
  # other items in the Line.
76
74
  def initialize(box, valign: :baseline)
77
- raise HexaPDF::Error, "Width of box not set" if box.width == 0
78
75
  @box = box
79
76
  @valign = valign
80
77
  end
@@ -102,8 +99,7 @@ module HexaPDF
102
99
  # Draws the wrapped box. If the box has margins specified, the x and y offsets are correctly
103
100
  # adjusted.
104
101
  def draw(canvas, x, y)
105
- canvas.translate(x - @fit_result.x + box.style.margin.left,
106
- y - @fit_result.y + box.style.margin.bottom) { @fit_result.draw(canvas) }
102
+ @fit_result.draw(canvas, dx: x, dy: y)
107
103
  end
108
104
 
109
105
  # The minimum x-coordinate which is always 0.
@@ -129,19 +125,17 @@ module HexaPDF
129
125
  # Fits the wrapped box.
130
126
  #
131
127
  # If the +frame+ argument is +nil+, a custom frame is created. Otherwise the given +frame+ is
132
- # used for the fitting operation.
133
- def fit_wrapped_box(frame)
134
- frame = if frame
135
- frame.child_frame(0, 0, box.width, box.height == 0 ? 100_000 : box.height)
136
- else
137
- Frame.new(0, 0, box.width, box.height == 0 ? 100_000 : box.height)
138
- end
139
- @fit_result = frame.fit(box)
140
- if !@fit_result.success?
141
- raise HexaPDF::Error, "Box for inline use could not be fit"
142
- elsif box.height > 99_000
143
- raise HexaPDF::Error, "Box for inline use has no valid height set after fitting"
144
- end
128
+ # used for creating an appropriate child frame for the fitting operation.
129
+ #
130
+ # After this operation the caller is responsible for checking the actual width and height of
131
+ # the inline box and whether it really fits.
132
+ def fit_wrapped_box(frame = nil)
133
+ @fit_result = box.fit(100_000, 100_000, frame || Frame.new(0, 0, 100_000, 100_000))
134
+ margin = box.style.margin if box.style.margin?
135
+ @fit_result.x = margin&.left.to_i
136
+ @fit_result.y = margin&.bottom.to_i
137
+ @fit_result.mask = Geom2D::Rectangle(0, 0, @fit_result.x + box.width + margin&.right.to_i,
138
+ @fit_result.y + box.height + margin&.top.to_i)
145
139
  end
146
140
 
147
141
  end
@@ -189,19 +189,9 @@ module HexaPDF
189
189
  private
190
190
 
191
191
  # Fits the list box into the current region of the frame.
192
- def fit_content(available_width, available_height, frame)
193
- @width = if @initial_width > 0
194
- @initial_width
195
- else
196
- (style.position == :flow ? frame.width : available_width)
197
- end
198
- height = if @initial_height > 0
199
- @initial_height - reserved_height
200
- else
201
- (style.position == :flow ? frame.y - frame.bottom : available_height) - reserved_height
202
- end
203
-
192
+ def fit_content(_available_width, _available_height, frame)
204
193
  width = @width - reserved_width
194
+ height = @height - reserved_height
205
195
  left = (style.position == :flow ? frame.left : frame.x) + reserved_width_left
206
196
  top = frame.y - reserved_height_top
207
197
 
@@ -245,7 +235,7 @@ module HexaPDF
245
235
  Array(child).each {|ibox| box_fitter.fit(ibox) }
246
236
  item_result.box_fitter = box_fitter
247
237
  item_result.height = [item_result.height.to_i, box_fitter.content_heights[0]].max
248
- @results << item_result
238
+ @results << item_result unless box_fitter.fit_results.empty?
249
239
 
250
240
  top -= item_result.height + item_spacing
251
241
  height -= item_result.height + item_spacing
@@ -253,7 +243,7 @@ module HexaPDF
253
243
  break if !box_fitter.success? || height <= 0
254
244
  end
255
245
 
256
- @height = @results.sum(&:height) + (@results.count - 1) * item_spacing + reserved_height
246
+ update_content_height { @results.sum(&:height) + (@results.count - 1) * item_spacing }
257
247
 
258
248
  if @results.size == @children.size && @results.all? {|r| r.box_fitter.success? }
259
249
  fit_result.success!
@@ -326,37 +316,32 @@ module HexaPDF
326
316
  def item_marker_box(document, index)
327
317
  return @marker_type.call(document, self, index) if @marker_type.kind_of?(Proc)
328
318
 
329
- unless (fragment = @item_marker_fragment)
319
+ unless (items = @item_marker_items)
330
320
  marker_style = {
331
- font: style.font? ? style.font : document.fonts.add("Times"),
332
- font_size: style.font_size || 10, fill_color: style.fill_color
321
+ font_size: style.font_size || 10,
322
+ fill_color: style.fill_color,
333
323
  }
334
- fragment = case @marker_type
335
- when :disc
336
- TextFragment.create("•", marker_style)
337
- when :circle
338
- unless marker_style[:font].decode_codepoint("❍".ord).valid?
339
- marker_style[:font] = document.fonts.add("ZapfDingbats")
340
- end
341
- TextFragment.create("❍", **marker_style,
342
- font_size: style.font_size / 2.0,
343
- text_rise: -style.font_size / 1.8)
344
- when :square
345
- unless marker_style[:font].decode_codepoint("■".ord).valid?
346
- marker_style[:font] = document.fonts.add("ZapfDingbats")
347
- end
348
- TextFragment.create("■", **marker_style,
349
- font_size: style.font_size / 2.0,
350
- text_rise: -style.font_size / 1.8)
351
- when :decimal
352
- text = (@start_number + index).to_s << "."
353
- TextFragment.create(text, marker_style)
354
- else
355
- raise HexaPDF::Error, "Unknown list marker type #{@marker_type.inspect}"
356
- end
357
- @item_marker_fragment = fragment unless @marker_type == :decimal
324
+ marker_style[:font] = style.font if style.font?
325
+ items = case @marker_type
326
+ when :disc
327
+ document.layout.text_fragments("•", style: marker_style)
328
+ when :circle
329
+ document.layout.text_fragments("", style: marker_style,
330
+ font_size: style.font_size / 2.0,
331
+ text_rise: -style.font_size / 1.8)
332
+ when :square
333
+ document.layout.text_fragments("■", style: marker_style,
334
+ font_size: style.font_size / 2.0,
335
+ text_rise: -style.font_size / 1.8)
336
+ when :decimal
337
+ text = (@start_number + index).to_s << "."
338
+ document.layout.text_fragments(text, style: marker_style)
339
+ else
340
+ raise HexaPDF::Error, "Unknown list marker type #{@marker_type.inspect}"
341
+ end
342
+ @item_marker_items = items unless @marker_type == :decimal
358
343
  end
359
- TextBox.new(items: [fragment], style: {text_align: :right, padding: [0, 5, 0, 0]})
344
+ TextBox.new(items: items, style: {text_align: :right, padding: [0, 5, 0, 0]})
360
345
  end
361
346
 
362
347
  # Draws the list items onto the canvas at position [x, y].
@@ -41,7 +41,10 @@ require 'hexapdf/layout/frame'
41
41
  module HexaPDF
42
42
  module Layout
43
43
 
44
- # A PageStyle defines the initial look of a page and the placement of one or more frames.
44
+ # A PageStyle defines the dimensions of a page, its initial look, the Frame for object placement
45
+ # and which page style should be used next.
46
+ #
47
+ # This class is used by HexaPDF::Composer to style the individual pages.
45
48
  class PageStyle
46
49
 
47
50
  # The page size.
@@ -65,14 +68,15 @@ module HexaPDF
65
68
  # The callable object is given a canvas and the page style as arguments. It needs to draw the
66
69
  # initial content of the page. Note that the graphics state of the canvas is *not* saved
67
70
  # before executing the template code and restored afterwards. If this is needed, the object
68
- # needs to do it itself.
71
+ # needs to do it itself. The #next_style attribute can optionally be set.
69
72
  #
70
- # Furthermore it should set the #frame and #next_style attributes appropriately, if not done
71
- # beforehand. The #create_frame method can be used for easily creating a rectangular frame.
73
+ # Furthermore, the callable object should set the #frame that defines the area on the page
74
+ # where content should be placed. The #create_frame method can be used for easily creating a
75
+ # rectangular frame.
72
76
  #
73
77
  # Example:
74
78
  #
75
- # page_style.template = lambda do |canvas, style
79
+ # page_style.template = lambda do |canvas, style|
76
80
  # box = canvas.context.box
77
81
  # canvas.fill_color("fd0") do
78
82
  # canvas.rectangle(0, 0, box.width, box.height).fill
@@ -81,22 +85,24 @@ module HexaPDF
81
85
  # end
82
86
  attr_accessor :template
83
87
 
84
- # The HexaPDF::Layout::Frame object that defines the area on the page where content should be
85
- # placed.
88
+ # The Frame object that defines the area for the last page created with #create_page where
89
+ # content should be placed.
86
90
  #
87
- # This can either be set beforehand or during execution of the #template.
88
- #
89
- # If no frame has been set, a frame covering the page except for a default margin on all sides
90
- # is set during #create_page.
91
+ # This value is usually updated during execution of the #template. If the value is not
92
+ # updated, a frame covering the page except for a default margin on all sides is set during
93
+ # #create_page.
91
94
  attr_accessor :frame
92
95
 
93
96
  # Defines the name of the page style that should be used for the next page.
94
97
  #
98
+ # Note that this value can be different each time a new page is created via #create_page.
99
+ #
95
100
  # If this attribute is +nil+ (the default), it means that this style should be used again.
96
101
  attr_accessor :next_style
97
102
 
98
103
  # Creates a new page style instance for the given page size, orientation and next style
99
- # values. If a block is given, it is used as template for defining the initial content.
104
+ # values. If a block is given, it is used as #template for defining the initial content of a
105
+ # page.
100
106
  #
101
107
  # Example:
102
108
  #
@@ -113,14 +119,15 @@ module HexaPDF
113
119
  @next_style = next_style
114
120
  end
115
121
 
116
- # Creates a new page in the given document with this page style and returns it.
122
+ # Creates a new page in the given document using this page style and returns it.
117
123
  #
118
- # If #frame has not been set beforehand or during execution of the #template, a default frame
119
- # covering the whole page except a margin of 36 is created.
124
+ # If the #frame has not changed during execution of the #template, a default frame covering
125
+ # the whole page except a margin of 36 is assigned.
120
126
  def create_page(document)
127
+ frame_before = @frame
121
128
  page = document.pages.create(media_box: page_size, orientation: orientation)
122
129
  template&.call(page.canvas, self)
123
- self.frame ||= create_frame(page, 36)
130
+ self.frame = create_frame(page, 36) if @frame.equal?(frame_before)
124
131
  page
125
132
  end
126
133
 
@@ -393,7 +393,7 @@ module HexaPDF
393
393
  # The object resolved in this way needs to respond to #call(canvas, box) where +canvas+ is the
394
394
  # HexaPDF::Content::Canvas object on which it should be drawn and +box+ is a box-like object
395
395
  # (e.g. Box or TextFragment). The coordinate system is translated so that the origin is at the
396
- # bottom left corner of the box during the drawing operations.
396
+ # bottom-left corner of the box during the drawing operations.
397
397
  class Layers
398
398
 
399
399
  # Creates a new Layers object popuplated with the given +layers+.
@@ -1268,8 +1268,8 @@ module HexaPDF
1268
1268
  # composer.lorem_ipsum(position: :flow)
1269
1269
  #
1270
1270
  # [x, y]::
1271
- # Position the box with the bottom left corner at the given absolute position relative to
1272
- # the bottom left corner of the frame.
1271
+ # Position the box with the bottom-left corner at the given absolute position relative to
1272
+ # the bottom-left corner of the frame.
1273
1273
  #
1274
1274
  # Examples:
1275
1275
  #
@@ -1369,8 +1369,8 @@ module HexaPDF
1369
1369
  # composer.text('Text underneath')
1370
1370
  #
1371
1371
  # :fill_frame_horizontal::
1372
- # The mask covers the box including the margin around the box and the space to the left
1373
- # and right in the frame.
1372
+ # The mask covers the box including the margin around the box, the space to the left and
1373
+ # right in the frame and the space to the top of the current region.
1374
1374
  #
1375
1375
  # Examples:
1376
1376
  #
@@ -228,18 +228,22 @@ module HexaPDF
228
228
  case children
229
229
  when Box
230
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?
231
+ if child_result.success?
232
+ @preferred_width = child_result.x + child_result.box.width + reserved_width
233
+ @height = @preferred_height = child_result.box.height + reserved_height
234
+ @fit_results = [child_result]
235
+ fit_result.success!
236
+ end
235
237
  when Array
236
238
  box_fitter = BoxFitter.new([frame])
237
239
  children.each {|box| box_fitter.fit(box) }
238
- max_x_result = box_fitter.fit_results.max_by {|result| result.x + result.box.width }
239
- @preferred_width = max_x_result.x + max_x_result.box.width + reserved_width
240
- @height = @preferred_height = box_fitter.content_heights[0] + reserved_height
241
- @fit_results = box_fitter.fit_results
242
- fit_result.success! if box_fitter.success?
240
+ if box_fitter.success?
241
+ max_x_result = box_fitter.fit_results.max_by {|result| result.x + result.box.width }
242
+ @preferred_width = max_x_result.x + max_x_result.box.width + reserved_width
243
+ @height = @preferred_height = box_fitter.content_heights[0] + reserved_height
244
+ @fit_results = box_fitter.fit_results
245
+ fit_result.success!
246
+ end
243
247
  else
244
248
  @preferred_width = reserved_width
245
249
  @height = @preferred_height = reserved_height
@@ -590,7 +594,7 @@ module HexaPDF
590
594
  def fit_content(_available_width, _available_height, frame)
591
595
  # Adjust reserved width/height to include space used by the edge cells for their border
592
596
  # since cell borders are drawn on the bounds and not inside.
593
- # This uses the top left and bottom right cells and so might not be correct in all cases.
597
+ # This uses the top-left and bottom-right cells and so might not be correct in all cases.
594
598
  @cell_tl_border_width = @cells[0, 0].style.border.width
595
599
  cell_br_border_width = @cells[-1, -1].style.border.width
596
600
  rw = (@cell_tl_border_width.left + cell_br_border_width.right) / 2.0
@@ -42,12 +42,46 @@ module HexaPDF
42
42
  # A TextBox is used for drawing text, either inside a rectangular box or by flowing it around
43
43
  # objects of a Frame.
44
44
  #
45
+ # The standard usage is through the helper methods Document::Layout#text and
46
+ # Document::Layout#formatted_text.
47
+ #
45
48
  # This class uses TextLayouter behind the scenes to do the hard work.
46
49
  #
47
50
  # == Used Box Properties
48
51
  #
49
52
  # The spacing after the last line can be controlled via the style property +last_line_gap+. Also
50
53
  # see TextLayouter#style for other style properties taken into account.
54
+ #
55
+ # == Limitations
56
+ #
57
+ # When setting the style property 'position' to +:flow+, padding and border to the left and
58
+ # right as well as a predefined fixed width are not respected and the result will look wrong.
59
+ #
60
+ # == Examples
61
+ #
62
+ # Showing some text:
63
+ #
64
+ # #>pdf-composer
65
+ # composer.box(:text, items: layout.text_fragments("This is some text."))
66
+ # # Or easier with the provided convenience method
67
+ # composer.text("This is also some text")
68
+ #
69
+ # It is possible to flow the text around other objects by using the style property
70
+ # 'position' with the value +:flow+:
71
+ #
72
+ # #>pdf-composer
73
+ # composer.box(:base, width: 30, height: 30,
74
+ # style: {margin: 5, position: :float, background_color: "hp-blue-light"})
75
+ # composer.text("This is some text. " * 20, position: :flow)
76
+ #
77
+ # While top and bottom padding and border can be used with flow positioning, left and right
78
+ # padding and border are not supported and the result will look wrong:
79
+ #
80
+ # #>pdf-composer
81
+ # composer.box(:base, width: 30, height: 30,
82
+ # style: {margin: 5, position: :float, background_color: "hp-blue-light"})
83
+ # composer.text("This is some text. " * 20, padding: 10, position: :flow,
84
+ # text_align: :justify)
51
85
  class TextBox < Box
52
86
 
53
87
  # Creates a new TextBox object with the given inline items (e.g. TextFragment and InlineBox
@@ -72,11 +106,6 @@ module HexaPDF
72
106
  true
73
107
  end
74
108
 
75
- # :nodoc:
76
- def draw(canvas, x, y)
77
- super(canvas, x + @x_offset, y)
78
- end
79
-
80
109
  # :nodoc:
81
110
  def empty?
82
111
  super && (!@result || @result.lines.empty?)
@@ -89,40 +118,35 @@ module HexaPDF
89
118
  # Depending on the 'position' style property, the text is either fit into the current region
90
119
  # of the frame using +available_width+ and +available_height+, or fit to the shape of the
91
120
  # frame starting from the top (when 'position' is set to :flow).
92
- def fit_content(available_width, available_height, frame)
121
+ def fit_content(_available_width, _available_height, frame)
93
122
  frame = frame.child_frame(box: self)
94
- @width = @x_offset = @height = 0
123
+ @x_offset = 0
95
124
 
96
- @result = if style.position == :flow
97
- @tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
125
+ if style.position == :flow
126
+ height = (@initial_height > 0 ? @initial_height : frame.shape.bbox.height) - reserved_height
127
+ @result = @tl.fit(@items, frame.width_specification(reserved_height_top), height,
128
+ apply_first_text_indent: !split_box?, frame: frame)
129
+ min_x = +Float::INFINITY
130
+ max_x = -Float::INFINITY
131
+ @result.lines.each do |line|
132
+ min_x = [min_x, line.x_offset].min
133
+ max_x = [max_x, line.x_offset + line.width].max
134
+ end
135
+ @width = (min_x.finite? ? max_x - min_x : 0) + reserved_width
136
+ fit_result.x = @x_offset = min_x
137
+ @height = @initial_height > 0 ? @initial_height : @result.height + reserved_height
138
+ else
139
+ @result = @tl.fit(@items, @width - reserved_width, @height - reserved_height,
98
140
  apply_first_text_indent: !split_box?, frame: frame)
99
- else
100
- @width = reserved_width
101
- @height = reserved_height
102
- width = (@initial_width > 0 ? @initial_width : available_width) - @width
103
- height = (@initial_height > 0 ? @initial_height : available_height) - @height
104
- @tl.fit(@items, width, height, apply_first_text_indent: !split_box?, frame: frame)
105
- end
106
-
107
- @width += if @initial_width > 0 || style.text_align == :center || style.text_align == :right
108
- width
109
- elsif style.position == :flow
110
- min_x = +Float::INFINITY
111
- max_x = -Float::INFINITY
112
- @result.lines.each do |line|
113
- min_x = [min_x, line.x_offset].min
114
- max_x = [max_x, line.x_offset + line.width].max
115
- end
116
- min_x.finite? ? (@x_offset = min_x; max_x - min_x) : 0
117
- else
118
- @result.lines.max_by(&:width)&.width || 0
119
- end
120
- @height += if @initial_height > 0 || style.text_valign == :center || style.text_valign == :bottom
121
- height
122
- else
123
- @result.height
124
- end
125
- if style.last_line_gap && @result.lines.last
141
+ if style.text_align == :left && @initial_width == 0
142
+ @width = (@result.lines.max_by(&:width)&.width || 0) + reserved_width
143
+ end
144
+ if style.text_valign == :top && @initial_height == 0
145
+ @height = @result.height + reserved_height
146
+ end
147
+ end
148
+
149
+ if style.last_line_gap && @result.lines.last && @initial_height == 0
126
150
  @height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
127
151
  end
128
152
 
@@ -52,7 +52,7 @@ module HexaPDF
52
52
  # The items of a text fragment may be frozen to indicate that the fragment is potentially used
53
53
  # multiple times.
54
54
  #
55
- # The rectangle with the bottom left corner (#x_min, #y_min) and the top right corner (#x_max,
55
+ # The rectangle with the bottom-left corner (#x_min, #y_min) and the top-right corner (#x_max,
56
56
  # #y_max) describes the minimum bounding box of the whole text fragment and is usually *not*
57
57
  # equal to the box (0, 0)-(#width, #height).
58
58
  class TextFragment
@@ -218,7 +218,10 @@ module HexaPDF
218
218
 
219
219
  # Breaks are detected at: space, tab, zero-width-space, non-breaking space, hyphen,
220
220
  # soft-hypen and any valid Unicode newline separator
221
- BREAK_RE = /[ \u{A}-\u{D}\u{85}\u{2028}\u{2029}\t\u{200B}\u{00AD}\u{00A0}-]/
221
+ BREAK_CHARS = {}
222
+ " \u{A}\u{B}\u{C}\u{D}\u{85}\u{2028}\u{2029}\t\u{200B}\u{00AD}\u{00A0}-".each_char do |c|
223
+ BREAK_CHARS[c] = true
224
+ end
222
225
 
223
226
  # Breaks the items (an array of InlineBox and TextFragment objects) into atomic pieces
224
227
  # wrapped by Box, Glue or Penalty items, and returns those as an array.
@@ -235,7 +238,7 @@ module HexaPDF
235
238
  # Collect characters and kerning values until break character is encountered
236
239
  box_items = []
237
240
  while (glyph = item.items[i]) &&
238
- (glyph.kind_of?(Numeric) || !BREAK_RE.match?(glyph.str))
241
+ (glyph.kind_of?(Numeric) || !BREAK_CHARS.key?(glyph.str))
239
242
  box_items << glyph
240
243
  i += 1
241
244
  end
@@ -428,9 +431,7 @@ module HexaPDF
428
431
  end
429
432
 
430
433
  line = create_unjustified_line
431
- last_line_used = true
432
- last_line_used = yield(line, nil) if item.nil? && !line.items.empty?
433
-
434
+ last_line_used = (item.nil? && !line.items.empty? ? yield(line, nil) : true)
434
435
  item.nil? && last_line_used ? [] : @items[@beginning_of_line_index..-1]
435
436
  end
436
437
 
@@ -500,9 +501,7 @@ module HexaPDF
500
501
  end
501
502
 
502
503
  line = create_unjustified_line
503
- last_line_used = true
504
- last_line_used = yield(line, nil) if item.nil? && !line.items.empty?
505
-
504
+ last_line_used = (item.nil? && !line.items.empty? ? yield(line, nil) : true)
506
505
  item.nil? && last_line_used ? [] : @items[@beginning_of_line_index..-1]
507
506
  end
508
507
 
@@ -71,6 +71,10 @@ module HexaPDF
71
71
  end
72
72
 
73
73
  # Returns +true+ if the PDF file is a linearized file.
74
+ #
75
+ # Note: The method uses heuristics to determine whether a PDF file is linearized. In case of
76
+ # slightly invalid or damaged PDFs that HexaPDF can recover from it is possible that this method
77
+ # returns +true+ even though the PDF isn't actually linearized.
74
78
  def linearized?
75
79
  @linearized ||=
76
80
  begin
@@ -293,7 +297,7 @@ module HexaPDF
293
297
  next
294
298
  elsif type == 'n'
295
299
  if pos == 0 || gen > 65535
296
- maybe_raise("Invalid in use cross-reference entry",
300
+ maybe_raise("Invalid in use cross-reference entry for object number #{oid}",
297
301
  pos: @tokenizer.pos)
298
302
  xref.add_free_entry(oid, gen)
299
303
  else
@@ -48,8 +48,8 @@ module HexaPDF
48
48
  #
49
49
  # [left, bottom, right, top]
50
50
  #
51
- # where +left+ is the bottom left x-coordinate, +bottom+ is the bottom left y-coordinate, +right+
52
- # is the top right x-coordinate and +top+ is the top right y-coordinate.
51
+ # where +left+ is the bottom-left x-coordinate, +bottom+ is the bottom-left y-coordinate, +right+
52
+ # is the top-right x-coordinate and +top+ is the top-right y-coordinate.
53
53
  #
54
54
  # See: PDF2.0 s7.9.5
55
55
  class Rectangle < HexaPDF::PDFArray
@@ -119,8 +119,8 @@ module HexaPDF
119
119
  #:nodoc:
120
120
  RECTANGLE_ERROR_MSG = "A PDF rectangle structure must contain an array of four numbers"
121
121
 
122
- # Ensures that the value is an array containing four numbers that specify the bottom left and
123
- # top right corner.
122
+ # Ensures that the value is an array containing four numbers that specify the bottom-left and
123
+ # top-right corners.
124
124
  def after_data_change
125
125
  super
126
126
  unless value.size == 4 && all? {|v| v.kind_of?(Numeric) }
@@ -97,7 +97,7 @@ module HexaPDF
97
97
  merge_revision = offset
98
98
  end
99
99
 
100
- if merge_revision == offset
100
+ if merge_revision == offset && !revisions.empty?
101
101
  xref_section.merge!(revisions.first.xref_section)
102
102
  offset = trailer[:Prev] # Get possible next offset before overwriting trailer
103
103
  trailer = revisions.first.trailer
@@ -51,6 +51,9 @@ module HexaPDF
51
51
  # normalized to arrays on assignment to ease further processing.
52
52
  class StreamData
53
53
 
54
+ # The source.
55
+ attr_reader :source
56
+
54
57
  # The filter(s) that need to be applied for getting the decoded stream data.
55
58
  attr_reader :filter
56
59
 
@@ -110,9 +113,6 @@ module HexaPDF
110
113
 
111
114
  protected
112
115
 
113
- # The source.
114
- attr_reader :source
115
-
116
116
  # The optional offset into the bytes provided by source.
117
117
  attr_reader :offset
118
118
 
@@ -82,6 +82,7 @@ module HexaPDF
82
82
  # correctable situations are only raised if the return value of calling the object is +true+.
83
83
  def initialize(io, on_correctable_error: nil)
84
84
  @io = io
85
+ @io_chunk = String.new(''.b)
85
86
  @ss = StringScanner.new(''.b)
86
87
  @original_pos = -1
87
88
  @on_correctable_error = on_correctable_error || proc { false }
@@ -439,9 +440,9 @@ module HexaPDF
439
440
  @io.seek(@next_read_pos)
440
441
  return false if @io.eof?
441
442
 
442
- @ss << @io.read(8192)
443
+ @ss << @io.read(8192, @io_chunk)
443
444
  if @ss.pos > 8192 && @ss.string.length > 16384
444
- @ss.string.slice!(0, 8192)
445
+ @ss.string.replace(@ss.string.byteslice(8192..-1))
445
446
  @ss.pos -= 8192
446
447
  @original_pos += 8192
447
448
  end
@@ -65,6 +65,8 @@ module HexaPDF
65
65
  #
66
66
  # == Type Specific Field Flags
67
67
  #
68
+ # See the class description for Field for the general field flags.
69
+ #
68
70
  # :no_toggle_to_off:: Only used with radio buttons fields. If this flag is set, one button
69
71
  # needs to be selected at all times. Otherwise, clicking on the selected
70
72
  # button deselects it.
@@ -51,6 +51,8 @@ module HexaPDF
51
51
  #
52
52
  # == Type Specific Field Flags
53
53
  #
54
+ # See the class description for Field for the general field flags.
55
+ #
54
56
  # :combo:: If set, the field represents a combo box.
55
57
  #
56
58
  # :edit:: If set, the combo box includes an editable text box for entering arbitrary values.