hexapdf 0.44.0 → 0.45.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b30b54b90222fbcc5469b23153efc4b15083797f527af6c5b34b3f6e161832d
4
- data.tar.gz: 969c67ab85591e3b7ccad17023bd3bc820ea5effa4c01e53d254915ab1626d71
3
+ metadata.gz: cd31e769d2a906198da2a4194085cb2a884fa3879442b75add168d7f81d67d8b
4
+ data.tar.gz: 43c2b4ba4df2f997d470835995f2581aa306c639c63ddc892e648d94605f3b22
5
5
  SHA512:
6
- metadata.gz: b60934dd6534ccd019953b278fa188b77996634b3e3878332302b9a2acccfd13b493b57432cff2113b7cc7727f028f24301c2d050f9b908c1b43cf66fbdeebfd
7
- data.tar.gz: fdd792109306bc7cf0e30bb2da770e58e81e333843e3c785a9a31873a296b7d027121b467b54a80405332735e99bdaf6b0d167c9eaf08dbc04a26922b890bb51
6
+ metadata.gz: f17a303dba8104974564a213c72c0f0862c520964a3255b575544489ccb5a17a468d093d3970817903c4eb798e888592410db18447d3264d535f3a01a4a94c82
7
+ data.tar.gz: d5727e2f2bfb5ed827ac92eecc8cd9e0ba73044150ba07d03623dccde43dc29db1cddc2fbce24bfd63268522e7965519e188271c2bc9c00162b1a660fc468b74
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 0.45.0 - 2024-06-18
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Document::Layout#styles] and [HexaPDF::Composer#styles] for defining
6
+ multiple styles at once
7
+
8
+ ### Changed
9
+
10
+ * [HexaPDF::Layout::Box#fit] to set width/height correctly for boxes with
11
+ position `:flow`
12
+
13
+ ### Fixed
14
+
15
+ * Regression in [HexaPDF::Layout::ListBox] that leads to missing markers
16
+ * [HexaPDF::Content::CanvasComposer#draw_box] to handle truncated boxes
17
+ * [HexaPDF::Layout::TableBox::Cell] to handle too-big content in all cases
18
+
19
+
1
20
  ## 0.44.0 - 2024-06-05
2
21
 
3
22
  ### Added
@@ -16,8 +16,10 @@
16
16
  require 'hexapdf'
17
17
 
18
18
  HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
19
- composer.style(:question, font_size: 16, margin: [0, 0, 16], fill_color: 'hp-blue')
20
- composer.style(:answer, font: 'ZapfDingbats', fill_color: "green")
19
+ composer.styles(
20
+ question: {font_size: 16, margin: [0, 0, 16], fill_color: 'hp-blue'},
21
+ answer: {font: 'ZapfDingbats', fill_color: "green"},
22
+ )
21
23
 
22
24
  all = composer.document.optional_content.ocg('All answers')
23
25
  a1 = composer.document.optional_content.ocg('Answer 1')
@@ -38,7 +40,7 @@ HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
38
40
  answers.text('Guido van Rossum')
39
41
  answers.multiple do |answer|
40
42
  answer.text('Yukihiro “Matz” Matsumoto', position: :float)
41
- answer.text("\u{a0}\u{a0}4", style: :answer,
43
+ answer.text("\u{a0}\u{a0}", style: :answer,
42
44
  properties: {'optional_content' => a1})
43
45
  end
44
46
  answers.text('Rob Pike')
@@ -54,7 +56,7 @@ HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
54
56
  answers.text('1992')
55
57
  answers.multiple do |answer|
56
58
  answer.text('1993', position: :float)
57
- answer.text("\u{a0}\u{a0}4", style: :answer,
59
+ answer.text("\u{a0}\u{a0}", style: :answer,
58
60
  properties: {'optional_content' => a2})
59
61
  end
60
62
  end
data/examples/030-pdfa.rb CHANGED
@@ -28,17 +28,18 @@ HexaPDF::Composer.create('pdfa.pdf') do |composer|
28
28
  }
29
29
 
30
30
  # Define all styles
31
- composer.style(:base, font: 'Lato', font_size: 10, line_spacing: 1.3)
32
- composer.style(:top, font_size: 8)
33
- composer.style(:top_box, padding: [100, 0, 0], margin: [0, 0, 10], border: {width: [0, 0, 1]})
34
- composer.style(:header, font: 'Lato bold', font_size: 20, margin: [50, 0, 20])
35
- composer.style(:line_items, border: {width: 1, color: "eee"}, margin: [20, 0])
36
- composer.style(:line_item_cell, font_size: 8)
37
- composer.style(:footer, border: {width: [1, 0, 0], color: "darkgrey"},
38
- padding: [5, 0, 0], valign: :bottom)
39
- composer.style(:footer_heading, font: 'Lato bold',
40
- font_size: 8, padding: [0, 0, 8])
41
- composer.style(:footer_text, font_size: 8, fill_color: "darkgrey")
31
+ composer.styles(
32
+ base: {font: 'Lato', font_size: 10, line_spacing: 1.3},
33
+ top: {font_size: 8},
34
+ top_box: {padding: [100, 0, 0], margin: [0, 0, 10], border: {width: [0, 0, 1]}},
35
+ header: {font: 'Lato bold', font_size: 20, margin: [50, 0, 20]},
36
+ line_items: {border: {width: 1, color: "eee"}, margin: [20, 0]},
37
+ line_item_cell: {font_size: 8},
38
+ footer: {border: {width: [1, 0, 0], color: "darkgrey"}, padding: [5, 0, 0],
39
+ valign: :bottom},
40
+ footer_heading: {font: 'Lato bold', font_size: 8, padding: [0, 0, 8]},
41
+ footer_text: {font_size: 8, fill_color: "darkgrey"},
42
+ )
42
43
 
43
44
  # Top part
44
45
  composer.box(:container, style: :top_box) do |container|
@@ -254,6 +254,28 @@ module HexaPDF
254
254
  @document.layout.style(name, base: base, **properties)
255
255
  end
256
256
 
257
+ # :call-seq:
258
+ # composer.styles -> styles
259
+ # composer.styles(**mapping) -> styles
260
+ #
261
+ # Creates multiple named styles at once if +mapping+ is provided, and returns the style mapping.
262
+ #
263
+ # See HexaPDF::Document::Layout#styles for details; this method is just a thin wrapper around
264
+ # that method.
265
+ #
266
+ # Example:
267
+ #
268
+ # composer.styles(
269
+ # base: {font_size: 12, leading: 1.2},
270
+ # header: {font: 'Helvetica', fill_color: "008"},
271
+ # header1: {base: :header, font_size: 30}
272
+ # )
273
+ #
274
+ # See: HexaPDF::Layout::Style
275
+ def styles(**mapping)
276
+ @document.layout.styles(**mapping)
277
+ end
278
+
257
279
  # :call-seq:
258
280
  # composer.page_style(name) -> page_style
259
281
  # composer.page_style(name, **attributes, &template_block) -> page_style
@@ -1127,7 +1127,7 @@ module HexaPDF
1127
1127
  # canvas.rectangle(x, y, width, height, radius: 0) => canvas
1128
1128
  #
1129
1129
  # Appends a rectangle to the current path as a complete subpath (drawn in counterclockwise
1130
- # direction), with the bottom left corner specified by +x+ and +y+ and the given +width+ and
1130
+ # direction), with the bottom-left corner specified by +x+ and +y+ and the given +width+ and
1131
1131
  # +height+. Returns +self+.
1132
1132
  #
1133
1133
  # If +radius+ is greater than 0, the corners are rounded with the given radius.
@@ -1137,7 +1137,7 @@ module HexaPDF
1137
1137
  #
1138
1138
  # If there is no current path when the method is invoked, a new path is automatically begun.
1139
1139
  #
1140
- # The current point is set to the bottom left corner if +radius+ is zero, otherwise it is set
1140
+ # The current point is set to the bottom-left corner if +radius+ is zero, otherwise it is set
1141
1141
  # to (x, y + radius).
1142
1142
  #
1143
1143
  # Examples:
@@ -1720,7 +1720,7 @@ module HexaPDF
1720
1720
  # If the filename or the IO specifies a PDF file, the first page of this file is used to
1721
1721
  # create a form XObject which is then drawn.
1722
1722
  #
1723
- # The +at+ argument has to be an array containing two numbers specifying the bottom left
1723
+ # The +at+ argument has to be an array containing two numbers specifying the bottom-left
1724
1724
  # corner at which to draw the XObject.
1725
1725
  #
1726
1726
  # If +width+ and +height+ are specified, the drawn XObject will have exactly these
@@ -96,6 +96,7 @@ module HexaPDF
96
96
  draw_box, box = @frame.split(result)
97
97
  if draw_box
98
98
  @frame.draw(@canvas, result)
99
+ (box = draw_box; break) unless box
99
100
  elsif !@frame.find_next_region
100
101
  raise HexaPDF::Error, "Frame for canvas composer is full and box doesn't fit anymore"
101
102
  end
@@ -175,9 +175,6 @@ module HexaPDF
175
175
 
176
176
  end
177
177
 
178
- # The mapping of style name (a Symbol) to Layout::Style instance.
179
- attr_reader :styles
180
-
181
178
  # Creates a new Layout object for the given PDF document.
182
179
  def initialize(document)
183
180
  @document = document
@@ -219,6 +216,21 @@ module HexaPDF
219
216
  style
220
217
  end
221
218
 
219
+ # :call-seq:
220
+ # layout.styles -> styles
221
+ # layout.styles(**mapping) -> styles
222
+ #
223
+ # Returns the mapping of style names to Layout::Style instances. If +mapping+ is provided,
224
+ # also defines the given styles using #style.
225
+ #
226
+ # The argument +mapping+ needs to be a hash mapping a style name (a Symbol) to style
227
+ # properties. The special key +:base+ can be used to define the base style. For details see
228
+ # #style.
229
+ def styles(**mapping)
230
+ mapping.each {|name, properties| style(name, **properties) } unless mapping.empty?
231
+ @styles
232
+ end
233
+
222
234
  # Creates an inline box for use together with text fragments.
223
235
  #
224
236
  # The +valign+ argument ist used to specify the vertical alignment of the box within the text
@@ -51,7 +51,7 @@ module HexaPDF
51
51
  attr_accessor :name
52
52
 
53
53
  # Character bounding box as array of four numbers, specifying the x- and y-coordinates of
54
- # the bottom left corner and the x- and y-coordinates of the top right corner.
54
+ # the bottom-left corner and the x- and y-coordinates of the top-right corner.
55
55
  attr_accessor :bbox
56
56
 
57
57
  end
@@ -63,7 +63,7 @@ module HexaPDF
63
63
  attr_accessor :weight
64
64
 
65
65
  # The font bounding box as array of four numbers, specifying the x- and y-coordinates of the
66
- # bottom left corner and the x- and y-coordinates of the top right corner.
66
+ # bottom-left corner and the x- and y-coordinates of the top-right corner.
67
67
  attr_accessor :bounding_box
68
68
 
69
69
  # The y-value of the top of the capital H (or 0 or nil if the font doesn't contain a capital
@@ -323,10 +323,12 @@ module HexaPDF
323
323
  # current region of the frame, adjusted for this box. The frame itself is provided as third
324
324
  # argument.
325
325
  #
326
- # The default implementation uses the given available width and height for the box width and
327
- # height if they were initially set to 0. Otherwise the intially specified dimensions are
328
- # used. The method returns early if the thus configured box already doesn't fit. Otherwise,
329
- # the #fit_content method is called which allows sub-classes to fit their content.
326
+ # If the box uses flow positioning, the width is set to the frame's width and the height to
327
+ # the remaining height in the frame. Otherwise the given available width and height are used
328
+ # for the width and height if they were initially set to 0. Otherwise the intially specified
329
+ # dimensions are used. The method returns early if the thus configured box already doesn't
330
+ # fit. Otherwise, the #fit_content method is called which allows sub-classes to fit their
331
+ # content.
330
332
  #
331
333
  # The following variables are set that may later be used during splitting or drawing:
332
334
  #
@@ -334,10 +336,23 @@ module HexaPDF
334
336
  # used to adjust the drawing position in #draw_content if necessary.
335
337
  def fit(available_width, available_height, frame)
336
338
  @fit_result.reset(frame)
337
- @width = (@initial_width > 0 ? @initial_width : available_width)
338
- @height = (@initial_height > 0 ? @initial_height : available_height)
339
- return @fit_result if style.position != :flow && (float_compare(@width, available_width) > 0 ||
340
- float_compare(@height, available_height) > 0)
339
+ position_flow = supports_position_flow? && style.position == :flow
340
+ @width = if @initial_width > 0
341
+ @initial_width
342
+ elsif position_flow
343
+ frame.width
344
+ else
345
+ available_width
346
+ end
347
+ @height = if @initial_height > 0
348
+ @initial_height
349
+ elsif position_flow
350
+ frame.y - frame.bottom
351
+ else
352
+ available_height
353
+ end
354
+ return @fit_result if !position_flow && (float_compare(@width, available_width) > 0 ||
355
+ float_compare(@height, available_height) > 0)
341
356
 
342
357
  fit_content(available_width, available_height, frame)
343
358
 
@@ -379,7 +394,7 @@ module HexaPDF
379
394
  # system is translated so that the origin is at the bottom left corner of the **content box**.
380
395
  #
381
396
  # Subclasses should not rely on the +@draw_block+ but implement the #draw_content method. The
382
- # coordinates passed to it are also modified to represent the bottom left corner of the
397
+ # coordinates passed to it are also modified to represent the bottom-left corner of the
383
398
  # content box but the coordinate system is not translated.
384
399
  def draw(canvas, x, y)
385
400
  if @fit_result.overflow? && @initial_height > 0 && style.overflow == :error
@@ -142,19 +142,11 @@ module HexaPDF
142
142
 
143
143
  # Fits the column box into the current region of the frame.
144
144
  #
145
- def fit_content(available_width, available_height, frame)
145
+ def fit_content(_available_width, _available_height, frame)
146
146
  initial_fit_successful = (@equal_height && @columns.size > 1 ? nil : false)
147
147
  tries = 0
148
- width = if style.position == :flow
149
- (@initial_width > 0 ? @initial_width : frame.width) - reserved_width
150
- else
151
- (@initial_width > 0 ? @initial_width : available_width) - reserved_width
152
- end
153
- height = if style.position == :flow
154
- (@initial_height > 0 ? @initial_height : frame.y - frame.bottom) - reserved_height
155
- else
156
- (@initial_height > 0 ? @initial_height : available_height) - reserved_height
157
- end
148
+ width = @width - reserved_width
149
+ height = @height - reserved_height
158
150
 
159
151
  columns = calculate_columns(width)
160
152
  return if columns.empty?
@@ -52,7 +52,7 @@ module HexaPDF
52
52
  # setting the style properties 'mask_mode', 'align' and 'valign', it is possible to lay out the
53
53
  # children bottom to top, left to right, or right to left:
54
54
  #
55
- # * The standard top to bottom layout:
55
+ # * The standard top-to-bottom layout:
56
56
  #
57
57
  # #>pdf-composer100
58
58
  # composer.container do |container|
@@ -61,7 +61,7 @@ module HexaPDF
61
61
  # container.box(:base, height: 20, style: {background_color: "hp-blue-light"})
62
62
  # end
63
63
  #
64
- # * The bottom to top layout (using valign = :bottom to fill up from the bottom and mask_mode =
64
+ # * The bottom-to-top layout (using valign = :bottom to fill up from the bottom and mask_mode =
65
65
  # :fill_horizontal to only remove the area to the left and right of the box):
66
66
  #
67
67
  # #>pdf-composer100
@@ -74,7 +74,7 @@ module HexaPDF
74
74
  # mask_mode: :fill_horizontal, valign: :bottom})
75
75
  # end
76
76
  #
77
- # * The left to right layout (using mask_mode = :fill_vertical to fill the area to the top and
77
+ # * The left-to-right layout (using mask_mode = :fill_vertical to fill the area to the top and
78
78
  # bottom of the box):
79
79
  #
80
80
  # #>pdf-composer100
@@ -87,7 +87,7 @@ module HexaPDF
87
87
  # mask_mode: :fill_vertical})
88
88
  # end
89
89
  #
90
- # * The right to left layout (using align = :right to fill up from the right and mask_mode =
90
+ # * The right-to-left layout (using align = :right to fill up from the right and mask_mode =
91
91
  # :fill_vertical to fill the area to the top and bottom of the box):
92
92
  #
93
93
  # #>pdf-composer100
@@ -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
@@ -84,10 +84,10 @@ module HexaPDF
84
84
 
85
85
  include HexaPDF::Utils
86
86
 
87
- # The x-coordinate of the bottom left corner.
87
+ # The x-coordinate of the bottom-left corner.
88
88
  attr_reader :left
89
89
 
90
- # The y-coordinate of the bottom left corner.
90
+ # The y-coordinate of the bottom-left corner.
91
91
  attr_reader :bottom
92
92
 
93
93
  # The width of the frame.
@@ -127,7 +127,7 @@ module HexaPDF
127
127
 
128
128
  # An array of box objects representing the parent boxes.
129
129
  #
130
- # The immediate parent is the last array entry, the top most parent the first one. All boxes
130
+ # The immediate parent is the last array entry, the top-most parent the first one. All boxes
131
131
  # that are fitted into this frame have to be child boxes of the immediate parent box.
132
132
  attr_reader :parent_boxes
133
133
 
@@ -216,6 +216,7 @@ module HexaPDF
216
216
  end
217
217
 
218
218
  fit_result = box.fit(aw, ah, self)
219
+ return fit_result if fit_result.failure?
219
220
 
220
221
  width = box.width
221
222
  height = box.height
@@ -382,7 +383,7 @@ module HexaPDF
382
383
  # Since not all text may start at the top of the frame, the offset argument can be used to
383
384
  # specify a vertical offset from the top of the frame where layouting should start.
384
385
  #
385
- # To be compatible with TextLayouter, the top left corner of the bounding box of the frame's
386
+ # To be compatible with TextLayouter, the top-left corner of the bounding box of the frame's
386
387
  # shape is the origin of the coordinate system for the width specification, with positive
387
388
  # x-values to the right and positive y-values downwards.
388
389
  #
@@ -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
@@ -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
  #
@@ -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
@@ -89,40 +123,34 @@ module HexaPDF
89
123
  # Depending on the 'position' style property, the text is either fit into the current region
90
124
  # of the frame using +available_width+ and +available_height+, or fit to the shape of the
91
125
  # frame starting from the top (when 'position' is set to :flow).
92
- def fit_content(available_width, available_height, frame)
126
+ def fit_content(_available_width, _available_height, frame)
93
127
  frame = frame.child_frame(box: self)
94
- @width = @x_offset = @height = 0
128
+ @x_offset = 0
95
129
 
96
- @result = if style.position == :flow
97
- @tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
130
+ if style.position == :flow
131
+ height = (@initial_height > 0 ? @initial_height : frame.shape.bbox.height) - reserved_height
132
+ @result = @tl.fit(@items, frame.width_specification(reserved_height_top), height,
98
133
  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
134
+ min_x = +Float::INFINITY
135
+ max_x = -Float::INFINITY
136
+ @result.lines.each do |line|
137
+ min_x = [min_x, line.x_offset].min
138
+ max_x = [max_x, line.x_offset + line.width].max
139
+ end
140
+ @width = (min_x.finite? ? (@x_offset = min_x; max_x - min_x) : 0) + reserved_width
141
+ @height = @initial_height > 0 ? @initial_height : @result.height + reserved_height
142
+ else
143
+ @result = @tl.fit(@items, @width - reserved_width, @height - reserved_height,
144
+ apply_first_text_indent: !split_box?, frame: frame)
145
+ if style.text_align == :left && @initial_width == 0
146
+ @width = (@result.lines.max_by(&:width)&.width || 0) + reserved_width
147
+ end
148
+ if style.text_valign == :top && @initial_height == 0
149
+ @height = @result.height + reserved_height
150
+ end
151
+ end
152
+
153
+ if style.last_line_gap && @result.lines.last && @initial_height == 0
126
154
  @height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
127
155
  end
128
156
 
@@ -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
 
@@ -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) }
@@ -159,8 +159,8 @@ module HexaPDF
159
159
  # retained without the need for parsing its contents.
160
160
  #
161
161
  # If the bounding box of the form XObject doesn't have its origin at (0, 0), the canvas origin
162
- # is translated into the bottom left corner so that this detail doesn't matter when using the
163
- # canvas. This means that the canvas' origin is always at the bottom left corner of the
162
+ # is translated into the bottom-left corner so that this detail doesn't matter when using the
163
+ # canvas. This means that the canvas' origin is always at the bottom-left corner of the
164
164
  # bounding box.
165
165
  #
166
166
  # *Note* that a canvas can only be retrieved for initially empty form XObjects!
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.44.0'
40
+ VERSION = '0.45.0'
41
41
 
42
42
  end
@@ -47,21 +47,20 @@ describe HexaPDF::Content::CanvasComposer do
47
47
  end
48
48
 
49
49
  it "splits the box if possible" do
50
- @composer.draw_box(create_box(width: 400, style: {position: :float}))
51
- box = create_box(width: 400, height: 100)
52
- box.define_singleton_method(:split) do |*|
53
- [box, HexaPDF::Layout::Box.new(height: 100) {}]
54
- end
50
+ @composer.draw_box(create_box(width: 300, height: 300, style: {position: :float}))
51
+ box = create_box(style: {mask_mode: :box})
52
+ box.define_singleton_method(:fit_content) {|*| fit_result.overflow! }
53
+ box.define_singleton_method(:split_content) { [box, HexaPDF::Layout::Box.new(height: 100) {}] }
55
54
  @composer.draw_box(box)
56
55
  assert_operators(@composer.canvas.contents,
57
56
  [[:save_graphics_state],
58
- [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
57
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 541.889764]],
59
58
  [:restore_graphics_state],
60
59
  [:save_graphics_state],
61
- [:concatenate_matrix, [1, 0, 0, 1, 400, 741.889764]],
60
+ [:concatenate_matrix, [1, 0, 0, 1, 300, 0]],
62
61
  [:restore_graphics_state],
63
62
  [:save_graphics_state],
64
- [:concatenate_matrix, [1, 0, 0, 1, 400, 641.889764]],
63
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 441.889764]],
65
64
  [:restore_graphics_state]])
66
65
  end
67
66
 
@@ -77,6 +76,12 @@ describe HexaPDF::Content::CanvasComposer do
77
76
  [:restore_graphics_state]])
78
77
  end
79
78
 
79
+ it "handles truncated boxes correctly" do
80
+ box = create_box(height: 400, style: {overflow: :truncate})
81
+ box.define_singleton_method(:fit_content) {|*| fit_result.overflow! }
82
+ assert_same(box, @composer.draw_box(box))
83
+ end
84
+
80
85
  it "returns the last drawn box" do
81
86
  box = create_box(height: 400)
82
87
  assert_same(box, @composer.draw_box(box))
@@ -141,6 +141,22 @@ describe HexaPDF::Document::Layout do
141
141
  end
142
142
  end
143
143
 
144
+ describe "styles" do
145
+ it "returns the existing styles" do
146
+ @layout.style(:test, font_size: 20)
147
+ assert_equal([:base, :test], @layout.styles.keys)
148
+ end
149
+
150
+ it "sets multiple styles at once" do
151
+ styles = @layout.styles(
152
+ test: {font_size: 20},
153
+ test2: {font_size: 30},
154
+ )
155
+ assert_same(styles, @layout.styles)
156
+ assert_equal([:base, :test, :test2], @layout.styles.keys)
157
+ end
158
+ end
159
+
144
160
  describe "inline_box" do
145
161
  it "takes a box as argument" do
146
162
  box = HexaPDF::Layout::Box.create(width: 10, height: 10)
@@ -40,6 +40,9 @@ describe HexaPDF::Layout::Box do
40
40
  @frame = Object.new
41
41
  def @frame.x; 0; end
42
42
  def @frame.y; 100; end
43
+ def @frame.bottom; 40; end
44
+ def @frame.width; 150; end
45
+ def @frame.height; 150; end
43
46
  end
44
47
 
45
48
  def create_box(**args, &block)
@@ -130,6 +133,14 @@ describe HexaPDF::Layout::Box do
130
133
  assert_equal(100, box.height)
131
134
  end
132
135
 
136
+ it "use the frame's width and its remaining height for position=:flow boxes" do
137
+ box = create_box(style: {position: :flow})
138
+ box.define_singleton_method(:supports_position_flow?) { true }
139
+ assert(box.fit(100, 100, @frame).success?)
140
+ assert_equal(150, box.width)
141
+ assert_equal(60, box.height)
142
+ end
143
+
133
144
  it "uses float comparison" do
134
145
  box = create_box(width: 50.0000002, height: 49.9999996)
135
146
  assert(box.fit(50, 50, @frame).success?)
@@ -423,6 +423,15 @@ describe HexaPDF::Layout::Frame do
423
423
  box = HexaPDF::Layout::Box.create
424
424
  refute(@frame.fit(box).success?)
425
425
  end
426
+
427
+ it "doesn't do post-fitting tasks if fitting is a failure" do
428
+ box = HexaPDF::Layout::Box.create(width: 400)
429
+ result = @frame.fit(box)
430
+ assert(result.failure?)
431
+ assert_nil(result.x)
432
+ assert_nil(result.y)
433
+ assert_nil(result.mask)
434
+ end
426
435
  end
427
436
 
428
437
  describe "split" do
@@ -134,13 +134,13 @@ describe HexaPDF::Layout::ListBox do
134
134
 
135
135
  describe "split" do
136
136
  it "splits before a list item if no part of it will fit" do
137
- box = create_box(children: @text_boxes[0, 2])
138
- assert(box.fit(100, 20, @frame).overflow?)
137
+ box = create_box(children: @text_boxes[0, 3])
138
+ assert(box.fit(100, 22, @frame).overflow?)
139
139
  box_a, box_b = box.split
140
140
  assert_same(box, box_a)
141
141
  assert_equal(:show_first_marker, box_b.split_box?)
142
142
  assert_equal(1, box_a.instance_variable_get(:@results)[0].box_fitter.fit_results.size)
143
- assert_equal(1, box_b.children.size)
143
+ assert_equal(2, box_b.children.size)
144
144
  assert_equal(2, box_b.start_number)
145
145
  end
146
146
 
@@ -116,7 +116,14 @@ describe HexaPDF::Layout::TableBox::Cell do
116
116
  assert_equal(12, cell.preferred_height)
117
117
  end
118
118
 
119
- it "doesn't fit anything if the available width or height are too small" do
119
+ it "doesn't fit children that are too big" do
120
+ cell = create_cell(children: HexaPDF::Layout::Box.create(width: 300, height: 20))
121
+ assert(cell.fit(100, 100, @frame).failure?)
122
+ cell = create_cell(children: [HexaPDF::Layout::Box.create(width: 300, height: 20)])
123
+ assert(cell.fit(100, 100, @frame).failure?)
124
+ end
125
+
126
+ it "doesn't fit anything if the available width or height are too small even if there are no children" do
120
127
  cell = create_cell(children: nil)
121
128
  assert(cell.fit(10, 100, @frame).failure?)
122
129
  assert(cell.fit(100, 10, @frame).failure?)
@@ -42,31 +42,32 @@ describe HexaPDF::Layout::TextBox do
42
42
  end
43
43
 
44
44
  it "respects the set width and height" do
45
- box = create_box([@inline_box], width: 40, height: 50, style: {padding: 10})
45
+ box = create_box([@inline_box] * 5, width: 44, height: 50,
46
+ style: {padding: 10, text_align: :right, text_valign: :bottom})
46
47
  assert(box.fit(100, 100, @frame).success?)
47
- assert_equal(40, box.width)
48
+ assert_equal(44, box.width)
48
49
  assert_equal(50, box.height)
49
- assert_equal([10], box.instance_variable_get(:@result).lines.map(&:width))
50
+ assert_equal([20, 20, 10], box.instance_variable_get(:@result).lines.map(&:width))
50
51
  end
51
52
 
52
- it "fits into the frame's outline" do
53
- @frame.remove_area(Geom2D::Rectangle(0, 80, 20, 20))
54
- @frame.remove_area(Geom2D::Rectangle(80, 70, 20, 20))
55
- box = create_box([@inline_box] * 20, style: {position: :flow})
56
- assert(box.fit(100, 100, @frame).success?)
57
- assert_equal(100, box.width)
58
- assert_equal(30, box.height)
59
- end
53
+ describe "style option last_line_gap" do
54
+ it "is taken into account" do
55
+ box = create_box([@inline_box] * 5, style: {last_line_gap: true, line_spacing: :double})
56
+ assert(box.fit(100, 100, @frame).success?)
57
+ assert_equal(50, box.width)
58
+ assert_equal(20, box.height)
59
+ end
60
60
 
61
- it "takes the style option last_line_gap into account" do
62
- box = create_box([@inline_box] * 5, style: {last_line_gap: true, line_spacing: :double})
63
- assert(box.fit(100, 100, @frame).success?)
64
- assert_equal(50, box.width)
65
- assert_equal(20, box.height)
61
+ it "will have no effect for fixed-height boxes" do
62
+ box = create_box([@inline_box] * 5, height: 40, style: {last_line_gap: true, line_spacing: :double})
63
+ assert(box.fit(100, 100, @frame).success?)
64
+ assert_equal(50, box.width)
65
+ assert_equal(40, box.height)
66
+ end
66
67
  end
67
68
 
68
- it "uses the whole available width when aligning to the center or right" do
69
- [:center, :right].each do |align|
69
+ it "uses the whole available width when aligning to the center, right or justified" do
70
+ [:center, :right, :justify].each do |align|
70
71
  box = create_box([@inline_box], style: {text_align: align})
71
72
  assert(box.fit(100, 100, @frame).success?)
72
73
  assert_equal(100, box.width)
@@ -103,6 +104,33 @@ describe HexaPDF::Layout::TextBox do
103
104
  assert(box.fit(100, 100, @frame).success?)
104
105
  end
105
106
 
107
+ describe "position :flow" do
108
+ it "fits into the frame's outline" do
109
+ @frame.remove_area(Geom2D::Rectangle(0, 80, 20, 20))
110
+ @frame.remove_area(Geom2D::Rectangle(80, 70, 20, 20))
111
+ box = create_box([@inline_box] * 20, style: {position: :flow})
112
+ assert(box.fit(100, 100, @frame).success?)
113
+ assert_equal(100, box.width)
114
+ assert_equal(30, box.height)
115
+ end
116
+
117
+ it "respects a set initial height" do
118
+ box = create_box([@inline_box] * 20, height: 13, style: {position: :flow})
119
+ assert(box.fit(100, 100, @frame).overflow?)
120
+ assert_equal(100, box.width)
121
+ assert_equal(13, box.height)
122
+ end
123
+
124
+ it "respects top/bottom padding/border" do
125
+ @frame.remove_area(Geom2D::Rectangle(0, 80, 20, 20))
126
+ box = create_box([@inline_box] * 20, style: {position: :flow, padding: 10, border: {width: 2}})
127
+ assert(box.fit(100, 100, @frame).success?)
128
+ assert_equal(124, box.width)
129
+ assert_equal(54, box.height)
130
+ assert_equal([80, 100, 20], box.instance_variable_get(:@result).lines.map(&:width))
131
+ end
132
+ end
133
+
106
134
  it "fails if no item of the text box fits due to the width" do
107
135
  box = create_box([@inline_box])
108
136
  assert(box.fit(5, 20, @frame).failure?)
@@ -167,12 +195,12 @@ describe HexaPDF::Layout::TextBox do
167
195
  @frame.remove_area(Geom2D::Rectangle(0, 0, 40, 100))
168
196
  box = create_box([@inline_box], style: {position: :flow, border: {width: 1}})
169
197
  box.fit(60, 100, @frame)
170
- box.draw(@canvas, 0, 90)
198
+ box.draw(@canvas, 0, 88)
171
199
  assert_operators(@canvas.contents, [[:save_graphics_state],
172
- [:append_rectangle, [40, 90, 10, 10]],
200
+ [:append_rectangle, [40, 88, 12, 12]],
173
201
  [:clip_path_non_zero],
174
202
  [:end_path],
175
- [:append_rectangle, [40.5, 90.5, 9.0, 9.0]],
203
+ [:append_rectangle, [40.5, 88.5, 11.0, 11.0]],
176
204
  [:stroke_path],
177
205
  [:restore_graphics_state],
178
206
  [:save_graphics_state],
@@ -119,6 +119,13 @@ describe HexaPDF::Composer do
119
119
  end
120
120
  end
121
121
 
122
+ describe "styles" do
123
+ it "delegates to layout.styles" do
124
+ @composer.styles(base: {font_size: 30}, other: {font_size: 40})
125
+ assert_equal([:base, :other], @composer.document.layout.styles.keys)
126
+ end
127
+ end
128
+
122
129
  describe "page_style" do
123
130
  it "returns the page style if no argument or block is given" do
124
131
  page_style = @composer.page_style(:default)
@@ -225,8 +232,9 @@ describe HexaPDF::Composer do
225
232
  first_page_contents = @composer.canvas.contents
226
233
  @composer.draw_box(create_box(height: 400))
227
234
 
228
- box = create_box(height: 400)
229
- box.define_singleton_method(:split) do |*|
235
+ box = create_box
236
+ box.define_singleton_method(:fit_content) {|*| fit_result.overflow! }
237
+ box.define_singleton_method(:split_content) do |*|
230
238
  [box, HexaPDF::Layout::Box.new(height: 100) {}]
231
239
  end
232
240
  @composer.draw_box(box)
@@ -235,7 +243,7 @@ describe HexaPDF::Composer do
235
243
  [:concatenate_matrix, [1, 0, 0, 1, 36, 405.889764]],
236
244
  [:restore_graphics_state],
237
245
  [:save_graphics_state],
238
- [:concatenate_matrix, [1, 0, 0, 1, 36, 5.889764]],
246
+ [:concatenate_matrix, [1, 0, 0, 1, 36, 36]],
239
247
  [:restore_graphics_state]])
240
248
  assert_operators(@composer.canvas.contents,
241
249
  [[:save_graphics_state],
@@ -273,9 +281,10 @@ describe HexaPDF::Composer do
273
281
  box = create_box(height: 400)
274
282
  assert_same(box, @composer.draw_box(box))
275
283
 
276
- box = create_box(height: 400)
277
284
  split_box = create_box(height: 100)
278
- box.define_singleton_method(:split) {|*| [box, split_box] }
285
+ box = create_box
286
+ box.define_singleton_method(:fit_content) {|*| fit_result.overflow! }
287
+ box.define_singleton_method(:split_content) {|*| [box, split_box] }
279
288
  assert_same(split_box, @composer.draw_box(box))
280
289
  end
281
290
 
@@ -89,6 +89,7 @@ describe HexaPDF::Serializer do
89
89
  assert_serialized('/ ', :"")
90
90
  assert_serialized('/H#c3#b6#c3#9fgang', :Hößgang)
91
91
  assert_serialized('/H#e8lp', "H\xE8lp".force_encoding('BINARY').intern)
92
+ assert_serialized('/#00#09#0a#0c#0d#20', :"\x00\t\n\f\r ")
92
93
  end
93
94
 
94
95
  it "serializes arrays" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.44.0
4
+ version: 0.45.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-05 00:00:00.000000000 Z
11
+ date: 2024-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse