hexapdf 0.44.0 → 0.46.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 +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.