hexapdf 0.44.0 → 0.45.0

Sign up to get free protection for your applications and to get access to all the features.
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