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 +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
|