hexapdf 0.8.0 → 0.9.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 +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
|