hexapdf 0.35.1 → 0.36.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: 4a7769176f4b753c876ecfb95a6cd2a954249388a7da9e308fbd8c25ba071c9b
4
- data.tar.gz: f39b081ab1408fd08a3dff88f8aa3417283e98478b3e9fad60a692afb16e92c1
3
+ metadata.gz: b7cec7494ffe5e4f8031e5a05f2a31da13741879ff138513f737048880702389
4
+ data.tar.gz: 238a71920dcd9bde03497a22f0583bf72f11d358176f28217214f86ca835764d
5
5
  SHA512:
6
- metadata.gz: 21fcdbd57cdcf436edd1de50f0268d05573b92b70edd1eafeb00af0414eed3cbaa8f107ac1310419a29a113dc5a33ec991fa49a4fa0b47edea1bd2eb7afb0768
7
- data.tar.gz: 0c6732024ebd325a4216fe8e4a2fdc69d51365b873ec53c926a9a1e575730cdb6d5f45eea5cd211c1cb2a21afd16cb75f3a1782411a2971eed7afe2f61708dfd
6
+ metadata.gz: 7850251dba07dbae11c280bc87852c91aa72e166da7983c4b4a1508a4c76d99306eaacbc108dad8f0ba54246a2fe6648714cd8bfd5f6d9bddeb0ec67abab9867
7
+ data.tar.gz: dace9a43ef57a0d27d33218ef4dc6503a961edfc2c96c04c16ebeb0d8675e09373c21908d159c992c4d2125499bdd1818acdf1150c86106e23888552210f4fc8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 0.36.0 - 2024-01-20
2
+
3
+ ### Added
4
+
5
+ * [HexaPDF::Layout::ContainerBox] for grouping child boxes together
6
+
7
+ ### Changed
8
+
9
+ * [HexaPDF::Layout::Frame::FitResult#draw] to allow drawing at an offset
10
+ * [HexaPDF::Layout::Box#fit] to delegate the actual content fitting to the
11
+ `#fit_content` method
12
+ * [HexaPDF::Document::Layout#box] to allow using the block as drawing block for
13
+ the base box class
14
+
15
+ ### Fixed
16
+
17
+ * [HexaPDF::Type::FontSimple#to_utf8] to work in case the font's encoding cannot
18
+ be retrieved
19
+
20
+
1
21
  ## 0.35.1 - 2024-01-11
2
22
 
3
23
  ### Added
@@ -545,6 +545,7 @@ module HexaPDF
545
545
  column: 'HexaPDF::Layout::ColumnBox',
546
546
  list: 'HexaPDF::Layout::ListBox',
547
547
  table: 'HexaPDF::Layout::TableBox',
548
+ container: 'HexaPDF::Layout::ContainerBox',
548
549
  },
549
550
  'page.default_media_box' => :A4,
550
551
  'page.default_media_orientation' => :portrait,
@@ -238,10 +238,12 @@ module HexaPDF
238
238
  #
239
239
  # The +name+ argument refers to the registered name of the box class that is looked up in the
240
240
  # 'layout.boxes.map' configuration option. The +box_options+ are passed as-is to the
241
- # initialization method of that box class
241
+ # initialization method of that box class.
242
242
  #
243
243
  # If a block is provided, a ChildrenCollector is yielded and the collected children are passed
244
- # to the box initialization method via the :children keyword argument.
244
+ # to the box initialization method via the :children keyword argument. There is one exception
245
+ # to this rule in case +name+ is +base+: The provided block is passed to the initialization
246
+ # method of the base box class to function as drawing method.
245
247
  #
246
248
  # See #text_box for details on +width+, +height+ and +style+ (note that there is no
247
249
  # +style_properties+ argument).
@@ -252,12 +254,19 @@ module HexaPDF
252
254
  # layout.box(:column) do |column| # column box with one child
253
255
  # column.lorem_ipsum
254
256
  # end
255
- def box(name, width: 0, height: 0, style: nil, **box_options, &block)
256
- if block_given? && !box_options.key?(:children)
257
- box_options[:children] = ChildrenCollector.collect(self, &block)
257
+ # layout.box(width: 100) do |canvas, box|
258
+ # canvas.line(0, 0, box.content_width, box.content_height).stroke
259
+ # end
260
+ def box(name = :base, width: 0, height: 0, style: nil, **box_options, &block)
261
+ if block_given?
262
+ if name == :base
263
+ box_block = block
264
+ elsif !box_options.key?(:children)
265
+ box_options[:children] = ChildrenCollector.collect(self, &block)
266
+ end
258
267
  end
259
268
  box_class_for_name(name).new(width: width, height: height,
260
- style: retrieve_style(style), **box_options)
269
+ style: retrieve_style(style), **box_options, &box_block)
261
270
  end
262
271
 
263
272
  # Creates an array of HexaPDF::Layout::TextFragment objects for the given +text+.
@@ -60,28 +60,59 @@ module HexaPDF
60
60
  # instantiated from the common convenience method HexaPDF::Document::Layout#box. To use this
61
61
  # facility subclasses need to be registered with the configuration option 'layout.boxes.map'.
62
62
  #
63
- # The methods #fit, #supports_position_flow?, #split or #split_content, #empty?, and #draw or
64
- # #draw_content need to be customized according to the subclass's use case.
65
- #
66
- # #fit:: This method should return +true+ if fitting was successful. Additionally, the
67
- # @fit_successful instance variable needs to be set to the fit result as it is used in
68
- # #split.
63
+ # The methods #supports_position_flow?, #empty?, #fit or #fit_content, #split or #split_content,
64
+ # and #draw or #draw_content need to be customized according to the subclass's use case (also
65
+ # see the documentation of the methods besides the informatione below):
69
66
  #
70
67
  # #supports_position_flow?::
71
- # If the subclass supports the value :flow of the 'position' style property, this method
72
- # needs to be overridden to return +true+.
68
+ # If the subclass supports the value :flow of the 'position' style property, this method
69
+ # needs to be overridden to return +true+.
70
+ #
71
+ # #empty?::
72
+ # This method should return +true+ if the subclass won't draw anything when #draw is called.
73
+ #
74
+ # #fit::
75
+ # This method should return +true+ if fitting was successful. Additionally, the
76
+ # @fit_successful instance variable needs to be set to the fit result as it is used in
77
+ # #split.
78
+ #
79
+ # The default implementation provides code common to most use-cases and delegates the
80
+ # specifics to the #fit_content method which needs to return +true+ if fitting was
81
+ # successful.
82
+ #
83
+ # #split::
84
+ # This method splits the content so that the current region is used as good as possible. The
85
+ # default implementation should be fine for most use-cases, so only #split_content needs to
86
+ # be implemented. The method #create_split_box should be used for getting a basic cloned
87
+ # box.
88
+ #
89
+ # #draw::
90
+ # This method draws the content and the default implementation already handles things like
91
+ # drawing the border and background. So it should not be overridden. The box specific
92
+ # drawing commands should be implemented in the #draw_content method.
93
+ #
94
+ # This base class provides various private helper methods for use in the above methods:
95
+ #
96
+ # +reserved_width+, +reserved_height+::
97
+ # Returns the width respectively the height of the reserved space inside the box that is
98
+ # used for the border and padding.
99
+ #
100
+ # +reserved_width_left+, +reserved_width_right+, +reserved_height_top+,
101
+ # +reserved_height_bottom+::
102
+ # Returns the reserved space inside the box at the specified edge (left, right, top,
103
+ # bottom).
73
104
  #
74
- # #split:: This method splits the content so that the current region is used as good as
75
- # possible. The default implementation should be fine for most use-cases, so only
76
- # #split_content needs to be implemented. The method #create_split_box should be used
77
- # for getting a basic cloned box.
105
+ # +update_content_width+, +update_content_height+::
106
+ # Takes a block that should return the content width respectively height and sets the box's
107
+ # width respectively height accordingly.
78
108
  #
79
- # #empty?:: This method should return +true+ if the subclass won't draw anything when #draw is
80
- # called.
109
+ # +create_split_box+::
110
+ # Creates a new box based on this one and resets the internal data back to their original
111
+ # values.
81
112
  #
82
- # #draw:: This method draws the content and the default implementation already handles things
83
- # like drawing the border and background. Therefore it's best to implement #draw_content
84
- # which should just draw the content.
113
+ # The keyword argument +split_box_value+ (defaults to +true+) is used to set the
114
+ # +@split_box+ variable to make the new box aware that it is a split box. This can be set to
115
+ # any other truthy value to convey more meaning.
85
116
  class Box
86
117
 
87
118
  include HexaPDF::Utils
@@ -162,7 +193,8 @@ module HexaPDF
162
193
  @split_box = false
163
194
  end
164
195
 
165
- # Returns +true+ if this is a split box, i.e. the rest of another box after it was split.
196
+ # Returns the set truthy value if this is a split box, i.e. the rest of another box after it
197
+ # was split.
166
198
  def split_box?
167
199
  @split_box
168
200
  end
@@ -184,18 +216,34 @@ module HexaPDF
184
216
  height < 0 ? 0 : height
185
217
  end
186
218
 
187
- # Fits the box into the Frame and returns +true+ if fitting was successful.
219
+ # Fits the box into the *frame* and returns +true+ if fitting was successful.
188
220
  #
189
221
  # The arguments +available_width+ and +available_height+ are the width and height of the
190
- # current region of the frame. The frame itself is provided as third argument.
222
+ # current region of the frame, adjusted for this box. The frame itself is provided as third
223
+ # argument.
191
224
  #
192
- # The default implementation uses the available width and height for the box width and height
193
- # if they were initially set to 0. Otherwise the specified dimensions are used.
194
- def fit(available_width, available_height, _frame)
225
+ # The default implementation uses the given available width and height for the box width and
226
+ # height if they were initially set to 0. Otherwise the intially specified dimensions are
227
+ # used. Then the #fit_content method is called which allows sub-classes to fit their content.
228
+ #
229
+ # The following variables are set that may later be used during splitting or drawing:
230
+ #
231
+ # * (@fit_x, @fit_y): The lower-left corner of the content box where fitting was done. Can be
232
+ # used to adjust the drawing position in #draw/#draw_content if necessary.
233
+ # * @fit_successful: +true+ if fitting was successful.
234
+ def fit(available_width, available_height, frame)
195
235
  @width = (@initial_width > 0 ? @initial_width : available_width)
196
236
  @height = (@initial_height > 0 ? @initial_height : available_height)
197
237
  @fit_successful = (float_compare(@width, available_width) <= 0 &&
198
238
  float_compare(@height, available_height) <= 0)
239
+ return unless @fit_successful
240
+
241
+ @fit_successful = fit_content(available_width, available_height, frame)
242
+
243
+ @fit_x = frame.x + reserved_width_left
244
+ @fit_y = frame.y - @height + reserved_height_bottom
245
+
246
+ @fit_successful
199
247
  end
200
248
 
201
249
  # Tries to split the box into two, the first of which needs to fit into the current region of
@@ -237,6 +285,8 @@ module HexaPDF
237
285
  # arguments. Subclasses can specify an on-demand drawing method by setting the +@draw_block+
238
286
  # instance variable to +nil+ or a valid block. This is useful to avoid unnecessary set-up
239
287
  # operations when the block does nothing.
288
+ #
289
+ # Alternatively, if a #draw_content method is defined, this method is called.
240
290
  def draw(canvas, x, y)
241
291
  if (oc = properties['optional_content'])
242
292
  canvas.optional_content(oc)
@@ -316,12 +366,26 @@ module HexaPDF
316
366
  result
317
367
  end
318
368
 
319
- # Draws the content of the box at position [x, y] which is the bottom-left corner of the
320
- # content box.
321
- def draw_content(canvas, x, y)
322
- if @draw_block
323
- canvas.translate(x, y) { @draw_block.call(canvas, self) }
324
- end
369
+ # Updates the width of the box using the content width returned by the block.
370
+ def update_content_width
371
+ return if @initial_width > 0
372
+ @width = yield + reserved_width
373
+ end
374
+
375
+ # Updates the height of the box using the content height returned by the block.
376
+ def update_content_height
377
+ return if @initial_height > 0
378
+ @height = yield + reserved_height
379
+ end
380
+
381
+ # Fits the content of the box and returns whether fitting was successful.
382
+ #
383
+ # This is just a stub implementation that returns +true+. Subclasses should override it to
384
+ # provide the box specific behaviour.
385
+ #
386
+ # See #fit for details.
387
+ def fit_content(available_width, available_height, frame)
388
+ true
325
389
  end
326
390
 
327
391
  # Splits the content of the box.
@@ -335,6 +399,17 @@ module HexaPDF
335
399
  [nil, self]
336
400
  end
337
401
 
402
+ # Draws the content of the box at position [x, y] which is the bottom-left corner of the
403
+ # content box.
404
+ #
405
+ # This implementation uses the drawing block provided on initialization, if set, to draw the
406
+ # contents. Subclasses should override it to provide box specific behaviour.
407
+ def draw_content(canvas, x, y)
408
+ if @draw_block
409
+ canvas.translate(x, y) { @draw_block.call(canvas, self) }
410
+ end
411
+ end
412
+
338
413
  # Creates a new box based on this one and resets the data back to their original values.
339
414
  #
340
415
  # The variable +@split_box+ is set to +split_box_value+ (defaults to +true+) to make the new
@@ -0,0 +1,159 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2023 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+ require 'hexapdf/layout/box'
37
+ require 'hexapdf/layout/box_fitter'
38
+ require 'hexapdf/layout/frame'
39
+
40
+ module HexaPDF
41
+ module Layout
42
+
43
+ # This is a simple container box for laying out a number of boxes together. It is registered
44
+ # under the :container name.
45
+ #
46
+ # The box does not support the value :flow for the style property position, so the child boxes
47
+ # are laid out in the current region only. Since the boxes should be laid out together, if any
48
+ # box doesn't fit, the whole container doesn't fit. Splitting the container is also not possible
49
+ # for the same reason.
50
+ #
51
+ # By default the child boxes are laid out from top to bottom by default. By appropriately
52
+ # setting the style properties 'mask_mode', 'align' and 'valign', it is possible to lay out the
53
+ # children bottom to top, left to right, or right to left:
54
+ #
55
+ # * The standard top to bottom layout:
56
+ #
57
+ # #>pdf-composer100
58
+ # composer.container do |container|
59
+ # container.box(:base, height: 20, style: {background_color: "hp-blue-dark"})
60
+ # container.box(:base, height: 20, style: {background_color: "hp-blue"})
61
+ # container.box(:base, height: 20, style: {background_color: "hp-blue-light"})
62
+ # end
63
+ #
64
+ # * The bottom to top layout (using valign = :bottom to fill up from the bottom and mask_mode =
65
+ # :fill_horizontal to only remove the area to the left and right of the box):
66
+ #
67
+ # #>pdf-composer100
68
+ # composer.container do |container|
69
+ # container.box(:base, height: 20, style: {background_color: "hp-blue-dark",
70
+ # mask_mode: :fill_horizontal, valign: :bottom})
71
+ # container.box(:base, height: 20, style: {background_color: "hp-blue",
72
+ # mask_mode: :fill_horizontal, valign: :bottom})
73
+ # container.box(:base, height: 20, style: {background_color: "hp-blue-light",
74
+ # mask_mode: :fill_horizontal, valign: :bottom})
75
+ # end
76
+ #
77
+ # * The left to right layout (using mask_mode = :fill_vertical to fill the area to the top and
78
+ # bottom of the box):
79
+ #
80
+ # #>pdf-composer100
81
+ # composer.container do |container|
82
+ # container.box(:base, width: 20, style: {background_color: "hp-blue-dark",
83
+ # mask_mode: :fill_vertical})
84
+ # container.box(:base, width: 20, style: {background_color: "hp-blue",
85
+ # mask_mode: :fill_vertical})
86
+ # container.box(:base, width: 20, style: {background_color: "hp-blue-light",
87
+ # mask_mode: :fill_vertical})
88
+ # end
89
+ #
90
+ # * The right to left layout (using align = :right to fill up from the right and mask_mode =
91
+ # :fill_vertical to fill the area to the top and bottom of the box):
92
+ #
93
+ # #>pdf-composer100
94
+ # composer.container do |container|
95
+ # container.box(:base, width: 20, style: {background_color: "hp-blue-dark",
96
+ # mask_mode: :fill_vertical, align: :right})
97
+ # container.box(:base, width: 20, style: {background_color: "hp-blue",
98
+ # mask_mode: :fill_vertical, align: :right})
99
+ # container.box(:base, width: 20, style: {background_color: "hp-blue-light",
100
+ # mask_mode: :fill_vertical, align: :right})
101
+ # end
102
+ class ContainerBox < Box
103
+
104
+ # The child boxes of this ContainerBox. They need to be finalized before #fit is called.
105
+ attr_reader :children
106
+
107
+ # Creates a new container box, optionally accepting an array of child boxes.
108
+ #
109
+ # Example:
110
+ #
111
+ # #>pdf-composer100
112
+ # composer.text("A paragraph here")
113
+ # composer.container(height: 40, style: {border: {width: 1}, padding: 5,
114
+ # align: :center}) do |container|
115
+ # container.text("Some", mask_mode: :fill_vertical)
116
+ # container.text("text", mask_mode: :fill_vertical, valign: :center)
117
+ # container.text("here", mask_mode: :fill_vertical, valign: :bottom)
118
+ # end
119
+ # composer.text("Another paragraph")
120
+ def initialize(children: [], **kwargs)
121
+ super(**kwargs)
122
+ @children = children
123
+ end
124
+
125
+ # Returns +true+ if no box was fitted into the container.
126
+ def empty?
127
+ super && (!@box_fitter || @box_fitter.fit_results.empty?)
128
+ end
129
+
130
+ private
131
+
132
+ # Fits the children into the container.
133
+ def fit_content(available_width, available_height, frame)
134
+ my_frame = Frame.new(frame.x + reserved_width_left, frame.y - @height + reserved_height_bottom,
135
+ content_width, content_height, context: frame.context)
136
+ @box_fitter = BoxFitter.new([my_frame])
137
+ children.each {|box| @box_fitter.fit(box) }
138
+
139
+ if @box_fitter.fit_successful?
140
+ update_content_width do
141
+ result = @box_fitter.fit_results.max_by {|result| result.mask.x + result.mask.width }
142
+ children.size > 0 ? result.mask.x + result.mask.width - my_frame.left : 0
143
+ end
144
+ update_content_height { @box_fitter.content_heights.max }
145
+ true
146
+ end
147
+ end
148
+
149
+ # Draws the image onto the canvas at position [x, y].
150
+ def draw_content(canvas, x, y)
151
+ dx = x - @fit_x
152
+ dy = y - @fit_y
153
+ @box_fitter.fit_results.each {|result| result.draw(canvas, dx: dx, dy: dy) }
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+ end
@@ -128,17 +128,20 @@ module HexaPDF
128
128
  @success
129
129
  end
130
130
 
131
- # Draws the #box onto the canvas at (#x, #y).
131
+ # Draws the #box onto the canvas at (#x + *dx*, #y + *dy*).
132
+ #
133
+ # The relative offset (dx, dy) is useful when rendering results that were accumulated and
134
+ # then need to be moved because the container holding them changes its position.
132
135
  #
133
136
  # The configuration option "debug" can be used to add visual debug output with respect to
134
137
  # box placement.
135
- def draw(canvas)
138
+ def draw(canvas, dx: 0, dy: 0)
136
139
  doc = canvas.context.document
137
140
  if doc.config['debug']
138
141
  name = "#{box.class} (#{x.to_i},#{y.to_i}-#{box.width.to_i}x#{box.height.to_i})"
139
142
  ocg = doc.optional_content.ocg(name)
140
143
  canvas.optional_content(ocg) do
141
- canvas.save_graphics_state do
144
+ canvas.translate(dx, dy) do
142
145
  canvas.fill_color("green").stroke_color("darkgreen").
143
146
  opacity(fill_alpha: 0.1, stroke_alpha: 0.2).
144
147
  draw(:geom2d, object: mask, path_only: true).fill_stroke
@@ -146,7 +149,7 @@ module HexaPDF
146
149
  end
147
150
  doc.optional_content.default_configuration.add_ocg_to_ui(ocg, path: 'Debug')
148
151
  end
149
- box.draw(canvas, x, y)
152
+ box.draw(canvas, x + dx, y + dy)
150
153
  end
151
154
 
152
155
  end
@@ -232,7 +232,8 @@ module HexaPDF
232
232
 
233
233
  if index != 0 || !split_box? || @split_box == :show_first_marker
234
234
  box = item_marker_box(frame.document, index)
235
- break unless box.fit(content_indentation, height, nil)
235
+ marker_frame = Frame.new(0, 0, content_indentation, height, context: frame.context)
236
+ break unless box.fit(content_indentation, height, marker_frame)
236
237
  item_result.marker = box
237
238
  item_result.marker_pos_x = item_frame.x - content_indentation
238
239
  item_result.height = box.height
@@ -58,6 +58,7 @@ module HexaPDF
58
58
  autoload(:ListBox, 'hexapdf/layout/list_box')
59
59
  autoload(:PageStyle, 'hexapdf/layout/page_style')
60
60
  autoload(:TableBox, 'hexapdf/layout/table_box')
61
+ autoload(:ContainerBox, 'hexapdf/layout/container_box')
61
62
 
62
63
  end
63
64
 
@@ -95,7 +95,8 @@ module HexaPDF
95
95
  # Returns the UTF-8 string for the given character code, or calls the configuration option
96
96
  # 'font.on_missing_unicode_mapping' if no mapping was found.
97
97
  def to_utf8(code)
98
- to_unicode_cmap&.to_unicode(code) || encoding.unicode(code) || missing_unicode_mapping(code)
98
+ to_unicode_cmap&.to_unicode(code) || (encoding.unicode(code) rescue nil) ||
99
+ missing_unicode_mapping(code)
99
100
  end
100
101
 
101
102
  # Returns the unscaled width of the given code point in glyph units, or 0 if the width for
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.35.1'
40
+ VERSION = '0.36.0'
41
41
 
42
42
  end
@@ -175,6 +175,12 @@ describe HexaPDF::Document::Layout do
175
175
  assert_equal(2, box.children.size)
176
176
  end
177
177
 
178
+ it "uses the provided block as drawing block for the base box class if name=:base" do
179
+ block = proc {}
180
+ box = @layout.box(width: 100, &block)
181
+ assert_equal(block, box.instance_variable_get(:@draw_block))
182
+ end
183
+
178
184
  it "fails if the name is not registered" do
179
185
  assert_raises(HexaPDF::Error) { @layout.box(:unknown) }
180
186
  end
@@ -5,6 +5,12 @@ require 'hexapdf/document'
5
5
  require 'hexapdf/layout/box'
6
6
 
7
7
  describe HexaPDF::Layout::Box do
8
+ before do
9
+ @frame = Object.new
10
+ def @frame.x; 0; end
11
+ def @frame.y; 100; end
12
+ end
13
+
8
14
  def create_box(**args, &block)
9
15
  HexaPDF::Layout::Box.new(**args, &block)
10
16
  end
@@ -70,15 +76,13 @@ describe HexaPDF::Layout::Box do
70
76
  end
71
77
 
72
78
  describe "fit" do
73
- before do
74
- @frame = Object.new
75
- end
76
-
77
79
  it "fits a fixed sized box" do
78
- box = create_box(width: 50, height: 50)
80
+ box = create_box(width: 50, height: 50, style: {padding: 5})
79
81
  assert(box.fit(100, 100, @frame))
80
82
  assert_equal(50, box.width)
81
83
  assert_equal(50, box.height)
84
+ assert_equal(5, box.instance_variable_get(:@fit_x))
85
+ assert_equal(55, box.instance_variable_get(:@fit_y))
82
86
  end
83
87
 
84
88
  it "uses the maximum available width" do
@@ -106,49 +110,71 @@ describe HexaPDF::Layout::Box do
106
110
  box = create_box(width: 101)
107
111
  refute(box.fit(100, 100, @frame))
108
112
  end
113
+
114
+ it "can use the #content_width/#content_height helper methods" do
115
+ box = create_box
116
+ box.define_singleton_method(:fit_content) do |aw, ah, frame|
117
+ update_content_width { 10 }
118
+ update_content_height { 20 }
119
+ true
120
+ end
121
+ assert(box.fit(100, 100, @frame))
122
+ assert_equal(10, box.width)
123
+ assert_equal(20, box.height)
124
+
125
+ box = create_box(width: 30, height: 50)
126
+ box.define_singleton_method(:fit_content) do |aw, ah, frame|
127
+ update_content_width { 10 }
128
+ update_content_height { 20 }
129
+ true
130
+ end
131
+ assert(box.fit(100, 100, @frame))
132
+ assert_equal(30, box.width)
133
+ assert_equal(50, box.height)
134
+ end
109
135
  end
110
136
 
111
137
  describe "split" do
112
138
  before do
113
139
  @box = create_box(width: 100, height: 100)
114
- @box.fit(100, 100, nil)
140
+ @box.fit(100, 100, @frame)
115
141
  end
116
142
 
117
143
  it "doesn't need to be split if it completely fits" do
118
- assert_equal([@box, nil], @box.split(100, 100, nil))
144
+ assert_equal([@box, nil], @box.split(100, 100, @frame))
119
145
  end
120
146
 
121
147
  it "can't be split if it doesn't (completely) fit and its width is greater than the available width" do
122
148
  @box.fit(90, 100, nil)
123
- assert_equal([nil, @box], @box.split(50, 150, nil))
149
+ assert_equal([nil, @box], @box.split(50, 150, @frame))
124
150
  end
125
151
 
126
152
  it "can't be split if it doesn't (completely) fit and its height is greater than the available height" do
127
153
  @box.fit(90, 100, nil)
128
- assert_equal([nil, @box], @box.split(150, 50, nil))
154
+ assert_equal([nil, @box], @box.split(150, 50, @frame))
129
155
  end
130
156
 
131
157
  it "can't be split if it doesn't (completely) fit and its content width is zero" do
132
158
  box = create_box(width: 0, height: 100)
133
- assert_equal([nil, box], box.split(150, 150, nil))
159
+ assert_equal([nil, box], box.split(150, 150, @frame))
134
160
  end
135
161
 
136
162
  it "can't be split if it doesn't (completely) fit and its content height is zero" do
137
163
  box = create_box(width: 100, height: 0)
138
- assert_equal([nil, box], box.split(150, 150, nil))
164
+ assert_equal([nil, box], box.split(150, 150, @frame))
139
165
  end
140
166
 
141
167
  it "can't be split if it doesn't (completely) fit as the default implementation " \
142
168
  "knows nothing about the content" do
143
169
  @box.style.position = :flow # make sure we would generally be splitable
144
170
  @box.fit(90, 100, nil)
145
- assert_equal([nil, @box], @box.split(150, 150, nil))
171
+ assert_equal([nil, @box], @box.split(150, 150, @frame))
146
172
  end
147
173
  end
148
174
 
149
175
  it "can create a cloned box for splitting" do
150
176
  box = create_box
151
- box.fit(100, 100, nil)
177
+ box.fit(100, 100, @frame)
152
178
  cloned_box = box.send(:create_split_box)
153
179
  assert(cloned_box.split_box?)
154
180
  refute(cloned_box.instance_variable_get(:@fit_successful))
@@ -0,0 +1,84 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/layout/container_box'
6
+
7
+ describe HexaPDF::Layout::ContainerBox do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @frame = HexaPDF::Layout::Frame.new(0, 0, 100, 100)
11
+ end
12
+
13
+ def create_box(children, **kwargs)
14
+ HexaPDF::Layout::ContainerBox.new(children: Array(children), **kwargs)
15
+ end
16
+
17
+ def check_box(box, width, height, fit_pos = nil)
18
+ assert(box.fit(@frame.available_width, @frame.available_height, @frame), "box didn't fit")
19
+ assert_equal(width, box.width, "box width")
20
+ assert_equal(height, box.height, "box height")
21
+ if fit_pos
22
+ box_fitter = box.instance_variable_get(:@box_fitter)
23
+ assert_equal(fit_pos.size, box_fitter.fit_results.size)
24
+ fit_pos.each_with_index do |(x, y), index|
25
+ assert_equal(x, box_fitter.fit_results[index].x, "result[#{index}].x")
26
+ assert_equal(y, box_fitter.fit_results[index].y, "result[#{index}].y")
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "empty?" do
32
+ it "is empty if nothing is fit yet" do
33
+ assert(create_box([]).empty?)
34
+ end
35
+
36
+ it "is empty if no box fits" do
37
+ box = create_box(@doc.layout.box(width: 110))
38
+ box.fit(@frame.available_width, @frame.available_height, @frame)
39
+ assert(box.empty?)
40
+ end
41
+
42
+ it "is not empty if at least one box fits" do
43
+ box = create_box(@doc.layout.box(width: 50, height: 20))
44
+ check_box(box, 100, 20)
45
+ refute(box.empty?)
46
+ end
47
+ end
48
+
49
+ describe "fit_content" do
50
+ it "fits the children according to their mask_mode, valign, and align style properties" do
51
+ box = create_box([@doc.layout.box(height: 20),
52
+ @doc.layout.box(height: 20, style: {valign: :bottom, mask_mode: :fill_horizontal}),
53
+ @doc.layout.box(width: 20, style: {align: :right, mask_mode: :fill_vertical})])
54
+ check_box(box, 100, 100, [[0, 80], [0, 0], [80, 20]])
55
+ end
56
+
57
+ it "respects the initially set width/height as well as border/padding" do
58
+ box = create_box(@doc.layout.box(height: 20), height: 50, width: 40,
59
+ style: {padding: 2, border: {width: 3}})
60
+ check_box(box, 40, 50, [[5, 75]])
61
+ end
62
+ end
63
+
64
+ describe "draw_content" do
65
+ before do
66
+ @canvas = @doc.pages.add.canvas
67
+ end
68
+
69
+ it "draws the result onto the canvas" do
70
+ child_box = @doc.layout.box(height: 20) do |canvas, b|
71
+ canvas.line(0, 0, b.content_width, b.content_height).end_path
72
+ end
73
+ box = create_box(child_box)
74
+ box.fit(100, 100, @frame)
75
+ box.draw(@canvas, 0, 50)
76
+ assert_operators(@canvas.contents, [[:save_graphics_state],
77
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 50]],
78
+ [:move_to, [0, 0]],
79
+ [:line_to, [100, 20]],
80
+ [:end_path],
81
+ [:restore_graphics_state]])
82
+ end
83
+ end
84
+ end
@@ -13,10 +13,11 @@ describe HexaPDF::Layout::Frame::FitResult do
13
13
  result = HexaPDF::Layout::Frame::FitResult.new(box)
14
14
  result.mask = Geom2D::Rectangle(0, 0, 20, 20)
15
15
  result.x = result.y = 0
16
- result.draw(canvas)
16
+ result.draw(canvas, dx: 10, dy: 15)
17
17
  assert_equal(<<~CONTENTS, canvas.contents)
18
18
  /OC /P1 BDC
19
19
  q
20
+ 1 0 0 1 10 15 cm
20
21
  0.0 0.501961 0.0 rg
21
22
  0.0 0.392157 0.0 RG
22
23
  /GS1 gs
@@ -25,7 +26,7 @@ describe HexaPDF::Layout::Frame::FitResult do
25
26
  Q
26
27
  EMC
27
28
  q
28
- 1 0 0 1 0 0 cm
29
+ 1 0 0 1 10 15 cm
29
30
  Q
30
31
  CONTENTS
31
32
  ocg = doc.optional_content.ocgs.first
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.35.1)>>
43
+ <</Producer(HexaPDF version 0.36.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.35.1)>>
75
+ <</Producer(HexaPDF version 0.36.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -116,9 +116,17 @@ describe HexaPDF::Type::FontSimple do
116
116
  assert_equal(" ", @font.to_utf8(32))
117
117
  end
118
118
 
119
+ it "swallows errors during retrieving the font's encoding" do
120
+ @font.delete(:ToUnicode)
121
+ @font.delete(:Encoding)
122
+ err = assert_raises(HexaPDF::Error) { @font.to_utf8(32) }
123
+ assert_match(/No Unicode mapping/, err.message)
124
+ end
125
+
119
126
  it "calls the configured proc if no correct mapping could be found" do
120
127
  @font.delete(:ToUnicode)
121
- assert_raises(HexaPDF::Error) { @font.to_utf8(0) }
128
+ err = assert_raises(HexaPDF::Error) { @font.to_utf8(0) }
129
+ assert_match(/No Unicode mapping/, err.message)
122
130
  end
123
131
  end
124
132
 
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.35.1
4
+ version: 0.36.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-01-10 00:00:00.000000000 Z
11
+ date: 2024-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -434,6 +434,7 @@ files:
434
434
  - lib/hexapdf/layout/box.rb
435
435
  - lib/hexapdf/layout/box_fitter.rb
436
436
  - lib/hexapdf/layout/column_box.rb
437
+ - lib/hexapdf/layout/container_box.rb
437
438
  - lib/hexapdf/layout/frame.rb
438
439
  - lib/hexapdf/layout/image_box.rb
439
440
  - lib/hexapdf/layout/inline_box.rb
@@ -695,6 +696,7 @@ files:
695
696
  - test/hexapdf/layout/test_box.rb
696
697
  - test/hexapdf/layout/test_box_fitter.rb
697
698
  - test/hexapdf/layout/test_column_box.rb
699
+ - test/hexapdf/layout/test_container_box.rb
698
700
  - test/hexapdf/layout/test_frame.rb
699
701
  - test/hexapdf/layout/test_image_box.rb
700
702
  - test/hexapdf/layout/test_inline_box.rb