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 +4 -4
- data/CHANGELOG.md +20 -0
- data/lib/hexapdf/configuration.rb +1 -0
- data/lib/hexapdf/document/layout.rb +15 -6
- data/lib/hexapdf/layout/box.rb +104 -29
- data/lib/hexapdf/layout/container_box.rb +159 -0
- data/lib/hexapdf/layout/frame.rb +7 -4
- data/lib/hexapdf/layout/list_box.rb +2 -1
- data/lib/hexapdf/layout.rb +1 -0
- data/lib/hexapdf/type/font_simple.rb +2 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/document/test_layout.rb +6 -0
- data/test/hexapdf/layout/test_box.rb +39 -13
- data/test/hexapdf/layout/test_container_box.rb +84 -0
- data/test/hexapdf/layout/test_frame.rb +3 -2
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/test_font_simple.rb +9 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7cec7494ffe5e4f8031e5a05f2a31da13741879ff138513f737048880702389
|
|
4
|
+
data.tar.gz: 238a71920dcd9bde03497a22f0583bf72f11d358176f28217214f86ca835764d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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+.
|
data/lib/hexapdf/layout/box.rb
CHANGED
|
@@ -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 #
|
|
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
|
-
#
|
|
72
|
-
#
|
|
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
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
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
|
-
#
|
|
80
|
-
#
|
|
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
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
|
193
|
-
# if they were initially set to 0. Otherwise the specified dimensions are
|
|
194
|
-
|
|
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
|
-
#
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
data/lib/hexapdf/layout/frame.rb
CHANGED
|
@@ -128,17 +128,20 @@ module HexaPDF
|
|
|
128
128
|
@success
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
# Draws the #box onto the canvas at (#x
|
|
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.
|
|
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
|
-
|
|
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
|
data/lib/hexapdf/layout.rb
CHANGED
|
@@ -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)
|
|
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
|
data/lib/hexapdf/version.rb
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
29
|
+
1 0 0 1 10 15 cm
|
|
29
30
|
Q
|
|
30
31
|
CONTENTS
|
|
31
32
|
ocg = doc.optional_content.ocgs.first
|
data/test/hexapdf/test_writer.rb
CHANGED
|
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
|
|
|
40
40
|
219
|
|
41
41
|
%%EOF
|
|
42
42
|
3 0 obj
|
|
43
|
-
<</Producer(HexaPDF version 0.
|
|
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.
|
|
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.
|
|
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-
|
|
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
|