hexapdf 0.43.0 → 0.44.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 +17 -0
- data/examples/030-pdfa.rb +1 -0
- data/lib/hexapdf/composer.rb +1 -0
- data/lib/hexapdf/document/files.rb +7 -2
- data/lib/hexapdf/document/metadata.rb +12 -1
- data/lib/hexapdf/layout/box.rb +160 -61
- data/lib/hexapdf/layout/box_fitter.rb +1 -0
- data/lib/hexapdf/layout/column_box.rb +22 -24
- data/lib/hexapdf/layout/container_box.rb +2 -2
- data/lib/hexapdf/layout/frame.rb +13 -95
- data/lib/hexapdf/layout/image_box.rb +4 -4
- data/lib/hexapdf/layout/list_box.rb +11 -19
- data/lib/hexapdf/layout/style.rb +5 -1
- data/lib/hexapdf/layout/table_box.rb +48 -55
- data/lib/hexapdf/layout/text_box.rb +27 -42
- data/lib/hexapdf/parser.rb +5 -2
- data/lib/hexapdf/type/file_specification.rb +9 -5
- data/lib/hexapdf/type/graphics_state_parameter.rb +1 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/document/test_files.rb +5 -0
- data/test/hexapdf/document/test_metadata.rb +21 -0
- data/test/hexapdf/layout/test_box.rb +82 -37
- data/test/hexapdf/layout/test_box_fitter.rb +7 -0
- data/test/hexapdf/layout/test_column_box.rb +7 -13
- data/test/hexapdf/layout/test_container_box.rb +1 -1
- data/test/hexapdf/layout/test_frame.rb +0 -48
- data/test/hexapdf/layout/test_image_box.rb +14 -6
- data/test/hexapdf/layout/test_list_box.rb +25 -26
- data/test/hexapdf/layout/test_table_box.rb +39 -53
- data/test/hexapdf/layout/test_text_box.rb +38 -66
- data/test/hexapdf/test_composer.rb +6 -0
- data/test/hexapdf/test_parser.rb +8 -0
- data/test/hexapdf/type/test_file_specification.rb +2 -1
- metadata +2 -2
data/lib/hexapdf/layout/frame.rb
CHANGED
@@ -45,7 +45,7 @@ module HexaPDF
|
|
45
45
|
#
|
46
46
|
# == Usage
|
47
47
|
#
|
48
|
-
# After a Frame object is initialized, it is ready for
|
48
|
+
# After a Frame object is initialized, it is ready for fitting boxes in it and drawing them.
|
49
49
|
#
|
50
50
|
# The explicit way of drawing a box follows these steps:
|
51
51
|
#
|
@@ -54,7 +54,7 @@ module HexaPDF
|
|
54
54
|
#
|
55
55
|
# The method #fit is also called for absolutely positioned boxes but since these boxes are not
|
56
56
|
# subject to the normal constraints, the provided available width and height are the width and
|
57
|
-
# height inside the frame to the right and top of the bottom
|
57
|
+
# height inside the frame to the right and top of the bottom left corner of the box.
|
58
58
|
#
|
59
59
|
# * If the box didn't fit, call #find_next_region to determine the next region for placing the
|
60
60
|
# box. If a new region was found, start over with #fit. Otherwise the frame has no more space
|
@@ -65,10 +65,6 @@ module HexaPDF
|
|
65
65
|
# splitting is successful, the first box can be drawn (Make sure that the second box is
|
66
66
|
# handled correctly). Otherwise, start over with #find_next_region.
|
67
67
|
#
|
68
|
-
# For applications where splitting is not necessary, an easier way is to just use #draw and
|
69
|
-
# #find_next_region together, as #draw calls #fit if the box was not fit into the current
|
70
|
-
# region.
|
71
|
-
#
|
72
68
|
# == Used Box Properties
|
73
69
|
#
|
74
70
|
# The style properties 'position', 'align', 'valign', 'margin' and 'mask_mode' are taken into
|
@@ -88,84 +84,10 @@ module HexaPDF
|
|
88
84
|
|
89
85
|
include HexaPDF::Utils
|
90
86
|
|
91
|
-
#
|
92
|
-
class FitResult
|
93
|
-
|
94
|
-
# The frame into which the box was fitted.
|
95
|
-
attr_accessor :frame
|
96
|
-
|
97
|
-
# The box that was fitted into the frame.
|
98
|
-
attr_accessor :box
|
99
|
-
|
100
|
-
# The horizontal position where the box will be drawn.
|
101
|
-
attr_accessor :x
|
102
|
-
|
103
|
-
# The vertical position where the box will be drawn.
|
104
|
-
attr_accessor :y
|
105
|
-
|
106
|
-
# The available width in the frame for this particular box.
|
107
|
-
attr_accessor :available_width
|
108
|
-
|
109
|
-
# The available height in the frame for this particular box.
|
110
|
-
attr_accessor :available_height
|
111
|
-
|
112
|
-
# The rectangle (a Geom2D::Rectangle object) that will be removed from the frame when
|
113
|
-
# drawing the box.
|
114
|
-
attr_accessor :mask
|
115
|
-
|
116
|
-
# Initialize the result object for the given frame and box.
|
117
|
-
def initialize(frame, box)
|
118
|
-
@frame = frame
|
119
|
-
@box = box
|
120
|
-
@available_width = 0
|
121
|
-
@available_height = 0
|
122
|
-
@success = false
|
123
|
-
end
|
124
|
-
|
125
|
-
# Marks the fitting status as success.
|
126
|
-
def success!
|
127
|
-
@success = true
|
128
|
-
end
|
129
|
-
|
130
|
-
# Returns +true+ if fitting was successful.
|
131
|
-
def success?
|
132
|
-
@success
|
133
|
-
end
|
134
|
-
|
135
|
-
# Draws the #box onto the canvas at (#x + *dx*, #y + *dy*).
|
136
|
-
#
|
137
|
-
# The relative offset (dx, dy) is useful when rendering results that were accumulated and
|
138
|
-
# then need to be moved because the container holding them changes its position.
|
139
|
-
#
|
140
|
-
# The configuration option "debug" can be used to add visual debug output with respect to
|
141
|
-
# box placement.
|
142
|
-
def draw(canvas, dx: 0, dy: 0)
|
143
|
-
doc = canvas.context.document
|
144
|
-
if doc.config['debug']
|
145
|
-
name = (frame.parent_boxes + [box]).map do |box|
|
146
|
-
box.class.to_s.sub(/.*::/, '')
|
147
|
-
end.join('-') << "##{box.object_id}"
|
148
|
-
name = "#{name} (#{(x + dx).to_i},#{(y + dy).to_i}-#{mask.width.to_i}x#{mask.height.to_i})"
|
149
|
-
ocg = doc.optional_content.ocg(name)
|
150
|
-
canvas.optional_content(ocg) do
|
151
|
-
canvas.translate(dx, dy) do
|
152
|
-
canvas.fill_color("green").stroke_color("darkgreen").
|
153
|
-
opacity(fill_alpha: 0.1, stroke_alpha: 0.2).
|
154
|
-
draw(:geom2d, object: mask, path_only: true).fill_stroke
|
155
|
-
end
|
156
|
-
end
|
157
|
-
page = "Page #{canvas.context.index + 1}" rescue "XObject"
|
158
|
-
doc.optional_content.default_configuration.add_ocg_to_ui(ocg, path: ['Debug', page])
|
159
|
-
end
|
160
|
-
box.draw(canvas, x + dx, y + dy)
|
161
|
-
end
|
162
|
-
|
163
|
-
end
|
164
|
-
|
165
|
-
# The x-coordinate of the bottom-left corner.
|
87
|
+
# The x-coordinate of the bottom left corner.
|
166
88
|
attr_reader :left
|
167
89
|
|
168
|
-
# The y-coordinate of the bottom
|
90
|
+
# The y-coordinate of the bottom left corner.
|
169
91
|
attr_reader :bottom
|
170
92
|
|
171
93
|
# The width of the frame.
|
@@ -253,16 +175,15 @@ module HexaPDF
|
|
253
175
|
@context&.document
|
254
176
|
end
|
255
177
|
|
256
|
-
# Fits the given box into the current region of available space and returns
|
257
|
-
# object.
|
178
|
+
# Fits the given box into the current region of available space and returns the associated
|
179
|
+
# Box::FitResult object.
|
258
180
|
#
|
259
181
|
# Fitting a box takes the style properties 'position', 'align', 'valign', 'margin', and
|
260
182
|
# 'mask_mode' into account.
|
261
183
|
#
|
262
|
-
# Use the FitResult#success? method to determine whether fitting was successful.
|
184
|
+
# Use the Box::FitResult#success? method to determine whether fitting was successful.
|
263
185
|
def fit(box)
|
264
|
-
|
265
|
-
return fit_result if full?
|
186
|
+
return Box::FitResult.new(box, frame: self) if full?
|
266
187
|
|
267
188
|
margin = box.style.margin if box.style.margin?
|
268
189
|
|
@@ -277,7 +198,7 @@ module HexaPDF
|
|
277
198
|
|
278
199
|
aw = width - x
|
279
200
|
ah = height - y
|
280
|
-
box.fit(aw, ah, self)
|
201
|
+
fit_result = box.fit(aw, ah, self)
|
281
202
|
fit_result.success!
|
282
203
|
|
283
204
|
x += left
|
@@ -294,7 +215,7 @@ module HexaPDF
|
|
294
215
|
ah -= margin_top = margin.top unless float_equal(@y, @bottom + @height)
|
295
216
|
end
|
296
217
|
|
297
|
-
fit_result
|
218
|
+
fit_result = box.fit(aw, ah, self)
|
298
219
|
|
299
220
|
width = box.width
|
300
221
|
height = box.height
|
@@ -372,27 +293,24 @@ module HexaPDF
|
|
372
293
|
create_rectangle(@x, @y - available_height, @x + available_width, @y)
|
373
294
|
end
|
374
295
|
|
375
|
-
fit_result.available_width = aw
|
376
|
-
fit_result.available_height = ah
|
377
296
|
fit_result.x = x
|
378
297
|
fit_result.y = y
|
379
298
|
fit_result.mask = rectangle
|
380
299
|
fit_result
|
381
300
|
end
|
382
301
|
|
383
|
-
# Tries to split the box of the given FitResult into two parts and returns both parts.
|
302
|
+
# Tries to split the box of the given Box::FitResult into two parts and returns both parts.
|
384
303
|
#
|
385
304
|
# See Box#split for further details.
|
386
305
|
def split(fit_result)
|
387
|
-
fit_result.box.split
|
306
|
+
fit_result.box.split
|
388
307
|
end
|
389
308
|
|
390
|
-
# Draws the box of the given FitResult onto the canvas at the fitted position.
|
309
|
+
# Draws the box of the given Box::FitResult onto the canvas at the fitted position.
|
391
310
|
#
|
392
311
|
# After a box is successfully drawn, the frame's shape is adjusted to remove the occupied
|
393
312
|
# area.
|
394
313
|
def draw(canvas, fit_result)
|
395
|
-
return if fit_result.box.height == 0 || fit_result.box.width == 0
|
396
314
|
fit_result.draw(canvas)
|
397
315
|
remove_area(fit_result.mask)
|
398
316
|
end
|
@@ -79,9 +79,11 @@ module HexaPDF
|
|
79
79
|
false
|
80
80
|
end
|
81
81
|
|
82
|
+
private
|
83
|
+
|
82
84
|
# Fits the image into the current region of the frame, taking the initially set width and
|
83
85
|
# height into account (see the class description for details).
|
84
|
-
def
|
86
|
+
def fit_content(available_width, available_height, _frame)
|
85
87
|
image_width = @image.width.to_f
|
86
88
|
image_height = @image.height.to_f
|
87
89
|
image_ratio = image_width / image_height
|
@@ -103,12 +105,10 @@ module HexaPDF
|
|
103
105
|
@height = image_height * ratio + rh
|
104
106
|
end
|
105
107
|
|
106
|
-
|
108
|
+
fit_result.success! if float_compare(@width, available_width) <= 0 &&
|
107
109
|
float_compare(@height, available_height) <= 0
|
108
110
|
end
|
109
111
|
|
110
|
-
private
|
111
|
-
|
112
112
|
# Draws the image onto the canvas at position [x, y].
|
113
113
|
def draw_content(canvas, x, y)
|
114
114
|
canvas.image(@image, at: [x, y], width: content_width, height: content_height)
|
@@ -186,8 +186,10 @@ module HexaPDF
|
|
186
186
|
super && (!@results || @results.all? {|result| result.box_fitter.fit_results.empty? })
|
187
187
|
end
|
188
188
|
|
189
|
+
private
|
190
|
+
|
189
191
|
# Fits the list box into the current region of the frame.
|
190
|
-
def
|
192
|
+
def fit_content(available_width, available_height, frame)
|
191
193
|
@width = if @initial_width > 0
|
192
194
|
@initial_width
|
193
195
|
else
|
@@ -253,15 +255,13 @@ module HexaPDF
|
|
253
255
|
|
254
256
|
@height = @results.sum(&:height) + (@results.count - 1) * item_spacing + reserved_height
|
255
257
|
|
256
|
-
@
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
258
|
+
if @results.size == @children.size && @results.all? {|r| r.box_fitter.success? }
|
259
|
+
fit_result.success!
|
260
|
+
elsif !@results.empty? && !@results[0].box_fitter.fit_results.empty?
|
261
|
+
fit_result.overflow!
|
262
|
+
end
|
261
263
|
end
|
262
264
|
|
263
|
-
private
|
264
|
-
|
265
265
|
# Removes the +content_indentation+ from the left side of the given shape (a Geom2D::PolygonSet).
|
266
266
|
def remove_indent_from_frame_shape(shape)
|
267
267
|
polygon_index = 0
|
@@ -307,7 +307,7 @@ module HexaPDF
|
|
307
307
|
end
|
308
308
|
|
309
309
|
# Splits the content of the list box. This method is called from Box#split.
|
310
|
-
def split_content
|
310
|
+
def split_content
|
311
311
|
remaining_boxes = @results[-1].box_fitter.remaining_boxes
|
312
312
|
first_is_split_box = !remaining_boxes.empty?
|
313
313
|
children = (remaining_boxes.empty? ? [] : [remaining_boxes]) + @children[@results.size..-1]
|
@@ -361,17 +361,9 @@ module HexaPDF
|
|
361
361
|
|
362
362
|
# Draws the list items onto the canvas at position [x, y].
|
363
363
|
def draw_content(canvas, x, y)
|
364
|
-
|
365
|
-
raise HexaPDF::Error, "Some items don't fit into box with limited height and " \
|
366
|
-
"style property overflow is set to :error"
|
367
|
-
end
|
364
|
+
translate = style.position != :flow && (x != @fit_x || y != @fit_y)
|
368
365
|
|
369
|
-
translate
|
370
|
-
|
371
|
-
if translate
|
372
|
-
canvas.save_graphics_state
|
373
|
-
canvas.translate(x - @draw_pos_x, y - @draw_pos_y)
|
374
|
-
end
|
366
|
+
canvas.save_graphics_state.translate(x - @fit_x, y - @fit_y) if translate
|
375
367
|
|
376
368
|
@results.each do |item_result|
|
377
369
|
box_fitter = item_result.box_fitter
|
data/lib/hexapdf/layout/style.rb
CHANGED
@@ -1254,7 +1254,11 @@ module HexaPDF
|
|
1254
1254
|
# doesn't. If a box doesn't support this value, it is positioned as if the value :default
|
1255
1255
|
# was set.
|
1256
1256
|
#
|
1257
|
-
#
|
1257
|
+
# Notes:
|
1258
|
+
#
|
1259
|
+
# * The properties #align and #valign are not used with this value.
|
1260
|
+
# * The rectangular area of the box is the rectangle containing all the flowed content.
|
1261
|
+
# That rectangle is used for drawing the border, background and so on.
|
1258
1262
|
#
|
1259
1263
|
# Examples:
|
1260
1264
|
#
|
@@ -211,21 +211,27 @@ module HexaPDF
|
|
211
211
|
@height = height
|
212
212
|
end
|
213
213
|
|
214
|
+
# :nodoc:
|
215
|
+
def inspect
|
216
|
+
"<Cell (#{row},#{column}) #{row_span}x#{col_span} #{Array(children).map(&:class)}>"
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
|
214
221
|
# Fits the children of the table cell into the given rectangular area.
|
215
|
-
def
|
216
|
-
@width = available_width
|
222
|
+
def fit_content(available_width, available_height, frame)
|
217
223
|
width = available_width - reserved_width
|
218
|
-
height = available_height - reserved_height
|
219
|
-
return
|
224
|
+
height = @used_height = available_height - reserved_height
|
225
|
+
return if width <= 0 || height <= 0
|
220
226
|
|
221
227
|
frame = frame.child_frame(0, 0, width, height, box: self)
|
222
228
|
case children
|
223
229
|
when Box
|
224
|
-
|
225
|
-
@preferred_width =
|
226
|
-
@height = @preferred_height =
|
227
|
-
@fit_results = [
|
228
|
-
|
230
|
+
child_result = frame.fit(children)
|
231
|
+
@preferred_width = child_result.x + child_result.box.width + reserved_width
|
232
|
+
@height = @preferred_height = child_result.box.height + reserved_height
|
233
|
+
@fit_results = [child_result]
|
234
|
+
fit_result.success! if child_result.success?
|
229
235
|
when Array
|
230
236
|
box_fitter = BoxFitter.new([frame])
|
231
237
|
children.each {|box| box_fitter.fit(box) }
|
@@ -233,34 +239,23 @@ module HexaPDF
|
|
233
239
|
@preferred_width = max_x_result.x + max_x_result.box.width + reserved_width
|
234
240
|
@height = @preferred_height = box_fitter.content_heights[0] + reserved_height
|
235
241
|
@fit_results = box_fitter.fit_results
|
236
|
-
|
242
|
+
fit_result.success! if box_fitter.success?
|
237
243
|
else
|
238
244
|
@preferred_width = reserved_width
|
239
245
|
@height = @preferred_height = reserved_height
|
240
246
|
@fit_results = []
|
241
|
-
|
247
|
+
fit_result.success!
|
242
248
|
end
|
243
249
|
end
|
244
250
|
|
245
|
-
# :nodoc:
|
246
|
-
def inspect
|
247
|
-
"<Cell (#{row},#{column}) #{row_span}x#{col_span} #{Array(children).map(&:class)}>"
|
248
|
-
end
|
249
|
-
|
250
|
-
private
|
251
|
-
|
252
251
|
# Draws the content of the cell.
|
253
252
|
def draw_content(canvas, x, y)
|
254
253
|
return if @fit_results.empty?
|
255
254
|
|
256
255
|
# available_width is always equal to content_width but we need to adjust for the
|
257
256
|
# difference in the y direction between fitting and drawing
|
258
|
-
y -= (@
|
259
|
-
@fit_results.each
|
260
|
-
#fit_result.x += x
|
261
|
-
#fit_result.y += y
|
262
|
-
fit_result.draw(canvas, dx: x, dy: y)
|
263
|
-
end
|
257
|
+
y -= (@used_height - content_height)
|
258
|
+
@fit_results.each {|fit_result| fit_result.draw(canvas, dx: x, dy: y) }
|
264
259
|
end
|
265
260
|
|
266
261
|
end
|
@@ -393,7 +388,7 @@ module HexaPDF
|
|
393
388
|
else
|
394
389
|
column_info[cell.column].last
|
395
390
|
end
|
396
|
-
unless cell.fit(available_cell_width, available_height, frame)
|
391
|
+
unless cell.fit(available_cell_width, available_height, frame).success?
|
397
392
|
row_fit = false
|
398
393
|
break
|
399
394
|
end
|
@@ -589,24 +584,23 @@ module HexaPDF
|
|
589
584
|
super && (!@last_fitted_row_index || @last_fitted_row_index < 0)
|
590
585
|
end
|
591
586
|
|
592
|
-
|
593
|
-
def fit(available_width, available_height, frame)
|
594
|
-
return false if (@initial_width > 0 && @initial_width > available_width) ||
|
595
|
-
(@initial_height > 0 && @initial_height > available_height)
|
587
|
+
private
|
596
588
|
|
589
|
+
# Fits the table into the current region of the frame.
|
590
|
+
def fit_content(_available_width, _available_height, frame)
|
597
591
|
# Adjust reserved width/height to include space used by the edge cells for their border
|
598
592
|
# since cell borders are drawn on the bounds and not inside.
|
599
|
-
# This uses the top
|
593
|
+
# This uses the top left and bottom right cells and so might not be correct in all cases.
|
600
594
|
@cell_tl_border_width = @cells[0, 0].style.border.width
|
601
595
|
cell_br_border_width = @cells[-1, -1].style.border.width
|
602
|
-
rw =
|
603
|
-
rh =
|
596
|
+
rw = (@cell_tl_border_width.left + cell_br_border_width.right) / 2.0
|
597
|
+
rh = (@cell_tl_border_width.top + cell_br_border_width.bottom) / 2.0
|
604
598
|
|
605
|
-
width =
|
606
|
-
height =
|
599
|
+
width = @width - reserved_width - rw
|
600
|
+
height = @height - reserved_height - rh
|
607
601
|
used_height = 0
|
608
602
|
columns = calculate_column_widths(width)
|
609
|
-
return
|
603
|
+
return if columns.empty?
|
610
604
|
|
611
605
|
frame = frame.child_frame(box: self)
|
612
606
|
@special_cells_fit_not_successful = false
|
@@ -616,18 +610,21 @@ module HexaPDF
|
|
616
610
|
height -= special_used_height
|
617
611
|
used_height += special_used_height
|
618
612
|
@special_cells_fit_not_successful = (last_fitted_row_index != special_cells.number_of_rows - 1)
|
619
|
-
return
|
613
|
+
return nil if @special_cells_fit_not_successful
|
620
614
|
end
|
621
615
|
|
622
616
|
main_used_height, @last_fitted_row_index = @cells.fit_rows(@start_row_index, height, columns, frame)
|
623
617
|
used_height += main_used_height
|
624
618
|
|
625
|
-
|
626
|
-
|
627
|
-
@fit_successful = (@last_fitted_row_index == @cells.number_of_rows - 1)
|
628
|
-
end
|
619
|
+
update_content_width { columns[-1].sum + rw }
|
620
|
+
update_content_height { used_height + rh }
|
629
621
|
|
630
|
-
|
622
|
+
if @last_fitted_row_index == @cells.number_of_rows - 1
|
623
|
+
fit_result.success!
|
624
|
+
elsif @last_fitted_row_index >= 0
|
625
|
+
fit_result.overflow!
|
626
|
+
end
|
627
|
+
end
|
631
628
|
|
632
629
|
# Calculates and returns the x-coordinates and widths of all columns based on the given total
|
633
630
|
# available width.
|
@@ -649,20 +646,16 @@ module HexaPDF
|
|
649
646
|
end
|
650
647
|
|
651
648
|
# Splits the content of the table box. This method is called from Box#split.
|
652
|
-
def split_content
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
footer_cells = @footer ? Cells.new(@footer.call(self), cell_style: @cell_style) : nil
|
663
|
-
box.instance_variable_set(:@footer_cells, footer_cells)
|
664
|
-
[self, box]
|
665
|
-
end
|
649
|
+
def split_content
|
650
|
+
box = create_split_box
|
651
|
+
box.instance_variable_set(:@start_row_index, @last_fitted_row_index + 1)
|
652
|
+
box.instance_variable_set(:@last_fitted_row_index, -1)
|
653
|
+
box.instance_variable_set(:@special_cells_fit_not_successful, nil)
|
654
|
+
header_cells = @header ? Cells.new(@header.call(self), cell_style: @cell_style) : nil
|
655
|
+
box.instance_variable_set(:@header_cells, header_cells)
|
656
|
+
footer_cells = @footer ? Cells.new(@footer.call(self), cell_style: @cell_style) : nil
|
657
|
+
box.instance_variable_set(:@footer_cells, footer_cells)
|
658
|
+
[self, box]
|
666
659
|
end
|
667
660
|
|
668
661
|
# Draws the child boxes onto the canvas at position [x, y].
|
@@ -43,6 +43,11 @@ module HexaPDF
|
|
43
43
|
# objects of a Frame.
|
44
44
|
#
|
45
45
|
# This class uses TextLayouter behind the scenes to do the hard work.
|
46
|
+
#
|
47
|
+
# == Used Box Properties
|
48
|
+
#
|
49
|
+
# The spacing after the last line can be controlled via the style property +last_line_gap+. Also
|
50
|
+
# see TextLayouter#style for other style properties taken into account.
|
46
51
|
class TextBox < Box
|
47
52
|
|
48
53
|
# Creates a new TextBox object with the given inline items (e.g. TextFragment and InlineBox
|
@@ -67,21 +72,27 @@ module HexaPDF
|
|
67
72
|
true
|
68
73
|
end
|
69
74
|
|
75
|
+
# :nodoc:
|
76
|
+
def draw(canvas, x, y)
|
77
|
+
super(canvas, x + @x_offset, y)
|
78
|
+
end
|
79
|
+
|
80
|
+
# :nodoc:
|
81
|
+
def empty?
|
82
|
+
super && (!@result || @result.lines.empty?)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
70
87
|
# Fits the text box into the Frame.
|
71
88
|
#
|
72
89
|
# Depending on the 'position' style property, the text is either fit into the current region
|
73
90
|
# of the frame using +available_width+ and +available_height+, or fit to the shape of the
|
74
91
|
# frame starting from the top (when 'position' is set to :flow).
|
75
|
-
|
76
|
-
# The spacing after the last line can be controlled via the style property +last_line_gap+.
|
77
|
-
#
|
78
|
-
# Also see TextLayouter#style for other style properties taken into account.
|
79
|
-
def fit(available_width, available_height, frame)
|
80
|
-
return false if (@initial_width > 0 && @initial_width > available_width) ||
|
81
|
-
(@initial_height > 0 && @initial_height > available_height)
|
82
|
-
|
92
|
+
def fit_content(available_width, available_height, frame)
|
83
93
|
frame = frame.child_frame(box: self)
|
84
94
|
@width = @x_offset = @height = 0
|
95
|
+
|
85
96
|
@result = if style.position == :flow
|
86
97
|
@tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
|
87
98
|
apply_first_text_indent: !split_box?, frame: frame)
|
@@ -92,6 +103,7 @@ module HexaPDF
|
|
92
103
|
height = (@initial_height > 0 ? @initial_height : available_height) - @height
|
93
104
|
@tl.fit(@items, width, height, apply_first_text_indent: !split_box?, frame: frame)
|
94
105
|
end
|
106
|
+
|
95
107
|
@width += if @initial_width > 0 || style.text_align == :center || style.text_align == :right
|
96
108
|
width
|
97
109
|
elsif style.position == :flow
|
@@ -114,47 +126,20 @@ module HexaPDF
|
|
114
126
|
@height += style.line_spacing.gap(@result.lines.last, @result.lines.last)
|
115
127
|
end
|
116
128
|
|
117
|
-
@result.status == :success
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
# Splits the text box into two boxes if necessary and possible.
|
122
|
-
def split(available_width, available_height, frame)
|
123
|
-
fit(available_width, available_height, frame) unless @result
|
124
|
-
|
125
|
-
if style.position != :flow && (float_compare(@width, available_width) > 0 ||
|
126
|
-
float_compare(@height, available_height) > 0)
|
127
|
-
[nil, self]
|
128
|
-
elsif @result.remaining_items.empty?
|
129
|
-
[self]
|
130
|
-
elsif @result.lines.empty?
|
131
|
-
[nil, self]
|
132
|
-
else
|
133
|
-
[self, create_box_for_remaining_items]
|
129
|
+
if @result.status == :success
|
130
|
+
fit_result.success!
|
131
|
+
elsif @result.status == :height && !@result.lines.empty?
|
132
|
+
fit_result.overflow!
|
134
133
|
end
|
135
134
|
end
|
136
135
|
|
137
|
-
#
|
138
|
-
def
|
139
|
-
|
136
|
+
# Splits the text box into two.
|
137
|
+
def split_content
|
138
|
+
[self, create_box_for_remaining_items]
|
140
139
|
end
|
141
140
|
|
142
|
-
# :nodoc:
|
143
|
-
def empty?
|
144
|
-
super && (!@result || @result.lines.empty?)
|
145
|
-
end
|
146
|
-
|
147
|
-
private
|
148
|
-
|
149
141
|
# Draws the text into the box.
|
150
142
|
def draw_content(canvas, x, y)
|
151
|
-
return unless @result
|
152
|
-
|
153
|
-
if @result.status == :height && @initial_height > 0 && style.overflow == :error
|
154
|
-
raise HexaPDF::Error, "Text doesn't fit into box with limited height and " \
|
155
|
-
"style property overflow is set to :error"
|
156
|
-
end
|
157
|
-
|
158
143
|
return if @result.lines.empty?
|
159
144
|
@result.draw(canvas, x - @x_offset, y + content_height)
|
160
145
|
end
|
data/lib/hexapdf/parser.rb
CHANGED
@@ -365,11 +365,14 @@ module HexaPDF
|
|
365
365
|
# Need to iterate through the whole lines array in case there are multiple %%EOF to try
|
366
366
|
eof_index = 0
|
367
367
|
while (eof_index = lines[0..(eof_index - 1)].rindex {|l| l.strip == '%%EOF' })
|
368
|
-
if lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
|
368
|
+
if eof_index > 0 && lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
|
369
369
|
startxref_offset = $1.to_i
|
370
370
|
startxref_mangled = true
|
371
371
|
break # we found it even if it the syntax is not entirely correct
|
372
|
-
elsif eof_index < 2
|
372
|
+
elsif eof_index < 2
|
373
|
+
startxref_missing = true
|
374
|
+
break
|
375
|
+
elsif lines[eof_index - 2].strip != "startxref"
|
373
376
|
startxref_missing = true
|
374
377
|
else
|
375
378
|
startxref_offset = lines[eof_index - 1].to_i
|
@@ -158,11 +158,11 @@ module HexaPDF
|
|
158
158
|
end
|
159
159
|
|
160
160
|
# :call-seq:
|
161
|
-
# file_spec.embed(filename, name: File.basename(filename), register: true) -> ef_stream
|
162
|
-
# file_spec.embed(io, name:, register: true) -> ef_stream
|
161
|
+
# file_spec.embed(filename, name: File.basename(filename), mime_type: nil, register: true) -> ef_stream
|
162
|
+
# file_spec.embed(io, name:, mime_type: nil, register: true) -> ef_stream
|
163
163
|
#
|
164
|
-
# Embeds the given file or IO stream into the PDF file, sets the path
|
165
|
-
# the created stream object.
|
164
|
+
# Embeds the given file or IO stream into the PDF file, sets the path and MIME type
|
165
|
+
# accordingly and returns the created stream object.
|
166
166
|
#
|
167
167
|
# If a file is given, the +name+ option defaults to the basename of the file. However, if an
|
168
168
|
# IO object is given, the +name+ argument is mandatory.
|
@@ -177,13 +177,16 @@ module HexaPDF
|
|
177
177
|
# name::
|
178
178
|
# The name that should be used as path value and when registering.
|
179
179
|
#
|
180
|
+
# mime_type::
|
181
|
+
# Optionally specifies the MIME type of the file.
|
182
|
+
#
|
180
183
|
# register::
|
181
184
|
# Specifies whether the embedded file will be added to the EmbeddedFiles name tree under
|
182
185
|
# the +name+. If the name is already taken, it's value is overwritten.
|
183
186
|
#
|
184
187
|
# The file has to be available until the PDF document gets written because reading and
|
185
188
|
# writing is done lazily.
|
186
|
-
def embed(file_or_io, name: nil, register: true)
|
189
|
+
def embed(file_or_io, name: nil, mime_type: nil, register: true)
|
187
190
|
name ||= File.basename(file_or_io) if file_or_io.kind_of?(String)
|
188
191
|
if name.nil?
|
189
192
|
raise ArgumentError, "The name argument is mandatory when given an IO object"
|
@@ -194,6 +197,7 @@ module HexaPDF
|
|
194
197
|
|
195
198
|
self[:EF] ||= {}
|
196
199
|
ef_stream = self[:EF][:UF] = self[:EF][:F] = document.add({Type: :EmbeddedFile})
|
200
|
+
ef_stream[:Subtype] = mime_type.to_sym if mime_type
|
197
201
|
stat = if file_or_io.kind_of?(String)
|
198
202
|
File.stat(file_or_io)
|
199
203
|
elsif file_or_io.respond_to?(:stat)
|
@@ -51,7 +51,7 @@ module HexaPDF
|
|
51
51
|
|
52
52
|
define_type :ExtGState
|
53
53
|
|
54
|
-
define_field :Type, type: Symbol,
|
54
|
+
define_field :Type, type: Symbol, default: type
|
55
55
|
define_field :LW, type: Numeric, version: "1.3"
|
56
56
|
define_field :LC, type: Integer, version: "1.3"
|
57
57
|
define_field :LJ, type: Integer, version: "1.3"
|
data/lib/hexapdf/version.rb
CHANGED
@@ -43,6 +43,11 @@ describe HexaPDF::Document::Files do
|
|
43
43
|
assert_equal('Some file', spec[:Desc])
|
44
44
|
end
|
45
45
|
|
46
|
+
it "optionally sets the MIME type of an embedded file" do
|
47
|
+
spec = @doc.files.add(@file.path, mime_type: 'application/pdf')
|
48
|
+
assert_equal(:'application/pdf', spec.embedded_file_stream[:Subtype])
|
49
|
+
end
|
50
|
+
|
46
51
|
it "requires the name argument when given an IO object" do
|
47
52
|
assert_raises(ArgumentError) { @doc.files.add(StringIO.new) }
|
48
53
|
end
|