hexapdf 0.35.1 → 0.36.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: 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