hexapdf 0.21.1 → 0.22.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 +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
|