hexapdf 0.8.0 → 0.9.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 +52 -1
- data/CONTRIBUTERS +1 -1
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/examples/018-composer.rb +44 -0
- data/lib/hexapdf/cli.rb +2 -0
- data/lib/hexapdf/cli/command.rb +2 -2
- data/lib/hexapdf/cli/optimize.rb +1 -1
- data/lib/hexapdf/cli/split.rb +82 -0
- data/lib/hexapdf/composer.rb +303 -0
- data/lib/hexapdf/configuration.rb +2 -2
- data/lib/hexapdf/content/canvas.rb +3 -6
- data/lib/hexapdf/dictionary.rb +0 -3
- data/lib/hexapdf/document.rb +30 -22
- data/lib/hexapdf/document/files.rb +1 -1
- data/lib/hexapdf/document/images.rb +1 -1
- data/lib/hexapdf/filter/predictor.rb +8 -8
- data/lib/hexapdf/layout.rb +1 -0
- data/lib/hexapdf/layout/box.rb +55 -12
- data/lib/hexapdf/layout/frame.rb +143 -46
- data/lib/hexapdf/layout/image_box.rb +96 -0
- data/lib/hexapdf/layout/inline_box.rb +10 -0
- data/lib/hexapdf/layout/line.rb +1 -1
- data/lib/hexapdf/layout/style.rb +55 -3
- data/lib/hexapdf/layout/text_box.rb +38 -8
- data/lib/hexapdf/layout/text_layouter.rb +66 -52
- data/lib/hexapdf/object.rb +3 -2
- data/lib/hexapdf/parser.rb +6 -1
- data/lib/hexapdf/rectangle.rb +6 -0
- data/lib/hexapdf/reference.rb +4 -6
- data/lib/hexapdf/revision.rb +34 -8
- data/lib/hexapdf/revisions.rb +12 -2
- data/lib/hexapdf/stream.rb +18 -0
- data/lib/hexapdf/task.rb +1 -1
- data/lib/hexapdf/task/dereference.rb +1 -1
- data/lib/hexapdf/task/optimize.rb +2 -2
- data/lib/hexapdf/type/form.rb +10 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +25 -5
- data/man/man1/hexapdf.1 +17 -0
- data/test/hexapdf/layout/test_box.rb +7 -1
- data/test/hexapdf/layout/test_frame.rb +38 -1
- data/test/hexapdf/layout/test_image_box.rb +73 -0
- data/test/hexapdf/layout/test_inline_box.rb +8 -0
- data/test/hexapdf/layout/test_line.rb +1 -1
- data/test/hexapdf/layout/test_style.rb +58 -1
- data/test/hexapdf/layout/test_text_box.rb +57 -12
- data/test/hexapdf/layout/test_text_layouter.rb +14 -4
- data/test/hexapdf/task/test_optimize.rb +3 -3
- data/test/hexapdf/test_composer.rb +258 -0
- data/test/hexapdf/test_document.rb +29 -3
- data/test/hexapdf/test_importer.rb +8 -2
- data/test/hexapdf/test_object.rb +3 -1
- data/test/hexapdf/test_parser.rb +6 -0
- data/test/hexapdf/test_rectangle.rb +7 -0
- data/test/hexapdf/test_reference.rb +3 -1
- data/test/hexapdf/test_revision.rb +13 -0
- data/test/hexapdf/test_revisions.rb +1 -0
- data/test/hexapdf/test_stream.rb +13 -0
- data/test/hexapdf/test_writer.rb +13 -2
- data/test/hexapdf/type/test_annotation.rb +1 -1
- data/test/hexapdf/type/test_form.rb +13 -2
- metadata +10 -4
@@ -443,7 +443,7 @@ module HexaPDF
|
|
443
443
|
XXViewerPreferences: 'HexaPDF::Type::ViewerPreferences',
|
444
444
|
Action: 'HexaPDF::Type::Action',
|
445
445
|
XXLaunchActionWinParameters: 'HexaPDF::Type::Actions::Launch::WinParameters',
|
446
|
-
|
446
|
+
Annot: 'HexaPDF::Type::Annotation',
|
447
447
|
},
|
448
448
|
'object.subtype_map' => {
|
449
449
|
nil => {
|
@@ -479,7 +479,7 @@ module HexaPDF
|
|
479
479
|
Launch: 'HexaPDF::Type::Actions::Launch',
|
480
480
|
URI: 'HexaPDF::Type::Actions::URI',
|
481
481
|
},
|
482
|
-
|
482
|
+
Annot: {
|
483
483
|
Text: 'HexaPDF::Type::Annotations::Text',
|
484
484
|
Link: 'HexaPDF::Type::Annotations::Link',
|
485
485
|
},
|
@@ -1258,12 +1258,9 @@ module HexaPDF
|
|
1258
1258
|
obj = context.document.images.add(obj)
|
1259
1259
|
end
|
1260
1260
|
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
else
|
1265
|
-
width, height = calculate_dimensions(obj.box.width, obj.box.height,
|
1266
|
-
rwidth: width, rheight: height)
|
1261
|
+
width, height = calculate_dimensions(obj.width, obj.height,
|
1262
|
+
rwidth: width, rheight: height)
|
1263
|
+
if obj[:Subtype] != :Image
|
1267
1264
|
width /= obj.box.width.to_f
|
1268
1265
|
height /= obj.box.height.to_f
|
1269
1266
|
at[0] -= obj.box.left
|
data/lib/hexapdf/dictionary.rb
CHANGED
@@ -261,9 +261,6 @@ module HexaPDF
|
|
261
261
|
each_set_key_or_required_field do |name, field|
|
262
262
|
obj = key?(name) ? self[name] : nil
|
263
263
|
|
264
|
-
# Validate nested objects
|
265
|
-
validate_nested(obj, &block)
|
266
|
-
|
267
264
|
# The checks below need a valid field definition
|
268
265
|
next if field.nil?
|
269
266
|
|
data/lib/hexapdf/document.rb
CHANGED
@@ -57,6 +57,8 @@ require 'hexapdf/layout'
|
|
57
57
|
# * HexaPDF::Content::Canvas provides the canvas API for drawing/writing on a page or form XObject
|
58
58
|
module HexaPDF
|
59
59
|
|
60
|
+
autoload(:Composer, 'hexapdf/composer')
|
61
|
+
|
60
62
|
# Represents one PDF document.
|
61
63
|
#
|
62
64
|
# A PDF document consists of (indirect) objects, so the main job of this class is to provide
|
@@ -367,12 +369,13 @@ module HexaPDF
|
|
367
369
|
end
|
368
370
|
|
369
371
|
# :call-seq:
|
370
|
-
# doc.each(
|
371
|
-
# doc.each(
|
372
|
-
# doc.each(
|
372
|
+
# doc.each(only_current: true, only_loaded: false) {|obj| block } -> doc
|
373
|
+
# doc.each(only_current: true, only_loaded: false) {|obj, rev| block } -> doc
|
374
|
+
# doc.each(only_current: true, only_loaded: false) -> Enumerator
|
373
375
|
#
|
374
|
-
# Calls the given block once for every object
|
375
|
-
# only the object or the object and the
|
376
|
+
# Calls the given block once for every object, or, if +only_loaded+ is +true+, for every loaded
|
377
|
+
# object in the PDF document. The block may either accept only the object or the object and the
|
378
|
+
# revision it is in.
|
376
379
|
#
|
377
380
|
# By default, only the current version of each object is returned which implies that each
|
378
381
|
# object number is yielded exactly once. If the +current+ option is +false+, all stored
|
@@ -387,14 +390,16 @@ module HexaPDF
|
|
387
390
|
# * Additionally, there may also be objects with the same object number but different
|
388
391
|
# generation numbers in different revisions, e.g. one object with oid/gen [3,0] and one with
|
389
392
|
# oid/gen [3,1].
|
390
|
-
def each(
|
391
|
-
|
393
|
+
def each(only_current: true, only_loaded: false, &block)
|
394
|
+
unless block_given?
|
395
|
+
return to_enum(__method__, only_current: only_current, only_loaded: only_loaded)
|
396
|
+
end
|
392
397
|
|
393
398
|
yield_rev = (block.arity == 2)
|
394
399
|
oids = {}
|
395
400
|
@revisions.reverse_each do |rev|
|
396
|
-
rev.each do |obj|
|
397
|
-
next if
|
401
|
+
rev.each(only_loaded: only_loaded) do |obj|
|
402
|
+
next if only_current && oids.include?(obj.oid)
|
398
403
|
(yield_rev ? yield(obj, rev) : yield(obj))
|
399
404
|
oids[obj.oid] = true
|
400
405
|
end
|
@@ -553,22 +558,18 @@ module HexaPDF
|
|
553
558
|
@security_handler
|
554
559
|
end
|
555
560
|
|
556
|
-
#
|
557
|
-
#
|
558
|
-
# doc.validate(auto_correct: true) {|object, msg, correctable| block } -> true or false
|
559
|
-
#
|
560
|
-
# Validates all objects of the document, with optional auto-correction, and returns +true+ if
|
561
|
-
# everything is fine.
|
561
|
+
# Validates all objects, or, if +only_loaded+ is +true+, only loaded objects, with optional
|
562
|
+
# auto-correction, and returns +true+ if everything is fine.
|
562
563
|
#
|
563
564
|
# If a block is given, it is called on validation problems.
|
564
565
|
#
|
565
566
|
# See HexaPDF::Object#validate for more information.
|
566
|
-
def validate(auto_correct: true)
|
567
|
+
def validate(auto_correct: true, only_loaded: false) #:yield: object, msg, correctable
|
567
568
|
cur_obj = trailer
|
568
569
|
block = (block_given? ? lambda {|msg, correctable| yield(cur_obj, msg, correctable) } : nil)
|
569
570
|
|
570
571
|
result = trailer.validate(auto_correct: auto_correct, &block)
|
571
|
-
each(
|
572
|
+
each(only_current: false, only_loaded: only_loaded) do |obj|
|
572
573
|
cur_obj = obj
|
573
574
|
result &&= obj.validate(auto_correct: auto_correct, &block)
|
574
575
|
end
|
@@ -576,8 +577,8 @@ module HexaPDF
|
|
576
577
|
end
|
577
578
|
|
578
579
|
# :call-seq:
|
579
|
-
# doc.write(filename, validate: true, update_fields: true, optimize: false)
|
580
|
-
# doc.write(io, validate: true, update_fields: true, optimize: false)
|
580
|
+
# doc.write(filename, incremental: false, validate: true, update_fields: true, optimize: false)
|
581
|
+
# doc.write(io, incremental: false, validate: true, update_fields: true, optimize: false)
|
581
582
|
#
|
582
583
|
# Writes the document to the given file (in case +io+ is a String) or IO stream.
|
583
584
|
#
|
@@ -586,6 +587,13 @@ module HexaPDF
|
|
586
587
|
#
|
587
588
|
# Options:
|
588
589
|
#
|
590
|
+
# incremental::
|
591
|
+
# Use the incremental writing mode which just adds a new revision to an existing document.
|
592
|
+
# This is needed, for example, when modifying a signed PDF and the original signature should
|
593
|
+
# stay valid.
|
594
|
+
#
|
595
|
+
# See: PDF1.7 s7.5.6
|
596
|
+
#
|
589
597
|
# validate::
|
590
598
|
# Validates the document and raises an error if an uncorrectable problem is found.
|
591
599
|
#
|
@@ -596,7 +604,7 @@ module HexaPDF
|
|
596
604
|
# optimize::
|
597
605
|
# Optimize the file size by using object and cross-reference streams. This will raise the PDF
|
598
606
|
# version to at least 1.5.
|
599
|
-
def write(file_or_io, validate: true, update_fields: true, optimize: false)
|
607
|
+
def write(file_or_io, incremental: false, validate: true, update_fields: true, optimize: false)
|
600
608
|
dispatch_message(:complete_objects)
|
601
609
|
|
602
610
|
if update_fields
|
@@ -619,9 +627,9 @@ module HexaPDF
|
|
619
627
|
dispatch_message(:before_write)
|
620
628
|
|
621
629
|
if file_or_io.kind_of?(String)
|
622
|
-
File.open(file_or_io, 'w+') {|file| Writer.write(self, file) }
|
630
|
+
File.open(file_or_io, 'w+') {|file| Writer.write(self, file, incremental: incremental) }
|
623
631
|
else
|
624
|
-
Writer.write(self, file_or_io)
|
632
|
+
Writer.write(self, file_or_io, incremental: incremental)
|
625
633
|
end
|
626
634
|
end
|
627
635
|
|
@@ -85,7 +85,7 @@ module HexaPDF
|
|
85
85
|
# Note that only real images are yielded which means, for example, that images used as soft
|
86
86
|
# mask are not.
|
87
87
|
def each(&block)
|
88
|
-
images = @document.each(
|
88
|
+
images = @document.each(only_current: false).select do |obj|
|
89
89
|
next unless obj.kind_of?(HexaPDF::Dictionary)
|
90
90
|
obj[:Subtype] == :Image && !obj[:ImageMask]
|
91
91
|
end
|
@@ -163,16 +163,16 @@ module HexaPDF
|
|
163
163
|
case data.getbyte(pos)
|
164
164
|
when PREDICTOR_PNG_SUB
|
165
165
|
bytes_per_pixel.upto(bytes_per_row - 2) do |i|
|
166
|
-
line.setbyte(i, line.getbyte(i) + line.getbyte(i - bytes_per_pixel))
|
166
|
+
line.setbyte(i, (line.getbyte(i) + line.getbyte(i - bytes_per_pixel)) % 256)
|
167
167
|
end
|
168
168
|
when PREDICTOR_PNG_UP
|
169
169
|
0.upto(bytes_per_row - 2) do |i|
|
170
|
-
line.setbyte(i, line.getbyte(i) + last_line.getbyte(i))
|
170
|
+
line.setbyte(i, (line.getbyte(i) + last_line.getbyte(i)) % 256)
|
171
171
|
end
|
172
172
|
when PREDICTOR_PNG_AVERAGE
|
173
173
|
0.upto(bytes_per_row - 2) do |i|
|
174
174
|
a = i < bytes_per_pixel ? 0 : line.getbyte(i - bytes_per_pixel)
|
175
|
-
line.setbyte(i, line.getbyte(i) + ((a + last_line.getbyte(i)) >> 1))
|
175
|
+
line.setbyte(i, (line.getbyte(i) + ((a + last_line.getbyte(i)) >> 1)) % 256)
|
176
176
|
end
|
177
177
|
when PREDICTOR_PNG_PAETH
|
178
178
|
0.upto(bytes_per_row - 2) do |i|
|
@@ -187,7 +187,7 @@ module HexaPDF
|
|
187
187
|
|
188
188
|
point = ((pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c))
|
189
189
|
|
190
|
-
line.setbyte(i, line.getbyte(i) + point)
|
190
|
+
line.setbyte(i, (line.getbyte(i) + point) % 256)
|
191
191
|
end
|
192
192
|
end
|
193
193
|
|
@@ -202,16 +202,16 @@ module HexaPDF
|
|
202
202
|
case predictor
|
203
203
|
when PREDICTOR_PNG_SUB
|
204
204
|
bytes_per_row.downto(bytes_per_pixel + 1) do |i|
|
205
|
-
line.setbyte(i, line.getbyte(i) - line.getbyte(i - bytes_per_pixel))
|
205
|
+
line.setbyte(i, (line.getbyte(i) - line.getbyte(i - bytes_per_pixel)) % 256)
|
206
206
|
end
|
207
207
|
when PREDICTOR_PNG_UP
|
208
208
|
bytes_per_row.downto(1) do |i|
|
209
|
-
line.setbyte(i, line.getbyte(i) - last_line.getbyte(i))
|
209
|
+
line.setbyte(i, (line.getbyte(i) - last_line.getbyte(i)) % 256)
|
210
210
|
end
|
211
211
|
when PREDICTOR_PNG_AVERAGE
|
212
212
|
bytes_per_row.downto(1) do |i|
|
213
213
|
a = i <= bytes_per_pixel ? 0 : line.getbyte(i - bytes_per_pixel)
|
214
|
-
line.setbyte(i, line.getbyte(i) - ((a + last_line.getbyte(i)) >> 1))
|
214
|
+
line.setbyte(i, (line.getbyte(i) - ((a + last_line.getbyte(i)) >> 1)) % 256)
|
215
215
|
end
|
216
216
|
when PREDICTOR_PNG_PAETH
|
217
217
|
bytes_per_row.downto(1) do |i|
|
@@ -226,7 +226,7 @@ module HexaPDF
|
|
226
226
|
|
227
227
|
point = ((pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c))
|
228
228
|
|
229
|
-
line.setbyte(i, line.getbyte(i) - point)
|
229
|
+
line.setbyte(i, (line.getbyte(i) - point) % 256)
|
230
230
|
end
|
231
231
|
end
|
232
232
|
|
data/lib/hexapdf/layout.rb
CHANGED
data/lib/hexapdf/layout/box.rb
CHANGED
@@ -95,31 +95,49 @@ module HexaPDF
|
|
95
95
|
@height = @initial_height = height
|
96
96
|
@style = (style.kind_of?(Style) ? style : Style.new(style))
|
97
97
|
@draw_block = block
|
98
|
-
@outline = nil
|
99
98
|
end
|
100
99
|
|
101
100
|
# The width of the content box, i.e. without padding and/or borders.
|
102
101
|
def content_width
|
103
|
-
|
104
|
-
|
102
|
+
width = @width - reserved_width
|
103
|
+
width < 0 ? 0 : width
|
105
104
|
end
|
106
105
|
|
107
106
|
# The height of the content box, i.e. without padding and/or borders.
|
108
107
|
def content_height
|
109
|
-
|
110
|
-
|
108
|
+
height = @height - reserved_height
|
109
|
+
height < 0 ? 0 : height
|
111
110
|
end
|
112
111
|
|
113
112
|
# Fits the box into the Frame and returns +true+ if fitting was successful.
|
114
113
|
#
|
115
114
|
# The default implementation uses the whole available space for width and height if they were
|
116
115
|
# initially set to 0. Otherwise the specified dimensions are used.
|
117
|
-
def fit(available_width, available_height,
|
116
|
+
def fit(available_width, available_height, _frame)
|
118
117
|
@width = (@initial_width > 0 ? @initial_width : available_width)
|
119
118
|
@height = (@initial_height > 0 ? @initial_height : available_height)
|
120
119
|
@width <= available_width && @height <= available_height
|
121
120
|
end
|
122
121
|
|
122
|
+
# Tries to split the box into two, the first of which needs to fit into the available space,
|
123
|
+
# and returns the parts as array.
|
124
|
+
#
|
125
|
+
# In many cases the first box in the list will be this box, meaning that even when #fit fails,
|
126
|
+
# a part of the box may still fit. Note that #fit may not be called if the first box is this
|
127
|
+
# box since it is assumed that it is already fitted. If not even a part of this box fits into
|
128
|
+
# the available space, +nil+ should be returned as the first array element.
|
129
|
+
#
|
130
|
+
# Possible return values:
|
131
|
+
#
|
132
|
+
# [self]:: The box fully fits into the available space.
|
133
|
+
# [nil, self]:: The box can't be split or no part of the box fits into the available space.
|
134
|
+
# [self, new_box]:: A part of the box fits and a new box is returned for the rest.
|
135
|
+
#
|
136
|
+
# This default implementation provides no splitting functionality.
|
137
|
+
def split(_available_width, _available_height, _frame)
|
138
|
+
[nil, self]
|
139
|
+
end
|
140
|
+
|
123
141
|
# Draws the content of the box onto the canvas at the position (x, y).
|
124
142
|
#
|
125
143
|
# The coordinate system is translated so that the origin is at the bottom left corner of the
|
@@ -139,12 +157,11 @@ module HexaPDF
|
|
139
157
|
style.underlays.draw(canvas, x, y, self) if style.underlays?
|
140
158
|
style.border.draw(canvas, x, y, width, height) if style.border?
|
141
159
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
end
|
160
|
+
cx = x
|
161
|
+
cy = y
|
162
|
+
(cx += style.padding.left; cy += style.padding.bottom) if style.padding?
|
163
|
+
(cx += style.border.width.left; cy += style.border.width.bottom) if style.border?
|
164
|
+
draw_content(canvas, cx, cy)
|
148
165
|
|
149
166
|
style.overlays.draw(canvas, x, y, self) if style.overlays?
|
150
167
|
end
|
@@ -158,6 +175,32 @@ module HexaPDF
|
|
158
175
|
(style.overlays? && !style.overlays.none?))
|
159
176
|
end
|
160
177
|
|
178
|
+
private
|
179
|
+
|
180
|
+
# Returns the width that is reserved by the padding and border style properties.
|
181
|
+
def reserved_width
|
182
|
+
result = 0
|
183
|
+
result += style.padding.left + style.padding.right if style.padding?
|
184
|
+
result += style.border.width.left + style.border.width.right if style.border?
|
185
|
+
result
|
186
|
+
end
|
187
|
+
|
188
|
+
# Returns the height that is reserved by the padding and border style properties.
|
189
|
+
def reserved_height
|
190
|
+
result = 0
|
191
|
+
result += style.padding.top + style.padding.bottom if style.padding?
|
192
|
+
result += style.border.width.top + style.border.width.bottom if style.border?
|
193
|
+
result
|
194
|
+
end
|
195
|
+
|
196
|
+
# Draws the content of the box at position [x, y] which is the bottom-left corner of the
|
197
|
+
# content box.
|
198
|
+
def draw_content(canvas, x, y)
|
199
|
+
if @draw_block
|
200
|
+
canvas.translate(x, y) { @draw_block.call(canvas, self) }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
161
204
|
end
|
162
205
|
|
163
206
|
end
|
data/lib/hexapdf/layout/frame.rb
CHANGED
@@ -42,11 +42,35 @@ module HexaPDF
|
|
42
42
|
#
|
43
43
|
# == Usage
|
44
44
|
#
|
45
|
-
# After a Frame object is initialized,
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
45
|
+
# After a Frame object is initialized, it is ready for drawing boxes on it.
|
46
|
+
#
|
47
|
+
# The explicit way of drawing a box follows these steps:
|
48
|
+
#
|
49
|
+
# * Call #fit with the box to see if the box can fit into the currently selected region of
|
50
|
+
# available space. If fitting is successful, the box can be drawn using #draw.
|
51
|
+
#
|
52
|
+
# The method #fit is also called for absolutely positioned boxes but since these boxes are not
|
53
|
+
# subject to the normal constraints, the available space used is the width and height inside
|
54
|
+
# the frame to the right and top of the bottom-left corner of the box.
|
55
|
+
#
|
56
|
+
# * If the box didn't fit, call #find_next_region to determine the next region for placing the
|
57
|
+
# box. If a new region was found, start over with #fit. Otherwise the frame has no more space
|
58
|
+
# for placing boxes.
|
59
|
+
#
|
60
|
+
# * Alternatively to calling #find_next_region it is also possible to call #split. This method
|
61
|
+
# tries to split the box into two so that the first part fits into the current region. If
|
62
|
+
# splitting is successful, the first box can be drawn (Make sure that the second box is
|
63
|
+
# handled correctly). Otherwise, start over with #find_next_region.
|
64
|
+
#
|
65
|
+
# For applications where splitting is not necessary, an easier way is to just use #draw and
|
66
|
+
# #find_next_region together, as #draw calls #fit if the box was not fit into the current
|
67
|
+
# region.
|
68
|
+
#
|
69
|
+
# == Used Box Properties
|
70
|
+
#
|
71
|
+
# The style properties "position", "position_hint" and "margin" are taken into account when
|
72
|
+
# fitting, splitting or drawing a box. Note that the margin is ignored if a box's side coincides
|
73
|
+
# with the frame's original boundary.
|
50
74
|
#
|
51
75
|
# == Frame Shape and Contour Line
|
52
76
|
#
|
@@ -63,6 +87,42 @@ module HexaPDF
|
|
63
87
|
|
64
88
|
include Geom2D::Utils
|
65
89
|
|
90
|
+
# Internal class for storing data of a fitted box.
|
91
|
+
class FitData
|
92
|
+
|
93
|
+
# The box that was fitted into the frame.
|
94
|
+
attr_accessor :box
|
95
|
+
|
96
|
+
# The available width for this particular box.
|
97
|
+
attr_accessor :available_width
|
98
|
+
|
99
|
+
# The available height for this particular box.
|
100
|
+
attr_accessor :available_height
|
101
|
+
|
102
|
+
# The left margin to use instead of +box.style.margin.left+.
|
103
|
+
attr_accessor :margin_left
|
104
|
+
|
105
|
+
# The right margin to use instead of +box.style.margin.right+.
|
106
|
+
attr_accessor :margin_right
|
107
|
+
|
108
|
+
# The top margin to use instead of +box.style.margin.top+.
|
109
|
+
attr_accessor :margin_top
|
110
|
+
|
111
|
+
# Initialize the object by calling #reset.
|
112
|
+
def initialize
|
113
|
+
reset
|
114
|
+
end
|
115
|
+
|
116
|
+
# Resets the object.
|
117
|
+
def reset(box = nil, available_width = 0, available_height = 0)
|
118
|
+
@box = box
|
119
|
+
@available_width = available_width
|
120
|
+
@available_height = available_height
|
121
|
+
@margin_left = @margin_right = @margin_top = 0
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
66
126
|
# The x-coordinate of the bottom-left corner.
|
67
127
|
attr_reader :left
|
68
128
|
|
@@ -78,12 +138,9 @@ module HexaPDF
|
|
78
138
|
# The shape of the frame, a Geom2D::PolygonSet consisting of rectilinear polygons.
|
79
139
|
attr_reader :shape
|
80
140
|
|
81
|
-
# The contour line of the frame, a Geom2D::PolygonSet consisting of arbitrary polygons.
|
82
|
-
attr_reader :contour_line
|
83
|
-
|
84
141
|
# The x-coordinate where the next box will be placed.
|
85
142
|
#
|
86
|
-
# Note: Since the algorithm for
|
143
|
+
# Note: Since the algorithm for drawing takes the margin of a box into account, the actual
|
87
144
|
# x-coordinate (and y-coordinate, available width and available height) might be different.
|
88
145
|
attr_reader :x
|
89
146
|
|
@@ -103,18 +160,12 @@ module HexaPDF
|
|
103
160
|
attr_reader :available_height
|
104
161
|
|
105
162
|
# Creates a new Frame object for the given rectangular area.
|
106
|
-
#
|
107
|
-
# If the contour line of the frame is not specified, then the rectangular area is used as
|
108
|
-
# contour line.
|
109
163
|
def initialize(left, bottom, width, height, contour_line: nil)
|
110
164
|
@left = left
|
111
165
|
@bottom = bottom
|
112
166
|
@width = width
|
113
167
|
@height = height
|
114
|
-
@contour_line = contour_line
|
115
|
-
[create_rectangle(left, bottom, left + width, bottom + height)]
|
116
|
-
)
|
117
|
-
|
168
|
+
@contour_line = contour_line
|
118
169
|
@shape = Geom2D::PolygonSet.new(
|
119
170
|
[create_rectangle(left, bottom, left + width, bottom + height)]
|
120
171
|
)
|
@@ -123,36 +174,69 @@ module HexaPDF
|
|
123
174
|
@available_width = width
|
124
175
|
@available_height = height
|
125
176
|
@region_selection = :max_height
|
177
|
+
@fit_data = FitData.new
|
126
178
|
end
|
127
179
|
|
128
|
-
#
|
129
|
-
|
130
|
-
#
|
131
|
-
# When positioning the box, the style properties "position", "position_hint" and "margin" are
|
132
|
-
# taken into account. Note that the margin is ignored if a box's side coincides with the
|
133
|
-
# frame's original boundary.
|
134
|
-
#
|
135
|
-
# After a box is successfully drawn, the frame's shape and contour line are adjusted to remove
|
136
|
-
# the occupied area.
|
137
|
-
def draw(canvas, box)
|
180
|
+
# Fits the given box into the current region of available space.
|
181
|
+
def fit(box)
|
138
182
|
aw = available_width
|
139
183
|
ah = available_height
|
140
|
-
|
184
|
+
@fit_data.reset(box, aw, ah)
|
141
185
|
|
142
|
-
if
|
186
|
+
if full?
|
187
|
+
false
|
188
|
+
elsif box.style.position == :absolute
|
189
|
+
x, y = box.style.position_hint
|
190
|
+
box.fit(width - x, height - y, self)
|
191
|
+
true
|
192
|
+
else
|
143
193
|
if box.style.margin?
|
144
194
|
margin = box.style.margin
|
145
195
|
ah -= margin.bottom unless float_equal(@y - ah, @bottom)
|
146
|
-
ah -=
|
147
|
-
aw -=
|
148
|
-
aw -=
|
196
|
+
ah -= @fit_data.margin_top = margin.top unless float_equal(@y, @bottom + @height)
|
197
|
+
aw -= @fit_data.margin_right = margin.right unless float_equal(@x + aw, @left + @width)
|
198
|
+
aw -= @fit_data.margin_left = margin.left unless float_equal(@x, @left)
|
199
|
+
@fit_data.available_width = aw
|
200
|
+
@fit_data.available_height = ah
|
149
201
|
end
|
150
202
|
|
151
|
-
|
203
|
+
box.fit(aw, ah, self)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Tries to split the (fitted) box into two parts, where the first part needs to fit into the
|
208
|
+
# available space, and returns both parts.
|
209
|
+
#
|
210
|
+
# If the given box is not the last fitted box, #fit is called before splitting the box.
|
211
|
+
#
|
212
|
+
# See Box#split for further details.
|
213
|
+
def split(box)
|
214
|
+
fit(box) unless box == @fit_data.box
|
215
|
+
boxes = box.split(@fit_data.available_width, @fit_data.available_height, self)
|
216
|
+
@fit_data.reset unless boxes[0] == @fit_data.box
|
217
|
+
boxes
|
218
|
+
end
|
219
|
+
|
220
|
+
# Draws the given (fitted) box onto the canvas at the frame's current position. Returns +true+
|
221
|
+
# if drawing was possible, +false+ otherwise.
|
222
|
+
#
|
223
|
+
# If the given box is not the last fitted box, #fit is called before drawing the box.
|
224
|
+
#
|
225
|
+
# After a box is successfully drawn, the frame's shape and contour line are adjusted to remove
|
226
|
+
# the occupied area.
|
227
|
+
def draw(canvas, box)
|
228
|
+
unless box == @fit_data.box
|
229
|
+
fit(box) || return
|
152
230
|
end
|
153
231
|
|
154
232
|
width = box.width
|
155
233
|
height = box.height
|
234
|
+
margin = box.style.margin if box.style.margin?
|
235
|
+
|
236
|
+
if height == 0
|
237
|
+
@fit_data.reset
|
238
|
+
return true
|
239
|
+
end
|
156
240
|
|
157
241
|
case box.style.position
|
158
242
|
when :absolute
|
@@ -160,18 +244,17 @@ module HexaPDF
|
|
160
244
|
x += left
|
161
245
|
y += bottom
|
162
246
|
rectangle = if box.style.margin?
|
163
|
-
margin = box.style.margin
|
164
247
|
create_rectangle(x - margin.left, y - margin.bottom,
|
165
248
|
x + width + margin.right, y + height + margin.top)
|
166
249
|
else
|
167
250
|
create_rectangle(x, y, x + width, y + height)
|
168
251
|
end
|
169
252
|
when :float
|
170
|
-
x = @x +
|
171
|
-
x +=
|
172
|
-
y = @y - height -
|
173
|
-
# We
|
174
|
-
#
|
253
|
+
x = @x + @fit_data.margin_left
|
254
|
+
x += @fit_data.available_width - width if box.style.position_hint == :right
|
255
|
+
y = @y - height - @fit_data.margin_top
|
256
|
+
# We use the real margins from the box because they either have the desired effect or just
|
257
|
+
# extend the rectangle outside the frame.
|
175
258
|
rectangle = create_rectangle(x - (margin&.left || 0), y - (margin&.bottom || 0),
|
176
259
|
x + width + (margin&.right || 0), @y)
|
177
260
|
when :flow
|
@@ -181,24 +264,25 @@ module HexaPDF
|
|
181
264
|
else
|
182
265
|
x = case box.style.position_hint
|
183
266
|
when :right
|
184
|
-
@x +
|
267
|
+
@x + @fit_data.margin_left + @fit_data.available_width - width
|
185
268
|
when :center
|
186
|
-
max_margin = [
|
269
|
+
max_margin = [@fit_data.margin_left, @fit_data.margin_right].max
|
187
270
|
# If we have enough space left for equal margins, we center perfectly
|
188
271
|
if available_width - width >= 2 * max_margin
|
189
272
|
@x + (available_width - width) / 2.0
|
190
273
|
else
|
191
|
-
@x +
|
274
|
+
@x + @fit_data.margin_left + (@fit_data.available_width - width) / 2.0
|
192
275
|
end
|
193
276
|
else
|
194
|
-
@x +
|
277
|
+
@x + @fit_data.margin_left
|
195
278
|
end
|
196
|
-
y = @y - height -
|
279
|
+
y = @y - height - @fit_data.margin_top
|
197
280
|
rectangle = create_rectangle(left, y - (margin&.bottom || 0), left + self.width, @y)
|
198
281
|
end
|
199
282
|
|
200
283
|
box.draw(canvas, x, y)
|
201
284
|
remove_area(rectangle)
|
285
|
+
@fit_data.reset
|
202
286
|
|
203
287
|
true
|
204
288
|
end
|
@@ -229,6 +313,7 @@ module HexaPDF
|
|
229
313
|
trim_shape
|
230
314
|
end
|
231
315
|
|
316
|
+
@fit_data.reset
|
232
317
|
available_width != 0
|
233
318
|
end
|
234
319
|
|
@@ -236,12 +321,24 @@ module HexaPDF
|
|
236
321
|
# line.
|
237
322
|
def remove_area(polygon)
|
238
323
|
@shape = Geom2D::Algorithms::PolygonOperation.run(@shape, polygon, :difference)
|
239
|
-
|
240
|
-
|
324
|
+
if @contour_line
|
325
|
+
@contour_line = Geom2D::Algorithms::PolygonOperation.run(@contour_line, polygon,
|
326
|
+
:difference)
|
327
|
+
end
|
241
328
|
@region_selection = :max_width
|
242
329
|
find_next_region
|
243
330
|
end
|
244
331
|
|
332
|
+
# Returns +true+ if the frame has no more space left.
|
333
|
+
def full?
|
334
|
+
available_width == 0
|
335
|
+
end
|
336
|
+
|
337
|
+
# The contour line of the frame, a Geom2D::PolygonSet consisting of arbitrary polygons.
|
338
|
+
def contour_line
|
339
|
+
@contour_line || @shape
|
340
|
+
end
|
341
|
+
|
245
342
|
# Returns a width specification for the frame's contour line that can be used, for example,
|
246
343
|
# with TextLayouter.
|
247
344
|
#
|
@@ -255,7 +352,7 @@ module HexaPDF
|
|
255
352
|
# Depending on the complexity of the frame, the result may be any of the allowed width
|
256
353
|
# specifications of TextLayouter#fit.
|
257
354
|
def width_specification(offset = 0)
|
258
|
-
WidthFromPolygon.new(
|
355
|
+
WidthFromPolygon.new(contour_line, offset)
|
259
356
|
end
|
260
357
|
|
261
358
|
private
|