hexapdf 0.21.1 → 0.22.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 +21 -0
- data/lib/hexapdf/cli/form.rb +4 -0
- data/lib/hexapdf/cli/inspect.rb +6 -2
- data/lib/hexapdf/dictionary_fields.rb +1 -1
- data/lib/hexapdf/type/acro_form/form.rb +11 -5
- data/lib/hexapdf/type/image.rb +47 -3
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/test_dictionary_fields.rb +1 -1
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_form.rb +2 -1
- data/test/hexapdf/type/test_image.rb +45 -9
- metadata +3 -5
- data/examples/020-column_box.rb +0 -57
- data/lib/hexapdf/layout/column_box.rb +0 -168
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1388a344a539c7273603549e014c635c4864af4de8665a260b15ac66d19ac461
|
4
|
+
data.tar.gz: 471e0f4933ac37348ac5ed217446031e742099de26773b25bb23c49fc8480a05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ab8c16c85475e68f12faea47f8dbf7e167ebdc864d11fb1151e107af1d3678b867ad0d7a6184155e1b55ed1469d3e7443f39184e0250974f5a7df9a920adb99
|
7
|
+
data.tar.gz: 43c80b555018068baf314d6ed7d6a03ae0a359f2e8be498341985ecb75879fd414d066d7c356b47fb896ec0e4aa74c2044f6a5e7965922de1136ddacf409765f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
## 0.22.0 - 2022-03-26
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- Support for writing images with an ICCBased color space
|
6
|
+
- Support for writing images with soft masks
|
7
|
+
|
8
|
+
### Changed
|
9
|
+
|
10
|
+
- CLI command `hexapdf form` to show a warning when working with a file
|
11
|
+
containing an XFA form
|
12
|
+
|
13
|
+
### Fixed
|
14
|
+
|
15
|
+
- [HexaPDF::Type::AcroForm::Form#field_by_name] to work correctly when field
|
16
|
+
name parts are UTF-16BE encoded
|
17
|
+
- `hexapdf inspect` command 'revision' to correctly detect the end of revisions
|
18
|
+
- [HexaPDF::DictionaryFields::StringConverter] to use correct method name
|
19
|
+
`HexaPDF::Document#config`
|
20
|
+
|
21
|
+
|
1
22
|
## 0.21.1 - 2022-03-12
|
2
23
|
|
3
24
|
### Fixed
|
data/lib/hexapdf/cli/form.rb
CHANGED
@@ -97,6 +97,10 @@ module HexaPDF
|
|
97
97
|
end
|
98
98
|
with_document(in_file, password: @password, out_file: out_file,
|
99
99
|
incremental: @incremental) do |doc|
|
100
|
+
if doc.acro_form[:XFA]
|
101
|
+
$stderr.puts "Warning: Unsupported XFA form detected, some things may not work correctly"
|
102
|
+
end
|
103
|
+
|
100
104
|
if !doc.acro_form
|
101
105
|
raise "This PDF doesn't contain an interactive form"
|
102
106
|
elsif out_file
|
data/lib/hexapdf/cli/inspect.rb
CHANGED
@@ -342,10 +342,14 @@ module HexaPDF
|
|
342
342
|
end_index = sig[:ByteRange][-2] + sig[:ByteRange][-1]
|
343
343
|
else
|
344
344
|
io.seek(startxrefs[index], IO::SEEK_SET)
|
345
|
+
buffer = ''.b
|
345
346
|
while io.pos < startxrefs[index + 1]
|
346
|
-
|
347
|
-
|
347
|
+
buffer << io.read(1_000)
|
348
|
+
if (buffer_index = buffer.index(/(?:\n|\r\n?)\s*%%EOF\s*(?:\n|\r\n?)/))
|
349
|
+
end_index = io.pos - buffer.size + buffer_index + $~[0].size
|
350
|
+
break
|
348
351
|
end
|
352
|
+
buffer = buffer[-20..-1]
|
349
353
|
end
|
350
354
|
end
|
351
355
|
yield(rev, index, rev.next_free_oid - 1, sig, end_index)
|
@@ -241,7 +241,7 @@ module HexaPDF
|
|
241
241
|
if str.valid_encoding?
|
242
242
|
str.encode!(Encoding::UTF_8)
|
243
243
|
else
|
244
|
-
document.
|
244
|
+
document.config['document.on_invalid_string'].call(str)
|
245
245
|
end
|
246
246
|
else
|
247
247
|
Utils::PDFDocEncoding.convert_to_utf8(str)
|
@@ -139,13 +139,19 @@ module HexaPDF
|
|
139
139
|
def field_by_name(name)
|
140
140
|
fields = root_fields
|
141
141
|
field = nil
|
142
|
+
|
142
143
|
name.split('.').each do |part|
|
143
|
-
field =
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
144
|
+
field = nil
|
145
|
+
fields&.each do |f|
|
146
|
+
f = document.wrap(f, type: :XXAcroFormField,
|
147
|
+
subtype: Field.inherited_value(f, :FT))
|
148
|
+
next unless f[:T] == part
|
149
|
+
field = f
|
150
|
+
fields = field[:Kids] unless field.terminal_field?
|
151
|
+
break
|
152
|
+
end
|
148
153
|
end
|
154
|
+
|
149
155
|
field
|
150
156
|
end
|
151
157
|
|
data/lib/hexapdf/type/image.rb
CHANGED
@@ -163,13 +163,23 @@ module HexaPDF
|
|
163
163
|
result.color_space = :cmyk
|
164
164
|
result.components = 4
|
165
165
|
result.writable = false if result.type == :png
|
166
|
+
when :ICCBased
|
167
|
+
result.color_space = :icc
|
168
|
+
result.components = self[:ColorSpace][1][:N]
|
169
|
+
result.writable = false if result.type == :png && result.components == 4
|
166
170
|
else
|
167
171
|
result.color_space = :other
|
168
172
|
result.components = -1
|
169
173
|
result.writable = false if result.type == :png
|
170
174
|
end
|
171
175
|
|
172
|
-
|
176
|
+
smask = self[:SMask]
|
177
|
+
if smask && (result.type != :png ||
|
178
|
+
!(result.bits_per_component == 8 || result.bits_per_component == 16) ||
|
179
|
+
result.bits_per_component != smask[:BitsPerComponent] ||
|
180
|
+
result.width != smask[:Width] || result.height != smask[:Height])
|
181
|
+
result.writable = false
|
182
|
+
end
|
173
183
|
|
174
184
|
result
|
175
185
|
end
|
@@ -224,10 +234,18 @@ module HexaPDF
|
|
224
234
|
ImageLoader::PNG::INDEXED
|
225
235
|
elsif info.color_space == :rgb
|
226
236
|
ImageLoader::PNG::TRUECOLOR
|
237
|
+
elsif info.color_space == :icc
|
238
|
+
info.components == 3 ? ImageLoader::PNG::TRUECOLOR : ImageLoader::PNG::GREYSCALE
|
227
239
|
else
|
228
240
|
ImageLoader::PNG::GREYSCALE
|
229
241
|
end
|
230
242
|
|
243
|
+
if self[:SMask] && color_type != ImageLoader::PNG::INDEXED
|
244
|
+
color_type += 4 # change it to TrueColor/Greyscale with Alpha
|
245
|
+
end
|
246
|
+
|
247
|
+
flate_decode = config.constantize('filter.map', :FlateDecode)
|
248
|
+
|
231
249
|
io << png_chunk('IHDR', [info.width, info.height, info.bits_per_component,
|
232
250
|
color_type, 0, 0, 0].pack('N2C5'))
|
233
251
|
|
@@ -239,6 +257,12 @@ module HexaPDF
|
|
239
257
|
png_chunk('cHRM', [31270, 32900, 64000, 33000, 30000, 60000, 15000, 6000].pack('N8'))
|
240
258
|
end
|
241
259
|
|
260
|
+
if info.color_space == :icc
|
261
|
+
_, stream = *self[:ColorSpace]
|
262
|
+
data = flate_decode.encoder(stream.stream_decoder)
|
263
|
+
io << png_chunk('iCCP', "ICCProfile\x00\x00".b << Filter.string_from_source(data))
|
264
|
+
end
|
265
|
+
|
242
266
|
if color_type == ImageLoader::PNG::INDEXED
|
243
267
|
palette_data = self[:ColorSpace][3]
|
244
268
|
palette_data = palette_data.stream unless palette_data.kind_of?(String)
|
@@ -258,11 +282,14 @@ module HexaPDF
|
|
258
282
|
|
259
283
|
filter, = *self[:Filter]
|
260
284
|
decode_parms, = *self[:DecodeParms]
|
261
|
-
if
|
285
|
+
if self[:SMask]
|
286
|
+
data = flate_decode.encoder(Fiber.new { png_combine_image_and_soft_mask(info) }, Predictor: 15,
|
287
|
+
Colors: info.components + 1, Columns: info.width,
|
288
|
+
BitsPerComponent: info.bits_per_component)
|
289
|
+
elsif filter == :FlateDecode && decode_parms && decode_parms[:Predictor].to_i >= 10
|
262
290
|
data = stream_source
|
263
291
|
else
|
264
292
|
colors = (color_type == ImageLoader::PNG::INDEXED ? 1 : info.components)
|
265
|
-
flate_decode = config.constantize('filter.map', :FlateDecode)
|
266
293
|
data = flate_decode.encoder(stream_decoder, Predictor: 15,
|
267
294
|
Colors: colors, Columns: info.width,
|
268
295
|
BitsPerComponent: info.bits_per_component)
|
@@ -277,6 +304,23 @@ module HexaPDF
|
|
277
304
|
[data.length].pack("N") << type << data << [Zlib.crc32(data, Zlib.crc32(type))].pack("N")
|
278
305
|
end
|
279
306
|
|
307
|
+
# Combines the image data with the soft mask data as needed for a PNG data stream.
|
308
|
+
def png_combine_image_and_soft_mask(info)
|
309
|
+
bytes_per_colors = info.bits_per_component * info.components / 8
|
310
|
+
bytes_per_alpha = info.bits_per_component / 8
|
311
|
+
image_data = stream
|
312
|
+
mask_data = self[:SMask].stream
|
313
|
+
|
314
|
+
data = ''.b
|
315
|
+
ii = im = 0
|
316
|
+
while ii < image_data.length
|
317
|
+
data << image_data[ii, bytes_per_colors] << mask_data[im, bytes_per_alpha]
|
318
|
+
ii += bytes_per_colors
|
319
|
+
im += bytes_per_alpha
|
320
|
+
end
|
321
|
+
data
|
322
|
+
end
|
323
|
+
|
280
324
|
end
|
281
325
|
|
282
326
|
end
|
data/lib/hexapdf/version.rb
CHANGED
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.22.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.22.0)>>
|
76
76
|
endobj
|
77
77
|
2 0 obj
|
78
78
|
<</Length 10>>stream
|
@@ -86,7 +86,8 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
86
86
|
before do
|
87
87
|
@acro_form[:Fields] = [
|
88
88
|
{T: "root only", Kids: [{Subtype: :Widget}]},
|
89
|
-
{T: "children", Kids: [{T: "
|
89
|
+
{T: "children", Kids: [{T: "\xFE\xFF".b << "child".encode('UTF-16BE').b, FT: :Btn},
|
90
|
+
{T: "sub", Kids: [{T: "child"}]}]},
|
90
91
|
]
|
91
92
|
end
|
92
93
|
|
@@ -115,16 +115,42 @@ describe HexaPDF::Type::Image do
|
|
115
115
|
assert(info.indexed)
|
116
116
|
assert(info.writable)
|
117
117
|
|
118
|
-
@image[:ColorSpace] = :ICCBased
|
118
|
+
@image[:ColorSpace] = [:ICCBased, {N: 3}]
|
119
119
|
info = @image.info
|
120
|
-
assert_equal(:
|
121
|
-
assert_equal(
|
120
|
+
assert_equal(:icc, info.color_space)
|
121
|
+
assert_equal(3, info.components)
|
122
|
+
assert(info.writable)
|
123
|
+
@image[:ColorSpace] = [:ICCBased, {N: 4}]
|
124
|
+
info = @image.info
|
125
|
+
refute(info.writable)
|
122
126
|
end
|
123
127
|
|
124
128
|
it "processes the SMask entry" do
|
125
|
-
@image[:SMask] = :
|
126
|
-
|
127
|
-
|
129
|
+
@image[:SMask] = {BitsPerComponent: 8, Width: 10, Height: 5}
|
130
|
+
|
131
|
+
@image[:BitsPerComponent] = 8
|
132
|
+
assert(@image.info.writable)
|
133
|
+
|
134
|
+
@image[:BitsPerComponent] = 16
|
135
|
+
refute(@image.info.writable)
|
136
|
+
@image[:SMask][:BitsPerComponent] = 16
|
137
|
+
assert(@image.info.writable)
|
138
|
+
|
139
|
+
@image[:BitsPerComponent] = 4
|
140
|
+
refute(@image.info.writable)
|
141
|
+
@image[:SMask][:BitsPerComponent] = 4
|
142
|
+
refute(@image.info.writable)
|
143
|
+
|
144
|
+
@image[:BitsPerComponent] = @image[:SMask][:BitsPerComponent] = 8
|
145
|
+
@image[:SMask][:Width] = 8
|
146
|
+
refute(@image.info.writable)
|
147
|
+
|
148
|
+
@image[:SMask][:Width] = 10
|
149
|
+
@image[:SMask][:Height] = 8
|
150
|
+
refute(@image.info.writable)
|
151
|
+
|
152
|
+
@image[:SMask][:Height] = 5
|
153
|
+
assert(@image.info.writable)
|
128
154
|
end
|
129
155
|
end
|
130
156
|
|
@@ -184,7 +210,7 @@ describe HexaPDF::Type::Image do
|
|
184
210
|
end
|
185
211
|
|
186
212
|
Dir.glob(File.join(TEST_DATA_DIR, 'images', '*.png')).each do |png_file|
|
187
|
-
next if png_file =~ /alpha/
|
213
|
+
next if png_file =~ /indexed-alpha/
|
188
214
|
it "writes #{File.basename(png_file)} correctly as PNG file" do
|
189
215
|
image = @doc.images.add(png_file)
|
190
216
|
if png_file =~ /greyscale-1bit.png/ # force use of arrays for one image
|
@@ -208,6 +234,7 @@ describe HexaPDF::Type::Image do
|
|
208
234
|
else
|
209
235
|
assert_nil(new_image[:Mask], "file: #{png_file}")
|
210
236
|
end
|
237
|
+
assert(new_image[:SMask]) if png_file =~ /alpha/
|
211
238
|
assert_equal(image.stream, new_image.stream, "file: #{png_file}")
|
212
239
|
|
213
240
|
# ColorSpace is currently not always preserved, e.g. with CalRGB
|
@@ -236,6 +263,15 @@ describe HexaPDF::Type::Image do
|
|
236
263
|
assert_equal(image.stream, new_image.stream)
|
237
264
|
end
|
238
265
|
|
266
|
+
it "works for images with an ICCBased color space" do
|
267
|
+
image = @doc.add({Type: :XObject, Subtype: :Image, Width: 2, Height: 2, BitsPerComponent: 2,
|
268
|
+
ColorSpace: [:ICCBased, @doc.wrap({}, stream: 'abcd')]})
|
269
|
+
image.stream = HexaPDF::StreamData.new(filter: :ASCIIHexDecode) { "10 B0".b }
|
270
|
+
image.write(@file.path)
|
271
|
+
assert_valid_png(@file.path)
|
272
|
+
assert_match(/iCCPICCProfile\x00\x00/, File.binread(@file.path))
|
273
|
+
end
|
274
|
+
|
239
275
|
it "fails if an unsupported stream filter is used" do
|
240
276
|
image = @doc.images.add(@jpg)
|
241
277
|
image.set_filter([:DCTDecode, :ASCIIHexDecode])
|
@@ -244,13 +280,13 @@ describe HexaPDF::Type::Image do
|
|
244
280
|
|
245
281
|
it "fails if an unsupported colorspace is used" do
|
246
282
|
image = @doc.add({Type: :XObject, Subtype: :Image, Width: 1, Height: 1, BitsPerComponent: 8,
|
247
|
-
ColorSpace: :
|
283
|
+
ColorSpace: :Unknown})
|
248
284
|
assert_raises(HexaPDF::Error) { image.write(@file) }
|
249
285
|
end
|
250
286
|
|
251
287
|
it "fails if an indexed image with an unsupported colorspace is used" do
|
252
288
|
image = @doc.add({Type: :XObject, Subtype: :Image, Width: 1, Height: 1, BitsPerComponent: 8,
|
253
|
-
ColorSpace: [:Indexed, :
|
289
|
+
ColorSpace: [:Indexed, :Unknown, 0, "0"]})
|
254
290
|
assert_raises(HexaPDF::Error) { image.write(@file) }
|
255
291
|
end
|
256
292
|
end
|
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.22.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: 2022-03-
|
11
|
+
date: 2022-03-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cmdparse
|
@@ -217,7 +217,6 @@ files:
|
|
217
217
|
- examples/017-frame_text_flow.rb
|
218
218
|
- examples/018-composer.rb
|
219
219
|
- examples/019-acro_form.rb
|
220
|
-
- examples/020-column_box.rb
|
221
220
|
- examples/emoji-smile.png
|
222
221
|
- examples/emoji-wink.png
|
223
222
|
- examples/machupicchu.jpg
|
@@ -334,7 +333,6 @@ files:
|
|
334
333
|
- lib/hexapdf/importer.rb
|
335
334
|
- lib/hexapdf/layout.rb
|
336
335
|
- lib/hexapdf/layout/box.rb
|
337
|
-
- lib/hexapdf/layout/column_box.rb
|
338
336
|
- lib/hexapdf/layout/frame.rb
|
339
337
|
- lib/hexapdf/layout/image_box.rb
|
340
338
|
- lib/hexapdf/layout/inline_box.rb
|
@@ -673,7 +671,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
673
671
|
- !ruby/object:Gem::Version
|
674
672
|
version: '0'
|
675
673
|
requirements: []
|
676
|
-
rubygems_version: 3.
|
674
|
+
rubygems_version: 3.2.32
|
677
675
|
signing_key:
|
678
676
|
specification_version: 4
|
679
677
|
summary: HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
data/examples/020-column_box.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
# ## Column Box
|
2
|
-
#
|
3
|
-
# This example shows how [HexaPDF::Layout::Frame] and [HexaPDF::Layout::TextBox]
|
4
|
-
# can be used to flow text around objects.
|
5
|
-
#
|
6
|
-
# Three boxes are placed repeatedly onto the frame until it is filled: two
|
7
|
-
# floating boxes (one left, one right) and a text box. The text box is styled to
|
8
|
-
# flow its content around the other two boxes.
|
9
|
-
#
|
10
|
-
# Usage:
|
11
|
-
# : `ruby frame_text_flow.rb`
|
12
|
-
#
|
13
|
-
|
14
|
-
require 'hexapdf'
|
15
|
-
require 'hexapdf/utils/graphics_helpers'
|
16
|
-
|
17
|
-
include HexaPDF::Layout
|
18
|
-
include HexaPDF::Utils::GraphicsHelpers
|
19
|
-
|
20
|
-
doc = HexaPDF::Document.new
|
21
|
-
|
22
|
-
sample_text = "Lorem ipsum dolor sit amet, con\u{00AD}sectetur
|
23
|
-
adipis\u{00AD}cing elit, sed do eiusmod tempor incididunt ut labore et
|
24
|
-
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
|
25
|
-
ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
26
|
-
".tr("\n", ' ') * 5
|
27
|
-
items = [TextFragment.create(sample_text, font: doc.fonts.add("Times"))]
|
28
|
-
|
29
|
-
page = doc.pages.add
|
30
|
-
media_box = page.box(:media)
|
31
|
-
canvas = page.canvas
|
32
|
-
frame = Frame.new(media_box.left + 20, media_box.bottom + 20,
|
33
|
-
media_box.width - 40, media_box.height - 40)
|
34
|
-
|
35
|
-
image = doc.images.add(File.join(__dir__, 'machupicchu.jpg'))
|
36
|
-
iw, ih = calculate_dimensions(image.width, image.height, rwidth: 100)
|
37
|
-
|
38
|
-
boxes = []
|
39
|
-
3.times do
|
40
|
-
boxes << Box.create(width: iw, height: ih,
|
41
|
-
margin: [10, 30], position: :float) do |canv, box|
|
42
|
-
canv.image(image, at: [0, 0], width: 100)
|
43
|
-
end
|
44
|
-
boxes << Box.create(width: 50, height: 50, margin: 20,
|
45
|
-
position: :float, position_hint: :right,
|
46
|
-
border: {width: 1, color: [[255, 0, 0]]})
|
47
|
-
boxes << TextBox.new(items, style: {position: :flow, align: :justify})
|
48
|
-
end
|
49
|
-
columns = ColumnBox.new(boxes) #, style: {position: :flow})
|
50
|
-
polygon = Geom2D::Polygon([250, 350], [350, 350], [350, 500], [250, 500])
|
51
|
-
#frame.remove_area(polygon)
|
52
|
-
#canvas.draw(:geom2d, object: polygon)
|
53
|
-
result = frame.fit(columns)
|
54
|
-
p result.success?
|
55
|
-
frame.draw(canvas, result)
|
56
|
-
|
57
|
-
doc.write("column_box.pdf", optimize: true)
|
@@ -1,168 +0,0 @@
|
|
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-2022 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
|
-
|
38
|
-
module HexaPDF
|
39
|
-
module Layout
|
40
|
-
|
41
|
-
# A ColumnBox arranges boxes in one or more columns.
|
42
|
-
#
|
43
|
-
# The number of columns as well as the size of the gap between the columns can be modified.
|
44
|
-
class ColumnBox < Box
|
45
|
-
|
46
|
-
# The child boxes of this ColumnBox.
|
47
|
-
attr_reader :children
|
48
|
-
|
49
|
-
# The number of columns.
|
50
|
-
# TODO: allow array with column widths later like [100, :*, :*]; same for gaps
|
51
|
-
attr_reader :columns
|
52
|
-
|
53
|
-
# The size of the gap between the columns.
|
54
|
-
attr_reader :gap
|
55
|
-
|
56
|
-
# Creates a new ColumnBox object for the given +children+ boxes.
|
57
|
-
def initialize(children = [], columns = 2, gap: 36, **kwargs)
|
58
|
-
super(**kwargs)
|
59
|
-
@children = children
|
60
|
-
@columns = columns
|
61
|
-
@gap = gap
|
62
|
-
end
|
63
|
-
|
64
|
-
# Fits the column box into the available space.
|
65
|
-
def fit(available_width, available_height, frame)
|
66
|
-
last_height_difference = 1_000_000
|
67
|
-
height = if style.position == :flow
|
68
|
-
frame.height
|
69
|
-
else
|
70
|
-
(@initial_height > 0 ? @initial_height : available_height) - reserved_height
|
71
|
-
end
|
72
|
-
while true
|
73
|
-
p '-'*100
|
74
|
-
@frames = []
|
75
|
-
if style.position == :flow
|
76
|
-
column_width = (frame.width - gap * (@columns - 1)).to_f / @columns
|
77
|
-
@columns.times do |col_nr|
|
78
|
-
left = (column_width + gap) * col_nr + frame.left
|
79
|
-
bottom = frame.bottom
|
80
|
-
rect = Geom2D::Polygon([left, bottom],
|
81
|
-
[left + column_width, bottom],
|
82
|
-
[left + column_width, bottom + height],
|
83
|
-
[left, bottom + height])
|
84
|
-
shape = Geom2D::Algorithms::PolygonOperation.run(frame.shape, rect, :intersection)
|
85
|
-
col_frame = Frame.new(left, bottom, column_width, height)
|
86
|
-
col_frame.shape = shape
|
87
|
-
@frames << col_frame
|
88
|
-
end
|
89
|
-
@frame_index = 0
|
90
|
-
@results = @children.map {|child_box| fit_box(child_box) }
|
91
|
-
@width = frame.width
|
92
|
-
@height = frame.height - @frames.min_by(&:y).y
|
93
|
-
else
|
94
|
-
width = (@initial_width > 0 ? @initial_width : available_width) - reserved_width
|
95
|
-
column_width = (width - gap * (@columns - 1)).to_f / @columns
|
96
|
-
@columns.times do |col_nr|
|
97
|
-
@frames << Frame.new((column_width + gap) * col_nr, 0, column_width, height)
|
98
|
-
end
|
99
|
-
@frame_index = 0
|
100
|
-
@results = @children.map {|child_box| fit_box(child_box) }
|
101
|
-
@width = width
|
102
|
-
@height = height - @frames.min_by(&:y).y
|
103
|
-
end
|
104
|
-
min_y, max_y = @frames.minmax_by(&:y).map(&:y)
|
105
|
-
p [height, @frames.map(&:y), last_height_difference, min_y, max_y]
|
106
|
-
# TOOD: @result.any?(&:empty?) only for the first run!!!! if the first run fails, we
|
107
|
-
# cannot balance the columns because there is too much content.
|
108
|
-
# TODO: another break condition is if the @results didn't change since the last run
|
109
|
-
p [:maybe_redo, min_y, max_y, height, last_height_difference]
|
110
|
-
p [@results.map {|arr| arr.all? {|r| r.status }}]
|
111
|
-
break if max_y != height && @results.all? {|arr| !arr.empty? && arr.all? {|r| r.success? }} &&
|
112
|
-
(@results.any?(&:empty?) ||
|
113
|
-
max_y - min_y >= last_height_difference ||
|
114
|
-
max_y - min_y < 0.5)
|
115
|
-
if max_y == 0 && min_y == 0
|
116
|
-
height += last_height_difference / 4.0
|
117
|
-
else
|
118
|
-
last_height_difference = max_y - min_y
|
119
|
-
height -= last_height_difference / 2.0
|
120
|
-
end
|
121
|
-
end
|
122
|
-
@results.all? {|res| res.length == 1 }
|
123
|
-
end
|
124
|
-
|
125
|
-
private
|
126
|
-
|
127
|
-
def fit_box(box)
|
128
|
-
cur_frame = @frames[@frame_index]
|
129
|
-
fit_results = []
|
130
|
-
while cur_frame
|
131
|
-
result = cur_frame.fit(box)
|
132
|
-
if result.success?
|
133
|
-
cur_frame.remove_area(result.mask)
|
134
|
-
fit_results << result
|
135
|
-
break
|
136
|
-
elsif cur_frame.full?
|
137
|
-
@frame_index += 1
|
138
|
-
break if @frame_index == @frames.length
|
139
|
-
cur_frame = @frames[@frame_index]
|
140
|
-
else
|
141
|
-
draw_box, box = cur_frame.split(result)
|
142
|
-
if draw_box
|
143
|
-
cur_frame.remove_area(result.mask)
|
144
|
-
fit_results << result
|
145
|
-
elsif !cur_frame.find_next_region
|
146
|
-
@frame_index += 1
|
147
|
-
break if @frame_index == @frames.length
|
148
|
-
cur_frame = @frames[@frame_index]
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
fit_results
|
153
|
-
end
|
154
|
-
|
155
|
-
# Draws the child boxes onto the canvas at position [x, y].
|
156
|
-
def draw_content(canvas, x, y)
|
157
|
-
x = y = 0 if style.position == :flow
|
158
|
-
@results.each do |result_boxes|
|
159
|
-
result_boxes.each do |result|
|
160
|
-
result.box.draw(canvas, x + result.x, y + result.y)
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
end
|
166
|
-
|
167
|
-
end
|
168
|
-
end
|