hexapdf 0.46.0 → 1.0.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 +83 -16
- data/lib/hexapdf/composer.rb +7 -0
- data/lib/hexapdf/configuration.rb +13 -0
- data/lib/hexapdf/content/parser.rb +3 -1
- data/lib/hexapdf/digital_signature/cms_handler.rb +13 -0
- data/lib/hexapdf/digital_signature/signature.rb +1 -1
- data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -0
- data/lib/hexapdf/document.rb +14 -3
- data/lib/hexapdf/encryption/standard_security_handler.rb +32 -26
- data/lib/hexapdf/font/cmap/writer.rb +58 -4
- data/lib/hexapdf/font/cmap.rb +7 -0
- data/lib/hexapdf/font/true_type_wrapper.rb +41 -16
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/layout/table_box.rb +57 -10
- data/lib/hexapdf/layout/text_fragment.rb +2 -1
- data/lib/hexapdf/object.rb +1 -1
- data/lib/hexapdf/parser.rb +1 -1
- data/lib/hexapdf/reference.rb +1 -1
- data/lib/hexapdf/task/merge_acro_form.rb +164 -0
- data/lib/hexapdf/task/optimize.rb +4 -4
- data/lib/hexapdf/task.rb +1 -0
- data/lib/hexapdf/tokenizer.rb +2 -0
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +8 -4
- data/lib/hexapdf/type/acro_form/form.rb +14 -24
- data/lib/hexapdf/type/acro_form/signature_field.rb +18 -7
- data/lib/hexapdf/type/acro_form/variable_text_field.rb +12 -4
- data/lib/hexapdf/type/actions/go_to.rb +1 -0
- data/lib/hexapdf/type/actions/go_to_r.rb +1 -0
- data/lib/hexapdf/type/actions/launch.rb +5 -1
- data/lib/hexapdf/type/annotation.rb +6 -1
- data/lib/hexapdf/type/annotations/markup_annotation.rb +14 -1
- data/lib/hexapdf/type/annotations/widget.rb +4 -2
- data/lib/hexapdf/type/catalog.rb +3 -0
- data/lib/hexapdf/type/cid_font.rb +4 -1
- data/lib/hexapdf/type/file_specification.rb +17 -14
- data/lib/hexapdf/type/font_descriptor.rb +4 -3
- data/lib/hexapdf/type/font_simple.rb +3 -1
- data/lib/hexapdf/type/font_true_type.rb +2 -0
- data/lib/hexapdf/type/font_type0.rb +1 -1
- data/lib/hexapdf/type/font_type1.rb +7 -0
- data/lib/hexapdf/type/font_type3.rb +0 -1
- data/lib/hexapdf/type/form.rb +5 -2
- data/lib/hexapdf/type/graphics_state_parameter.rb +7 -4
- data/lib/hexapdf/type/image.rb +8 -4
- data/lib/hexapdf/type/info.rb +2 -2
- data/lib/hexapdf/type/mark_information.rb +2 -2
- data/lib/hexapdf/type/optional_content_configuration.rb +1 -1
- data/lib/hexapdf/type/optional_content_membership.rb +1 -1
- data/lib/hexapdf/type/page.rb +5 -3
- data/lib/hexapdf/type/resources.rb +6 -6
- data/lib/hexapdf/type/viewer_preferences.rb +4 -3
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +1 -0
- data/test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf +43 -0
- data/test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf +44 -0
- data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf +43 -0
- data/test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf +0 -0
- data/test/hexapdf/common_tokenizer_tests.rb +5 -0
- data/test/hexapdf/digital_signature/signing/test_default_handler.rb +6 -0
- data/test/hexapdf/digital_signature/test_cms_handler.rb +12 -7
- data/test/hexapdf/digital_signature/test_signature.rb +7 -0
- data/test/hexapdf/digital_signature/test_signatures.rb +12 -7
- data/test/hexapdf/encryption/test_standard_security_handler.rb +5 -2
- data/test/hexapdf/font/cmap/test_writer.rb +73 -16
- data/test/hexapdf/font/test_true_type_wrapper.rb +17 -3
- data/test/hexapdf/layout/test_list_box.rb +7 -7
- data/test/hexapdf/layout/test_table_box.rb +52 -0
- data/test/hexapdf/layout/test_text_fragment.rb +3 -3
- data/test/hexapdf/layout/test_text_layouter.rb +4 -2
- data/test/hexapdf/task/test_merge_acro_form.rb +104 -0
- data/test/hexapdf/task/test_optimize.rb +2 -0
- data/test/hexapdf/test_composer.rb +8 -0
- data/test/hexapdf/test_document.rb +12 -3
- data/test/hexapdf/test_importer.rb +7 -0
- data/test/hexapdf/test_parser.rb +7 -0
- data/test/hexapdf/test_writer.rb +19 -5
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +40 -23
- data/test/hexapdf/type/acro_form/test_form.rb +7 -8
- data/test/hexapdf/type/acro_form/test_signature_field.rb +3 -1
- data/test/hexapdf/type/acro_form/test_variable_text_field.rb +14 -1
- data/test/hexapdf/type/actions/test_launch.rb +6 -2
- data/test/hexapdf/type/annotations/test_widget.rb +4 -0
- data/test/hexapdf/type/test_font_type1.rb +5 -0
- data/test/hexapdf/type/test_form.rb +1 -1
- data/test/hexapdf/type/test_page.rb +7 -1
- metadata +8 -2
|
@@ -57,6 +57,10 @@ module HexaPDF
|
|
|
57
57
|
class TrueTypeWrapper
|
|
58
58
|
|
|
59
59
|
# Represents a single glyph of the wrapped font.
|
|
60
|
+
#
|
|
61
|
+
# Since some characters/strings may be mapped to the same glyph id by the font's builtin cmap
|
|
62
|
+
# table, it is possible that different Glyph instances with the same #id but different #str
|
|
63
|
+
# exist.
|
|
60
64
|
class Glyph
|
|
61
65
|
|
|
62
66
|
# The associated TrueTypeWrapper object.
|
|
@@ -152,6 +156,7 @@ module HexaPDF
|
|
|
152
156
|
@id_to_glyph = {}
|
|
153
157
|
@codepoint_to_glyph = {}
|
|
154
158
|
@encoded_glyphs = {}
|
|
159
|
+
@last_char_code = 0
|
|
155
160
|
end
|
|
156
161
|
|
|
157
162
|
# Returns the type of the font, i.e. :TrueType.
|
|
@@ -179,14 +184,15 @@ module HexaPDF
|
|
|
179
184
|
!@subsetter.nil?
|
|
180
185
|
end
|
|
181
186
|
|
|
182
|
-
# Returns a Glyph object for the given glyph ID.
|
|
187
|
+
# Returns a Glyph object for the given glyph ID and +str+ pair.
|
|
183
188
|
#
|
|
184
|
-
# The optional argument +str+ should be the string representation of the glyph.
|
|
185
|
-
#
|
|
189
|
+
# The optional argument +str+ should be the string representation of the glyph. It is possible
|
|
190
|
+
# that multiple strings map to the same glyph (e.g. hyphen and soft-hyphen could be
|
|
191
|
+
# represented by the same glyph).
|
|
186
192
|
#
|
|
187
193
|
# Note: Although this method is public, it should normally not be used by application code!
|
|
188
194
|
def glyph(id, str = nil)
|
|
189
|
-
@id_to_glyph[id] ||=
|
|
195
|
+
@id_to_glyph[[id, str]] ||=
|
|
190
196
|
if id >= 0 && id < @wrapped_font[:maxp].num_glyphs
|
|
191
197
|
Glyph.new(self, id, str || (+'' << (@cmap.gid_to_code(id) || 0xFFFD)))
|
|
192
198
|
else
|
|
@@ -228,14 +234,12 @@ module HexaPDF
|
|
|
228
234
|
|
|
229
235
|
# Encodes the glyph and returns the code string.
|
|
230
236
|
def encode(glyph)
|
|
231
|
-
(@encoded_glyphs[glyph
|
|
237
|
+
(@encoded_glyphs[glyph] ||=
|
|
232
238
|
begin
|
|
233
239
|
raise HexaPDF::MissingGlyphError.new(glyph) if glyph.kind_of?(InvalidGlyph)
|
|
234
|
-
if @subsetter
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
[[glyph.id].pack('n'), glyph]
|
|
238
|
-
end
|
|
240
|
+
@subsetter.use_glyph(glyph.id) if @subsetter
|
|
241
|
+
@last_char_code += 1
|
|
242
|
+
[[@last_char_code].pack('n'), @last_char_code]
|
|
239
243
|
end)[0]
|
|
240
244
|
end
|
|
241
245
|
|
|
@@ -286,7 +290,7 @@ module HexaPDF
|
|
|
286
290
|
Supplement: 0},
|
|
287
291
|
CIDToGIDMap: :Identity})
|
|
288
292
|
dict = document.add({Type: :Font, Subtype: :Type0, BaseFont: cid_font[:BaseFont],
|
|
289
|
-
|
|
293
|
+
DescendantFonts: [cid_font]})
|
|
290
294
|
dict.font_wrapper = self
|
|
291
295
|
|
|
292
296
|
document.register_listener(:complete_objects) do
|
|
@@ -294,6 +298,7 @@ module HexaPDF
|
|
|
294
298
|
embed_font(dict, document)
|
|
295
299
|
complete_width_information(dict)
|
|
296
300
|
create_to_unicode_cmap(dict, document)
|
|
301
|
+
add_encoding_information_cmap(dict, document)
|
|
297
302
|
end
|
|
298
303
|
|
|
299
304
|
dict
|
|
@@ -306,7 +311,7 @@ module HexaPDF
|
|
|
306
311
|
return unless @subsetter
|
|
307
312
|
|
|
308
313
|
tag = +''
|
|
309
|
-
data = @encoded_glyphs.each_with_object(''.b) {|(
|
|
314
|
+
data = @encoded_glyphs.each_with_object(''.b) {|(g, v), s| s << g.id.to_s << v[0] }
|
|
310
315
|
hash = Digest::MD5.hexdigest(data << @wrapped_font.font_name).to_i(16)
|
|
311
316
|
while hash != 0 && tag.length < 6
|
|
312
317
|
hash, mod = hash.divmod(UPPERCASE_LETTERS.length)
|
|
@@ -336,8 +341,8 @@ module HexaPDF
|
|
|
336
341
|
# Adds the /DW and /W fields to the CIDFont dictionary.
|
|
337
342
|
def complete_width_information(dict)
|
|
338
343
|
default_width = glyph(3, " ").width.to_i
|
|
339
|
-
widths = @encoded_glyphs.reject {|
|
|
340
|
-
[(@subsetter ? @subsetter.subset_glyph_id(id) : id),
|
|
344
|
+
widths = @encoded_glyphs.reject {|g, _| g.width == default_width }.map do |g, _|
|
|
345
|
+
[(@subsetter ? @subsetter.subset_glyph_id(g.id) : g.id), g.width]
|
|
341
346
|
end.sort!
|
|
342
347
|
dict[:DescendantFonts].first.set_widths(widths, default_width: default_width)
|
|
343
348
|
end
|
|
@@ -346,9 +351,10 @@ module HexaPDF
|
|
|
346
351
|
# correctly.
|
|
347
352
|
def create_to_unicode_cmap(dict, document)
|
|
348
353
|
stream = HexaPDF::StreamData.new do
|
|
349
|
-
mapping = @encoded_glyphs.
|
|
354
|
+
mapping = @encoded_glyphs.map do |glyph, (_, char_code)|
|
|
350
355
|
# Using 0xFFFD as mentioned in Adobe #5411, last line before section 1.5
|
|
351
|
-
|
|
356
|
+
# TODO: glyph.str assumed to consist of single char, No support for multiple chars
|
|
357
|
+
[char_code, glyph.str.ord || 0xFFFD]
|
|
352
358
|
end.sort_by!(&:first)
|
|
353
359
|
HexaPDF::Font::CMap.create_to_unicode_cmap(mapping)
|
|
354
360
|
end
|
|
@@ -357,6 +363,25 @@ module HexaPDF
|
|
|
357
363
|
dict[:ToUnicode] = stream_obj
|
|
358
364
|
end
|
|
359
365
|
|
|
366
|
+
# Adds the /Encoding entry to the +dict+.
|
|
367
|
+
#
|
|
368
|
+
# This can either be the identity mapping or, if some Unicode codepoints are mapped to the
|
|
369
|
+
# same glyph, a custom CMap.
|
|
370
|
+
def add_encoding_information_cmap(dict, document)
|
|
371
|
+
mapping = @encoded_glyphs.map do |glyph, (_, char_code)|
|
|
372
|
+
# Using 0xFFFD as mentioned in Adobe #5411, last line before section 1.5
|
|
373
|
+
[char_code, (@subsetter ? @subsetter.subset_glyph_id(glyph.id) : glyph.id)]
|
|
374
|
+
end.sort_by!(&:first)
|
|
375
|
+
if mapping.all? {|char_code, cid| char_code == cid }
|
|
376
|
+
dict[:Encoding] = :'Identity-H'
|
|
377
|
+
else
|
|
378
|
+
stream = HexaPDF::StreamData.new { HexaPDF::Font::CMap.create_cid_cmap(mapping) }
|
|
379
|
+
stream_obj = document.add({}, stream: stream)
|
|
380
|
+
stream_obj.set_filter(:FlateDecode)
|
|
381
|
+
dict[:Encoding] = stream_obj
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
360
385
|
end
|
|
361
386
|
|
|
362
387
|
end
|
data/lib/hexapdf/importer.rb
CHANGED
|
@@ -141,7 +141,7 @@ module HexaPDF
|
|
|
141
141
|
internal_import(wrapper.source.object(object), wrapper)
|
|
142
142
|
when HexaPDF::Object
|
|
143
143
|
wrapper.source ||= object.document
|
|
144
|
-
if !@allow_all && (object.type == :Catalog || object.type == :Pages)
|
|
144
|
+
if object.null? || (!@allow_all && (object.type == :Catalog || object.type == :Pages))
|
|
145
145
|
@mapper[object.data] = nil
|
|
146
146
|
elsif (mapped_object = @mapper[object.data]&.__getobj__) && !mapped_object.null?
|
|
147
147
|
mapped_object
|
|
@@ -382,7 +382,14 @@ module HexaPDF
|
|
|
382
382
|
def fit_rows(start_row, available_height, column_info, frame)
|
|
383
383
|
height = available_height
|
|
384
384
|
last_fitted_row_index = -1
|
|
385
|
+
row_heights = {}
|
|
386
|
+
zero_height_rows = {}
|
|
387
|
+
row_spans = []
|
|
388
|
+
|
|
385
389
|
@cells[start_row..-1].each.with_index(start_row) do |columns, row_index|
|
|
390
|
+
# 1. Fit all columns of the row and record the max height of all non-row-span cells. If
|
|
391
|
+
# a row has zero height (usually because it only has row-span cells), record that
|
|
392
|
+
# information. Additionally store all cells with row-spans.
|
|
386
393
|
row_fit = true
|
|
387
394
|
row_height = 0
|
|
388
395
|
columns.each_with_index do |cell, col_index|
|
|
@@ -396,27 +403,67 @@ module HexaPDF
|
|
|
396
403
|
row_fit = false
|
|
397
404
|
break
|
|
398
405
|
end
|
|
399
|
-
cell.
|
|
400
|
-
|
|
401
|
-
|
|
406
|
+
if row_height < cell.preferred_height && cell.row_span == 1
|
|
407
|
+
row_height = cell.preferred_height
|
|
408
|
+
end
|
|
409
|
+
row_spans << cell if cell.row_span > 1
|
|
402
410
|
end
|
|
403
411
|
|
|
404
|
-
if
|
|
405
|
-
seen = {}
|
|
406
|
-
columns.each do |cell|
|
|
407
|
-
next if seen[cell]
|
|
408
|
-
cell.update_height(cell.row == row_index ? row_height : cell.height + row_height)
|
|
409
|
-
seen[cell] = true
|
|
410
|
-
end
|
|
412
|
+
zero_height_rows[row_index] = true if row_height == 0
|
|
411
413
|
|
|
414
|
+
if row_fit
|
|
415
|
+
# 2. If all cells of the row fit, we subtract the recorded row height of the
|
|
416
|
+
# non-row-span cells from the available height for the next pass.
|
|
412
417
|
last_fitted_row_index = row_index
|
|
418
|
+
row_heights[row_index] = row_height
|
|
413
419
|
available_height -= row_height
|
|
420
|
+
|
|
421
|
+
# 3. We look at all row-span cells that end at the current row index. If the row-span
|
|
422
|
+
# cell is larger than the sum of the row heights, we proportionally enlarge the
|
|
423
|
+
# stored height of each spanned row and subtract the difference from the available
|
|
424
|
+
# height for the next pass. If the row span contains initially zero-height rows,
|
|
425
|
+
# only those rows are enlarged. Row-span cells themselves are not updated at this
|
|
426
|
+
# point!
|
|
427
|
+
row_spans.each do |cell|
|
|
428
|
+
upper_row_index = cell.row + cell.row_span - 1
|
|
429
|
+
next unless upper_row_index == row_index
|
|
430
|
+
|
|
431
|
+
rows = cell.row.upto(upper_row_index)
|
|
432
|
+
row_span_height = rows.sum {|ri| row_heights[ri] }
|
|
433
|
+
if row_span_height < cell.preferred_height
|
|
434
|
+
zero_height_rows_in_span = rows.select {|ri| zero_height_rows[ri] }
|
|
435
|
+
rows = zero_height_rows_in_span if zero_height_rows_in_span.size > 0
|
|
436
|
+
adjustment = (cell.preferred_height - row_span_height) / rows.size.to_f
|
|
437
|
+
rows.each {|ri| row_heights[ri] += adjustment }
|
|
438
|
+
available_height -= cell.preferred_height - row_span_height
|
|
439
|
+
end
|
|
440
|
+
end
|
|
414
441
|
else
|
|
415
442
|
last_fitted_row_index = columns.min_by(&:row).row - 1 if height != available_height
|
|
416
443
|
break
|
|
417
444
|
end
|
|
418
445
|
end
|
|
419
446
|
|
|
447
|
+
if last_fitted_row_index >= 0
|
|
448
|
+
# 4. Once all possible rows have been fitted and the heights of the rows are fixed, the
|
|
449
|
+
# final height and top-left corner of each cell needs to be set.
|
|
450
|
+
running_height = 0
|
|
451
|
+
@cells[start_row..last_fitted_row_index].each.with_index(start_row) do |columns, row_index|
|
|
452
|
+
columns.each_with_index do |cell, col_index|
|
|
453
|
+
next if cell.row != row_index || cell.column != col_index
|
|
454
|
+
cell.left = column_info[cell.column].first
|
|
455
|
+
cell.top = running_height
|
|
456
|
+
if cell.row_span == 1
|
|
457
|
+
cell.update_height(row_heights[row_index])
|
|
458
|
+
else
|
|
459
|
+
new_height = cell.row.upto(cell.row + cell.row_span - 1).sum {|ri| row_heights[ri] }
|
|
460
|
+
cell.update_height(new_height)
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
running_height += row_heights[row_index]
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
420
467
|
[height - available_height, last_fitted_row_index < start_row ? -1 : last_fitted_row_index]
|
|
421
468
|
end
|
|
422
469
|
|
|
@@ -235,6 +235,7 @@ module HexaPDF
|
|
|
235
235
|
end
|
|
236
236
|
end
|
|
237
237
|
|
|
238
|
+
in_text_object = (canvas.graphics_object == :text)
|
|
238
239
|
canvas.begin_text
|
|
239
240
|
tlm = canvas.graphics_state.tlm
|
|
240
241
|
tx = x - tlm.e
|
|
@@ -248,7 +249,7 @@ module HexaPDF
|
|
|
248
249
|
elsif ty.abs < PRECISION
|
|
249
250
|
canvas.move_text_cursor(offset: [tx, 0], absolute: false)
|
|
250
251
|
else
|
|
251
|
-
canvas.move_text_cursor(offset: [x, y])
|
|
252
|
+
canvas.move_text_cursor(offset: [x, y], absolute: in_text_object)
|
|
252
253
|
end
|
|
253
254
|
canvas.show_glyphs_only(items)
|
|
254
255
|
|
data/lib/hexapdf/object.rb
CHANGED
data/lib/hexapdf/parser.rb
CHANGED
data/lib/hexapdf/reference.rb
CHANGED
|
@@ -0,0 +1,164 @@
|
|
|
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-2024 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
|
+
|
|
37
|
+
require 'hexapdf/serializer'
|
|
38
|
+
|
|
39
|
+
module HexaPDF
|
|
40
|
+
module Task
|
|
41
|
+
|
|
42
|
+
# Task for merging an AcroForm from one PDF into another.
|
|
43
|
+
#
|
|
44
|
+
# It takes care of
|
|
45
|
+
#
|
|
46
|
+
# * adding the fields to the main Type::AcroForm::Form dictionary,
|
|
47
|
+
# * adjusting the field names so that they are unique,
|
|
48
|
+
# * and merging the properties of the main AcroForm dictionary itself and adjusting field
|
|
49
|
+
# information appropriately.
|
|
50
|
+
#
|
|
51
|
+
# Note that the pages with the fields need to be imported already.
|
|
52
|
+
#
|
|
53
|
+
# The steps for using this task are:
|
|
54
|
+
#
|
|
55
|
+
# 1. Import the pages into the target document and add all imported pages to an array
|
|
56
|
+
# 2. Call this task using the created array of pages.
|
|
57
|
+
#
|
|
58
|
+
# Example:
|
|
59
|
+
#
|
|
60
|
+
# pages = doc.pages.map {|page| target.pages.add(target.import(page)) }
|
|
61
|
+
# target.task(:merge_acro_form, source: doc, pages: pages)
|
|
62
|
+
module MergeAcroForm
|
|
63
|
+
|
|
64
|
+
# Performs the necessary steps to merge the AcroForm fields from the +source+ into the target
|
|
65
|
+
# document +doc+.
|
|
66
|
+
#
|
|
67
|
+
# +source+::
|
|
68
|
+
# Specifies the source PDF document the information from which should be merged into the
|
|
69
|
+
# target document.
|
|
70
|
+
#
|
|
71
|
+
# +pages+::
|
|
72
|
+
# An array of pages that were imported from +source+ and contain the widgets of the fields
|
|
73
|
+
# that should be merged.
|
|
74
|
+
def self.call(doc, source:, pages:)
|
|
75
|
+
return unless source.acro_form
|
|
76
|
+
|
|
77
|
+
acro_form = doc.acro_form(create: true)
|
|
78
|
+
|
|
79
|
+
# Determine a unique name for root field and create root field
|
|
80
|
+
import_name = 'merged_' +
|
|
81
|
+
(acro_form.root_fields.select {|field| field[:T] =~ /\Amerged_\d+\z/ }.
|
|
82
|
+
map {|field| field[:T][/\d+/].to_i }.sort.last || 0).succ.to_s
|
|
83
|
+
root_field = doc.add({T: import_name, Kids: []})
|
|
84
|
+
acro_form.root_fields << root_field
|
|
85
|
+
|
|
86
|
+
# Merge the main AcroForm dictionary
|
|
87
|
+
font_name_mapping = merge_form_dictionary(acro_form, source.acro_form, root_field)
|
|
88
|
+
font_name_re = font_name_mapping.keys.map {|name| Regexp.escape(name) }.join('|')
|
|
89
|
+
root_field[:DA] && root_field[:DA].sub!(font_name_re, font_name_mapping)
|
|
90
|
+
|
|
91
|
+
# Process all field widgets of the given pages
|
|
92
|
+
process_calculate_actions = false
|
|
93
|
+
signature_field_seen = false
|
|
94
|
+
pages.each do |page|
|
|
95
|
+
page.each_annotation do |widget|
|
|
96
|
+
next unless widget[:Subtype] == :Widget
|
|
97
|
+
field = widget.form_field
|
|
98
|
+
|
|
99
|
+
# Correct the font name in the default appearance string
|
|
100
|
+
widget[:DA] && widget[:DA].sub!(font_name_re, font_name_mapping)
|
|
101
|
+
field[:DA] && field[:DA].sub!(font_name_re, font_name_mapping)
|
|
102
|
+
|
|
103
|
+
process_calculate_actions = true if field[:AA]&.[](:C)
|
|
104
|
+
signature_field_seen = true if field.field_type == :Sig
|
|
105
|
+
|
|
106
|
+
# Add to the root field
|
|
107
|
+
field = field[:Parent] while field[:Parent]
|
|
108
|
+
if field != root_field
|
|
109
|
+
field[:Parent] = root_field
|
|
110
|
+
root_field[:Kids] << field
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Update calculation JavaScript actions with changed field names
|
|
116
|
+
fix_calculate_actions(acro_form, source.acro_form, import_name) if process_calculate_actions
|
|
117
|
+
|
|
118
|
+
# Update signature flags if necessary
|
|
119
|
+
if signature_field_seen && source.acro_form.signature_flag?(:signatures_exist)
|
|
120
|
+
acro_form.signature_flag(:signatures_exist)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Merges the AcroForm +source_form+ into the +target_form+ and returns a mapping of old font
|
|
125
|
+
# names to new ones.
|
|
126
|
+
def self.merge_form_dictionary(target_form, source_form, root_field)
|
|
127
|
+
target_resources = target_form.default_resources
|
|
128
|
+
font_name_mapping = {}
|
|
129
|
+
serializer = HexaPDF::Serializer.new
|
|
130
|
+
|
|
131
|
+
source_form.default_resources[:Font].each do |font_name, value|
|
|
132
|
+
new_name = target_resources.add_font(target_form.document.import(value))
|
|
133
|
+
font_name_mapping[serializer.serialize(font_name)] = serializer.serialize(new_name)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
root_field[:DA] = target_form.document.import(source_form[:DA])
|
|
137
|
+
root_field[:Q] = target_form.document.import(source_form[:Q])
|
|
138
|
+
|
|
139
|
+
font_name_mapping
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Fixes the calculate actions listed in the /CO entry of the main AcroForm dictionary to use
|
|
143
|
+
# the new names of the fields.
|
|
144
|
+
def self.fix_calculate_actions(acro_form, source_form, import_name)
|
|
145
|
+
if source_form[:CO]
|
|
146
|
+
acro_form[:CO] ||= []
|
|
147
|
+
acro_form[:CO].value.concat(acro_form.document.import(source_form[:CO]).value)
|
|
148
|
+
acro_form[:CO].each do |field|
|
|
149
|
+
next unless (action = field[:AA]&.[](:C))
|
|
150
|
+
action[:JS].gsub!(/"(.*?)"/) do |match|
|
|
151
|
+
if source_form.field_by_name($1)
|
|
152
|
+
"\"#{import_name}.#{$1}\""
|
|
153
|
+
else
|
|
154
|
+
match
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -214,13 +214,13 @@ module HexaPDF
|
|
|
214
214
|
end
|
|
215
215
|
end
|
|
216
216
|
|
|
217
|
-
# Deletes field entries of the object that are optional and currently set
|
|
218
|
-
# value.
|
|
217
|
+
# Deletes field entries (except for /Type) of the object that are optional and currently set
|
|
218
|
+
# to their default value.
|
|
219
219
|
def self.delete_fields_with_defaults(obj)
|
|
220
220
|
return unless obj.kind_of?(HexaPDF::Dictionary) && !obj.null?
|
|
221
221
|
obj.each do |name, value|
|
|
222
|
-
if (field = obj.class.field(name)) && !field.required? &&
|
|
223
|
-
|
|
222
|
+
if name != :Type && (field = obj.class.field(name)) && !field.required? &&
|
|
223
|
+
field.default? && value == field.default
|
|
224
224
|
obj.delete(name)
|
|
225
225
|
end
|
|
226
226
|
end
|
data/lib/hexapdf/task.rb
CHANGED
data/lib/hexapdf/tokenizer.rb
CHANGED
|
@@ -144,6 +144,8 @@ module HexaPDF
|
|
|
144
144
|
elsif byte == 93 # ]
|
|
145
145
|
@ss.pos += 1
|
|
146
146
|
TOKEN_ARRAY_END
|
|
147
|
+
elsif byte == 41 # )
|
|
148
|
+
raise HexaPDF::MalformedPDFError.new("Delimiter ')' found at invalid position", pos: pos)
|
|
147
149
|
elsif byte == 123 || byte == 125 # { }
|
|
148
150
|
Token.new(@ss.get_byte)
|
|
149
151
|
elsif byte == 37 # %
|
|
@@ -134,11 +134,12 @@ module HexaPDF
|
|
|
134
134
|
if !normal_appearance.kind_of?(HexaPDF::Dictionary) || normal_appearance.kind_of?(HexaPDF::Stream)
|
|
135
135
|
(@widget[:AP] ||= {})[:N] = {Off: nil}
|
|
136
136
|
normal_appearance = @widget[:AP][:N]
|
|
137
|
-
normal_appearance[@field
|
|
137
|
+
normal_appearance[@field.field_value&.to_sym || :Yes] = nil
|
|
138
138
|
end
|
|
139
139
|
on_name = (normal_appearance.value.keys - [:Off]).first
|
|
140
140
|
unless on_name
|
|
141
|
-
|
|
141
|
+
on_name = @field.field_value&.to_sym || :Yes
|
|
142
|
+
normal_appearance[on_name] = nil
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
@widget[:AS] = (@field[:V] == on_name ? on_name : :Off)
|
|
@@ -226,8 +227,11 @@ module HexaPDF
|
|
|
226
227
|
|
|
227
228
|
form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
|
|
228
229
|
# Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
|
|
229
|
-
# key; we can do this since we know this has to be a
|
|
230
|
-
|
|
230
|
+
# key or the type of the object is wrong; we can do this since we know this has to be a
|
|
231
|
+
# Form object
|
|
232
|
+
unless form.type == :XObject && form[:Subtype] == :Form
|
|
233
|
+
form = @document.wrap(form, type: :XObject, subtype: :Form)
|
|
234
|
+
end
|
|
231
235
|
form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
|
|
232
236
|
Matrix: matrix, Resources: HexaPDF::Object.deep_copy(default_resources)})
|
|
233
237
|
form.contents = ''
|
|
@@ -81,6 +81,7 @@ module HexaPDF
|
|
|
81
81
|
define_field :CO, type: PDFArray, version: '1.3'
|
|
82
82
|
define_field :DR, type: :XXResources
|
|
83
83
|
define_field :DA, type: String
|
|
84
|
+
define_field :Q, type: Integer
|
|
84
85
|
define_field :XFA, type: [Stream, PDFArray], version: '1.5'
|
|
85
86
|
|
|
86
87
|
bit_field(:signature_flags, {signatures_exist: 0, append_only: 1},
|
|
@@ -182,24 +183,18 @@ module HexaPDF
|
|
|
182
183
|
# The optional keyword arguments allow setting often used properties of the field:
|
|
183
184
|
#
|
|
184
185
|
# +font+::
|
|
185
|
-
# The font that should be used for the text of the field. If
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
# If no font is set on the text field, the default font properties of the AcroForm form
|
|
189
|
-
# are used. Note that field specific or form specific font properties have to be set.
|
|
190
|
-
# Otherwise there will be an error when trying to generate a visual representation of
|
|
191
|
-
# the field value.
|
|
186
|
+
# The font that should be used for the text of the field. If not specified, it
|
|
187
|
+
# defaults to Helvetica.
|
|
192
188
|
#
|
|
193
189
|
# +font_options+::
|
|
194
|
-
# A hash with font options like :variant that should be used.
|
|
190
|
+
# A hash with font options like :variant that should be used. If not specified, it
|
|
191
|
+
# defaults to the empty hash.
|
|
195
192
|
#
|
|
196
193
|
# +font_size+::
|
|
197
|
-
# The font size that should be used. If
|
|
198
|
-
# specified but +font_size+ isn't, font size defaults to 0 (= auto-sizing).
|
|
194
|
+
# The font size that should be used. If not specified, it defaults to 0 (= auto-sizing).
|
|
199
195
|
#
|
|
200
196
|
# +font_color+::
|
|
201
|
-
# The font color that should be used. If
|
|
202
|
-
# specified but +font_color+ isn't, font color defaults to 0 (i.e. black).
|
|
197
|
+
# The font color that should be used. If not specified, it defaults to 0 (i.e. black).
|
|
203
198
|
#
|
|
204
199
|
# +align+::
|
|
205
200
|
# The alignment of the text, either :left, :center or :right.
|
|
@@ -440,8 +435,7 @@ module HexaPDF
|
|
|
440
435
|
|
|
441
436
|
# Returns the dictionary containing the default resources for form field appearance streams.
|
|
442
437
|
def default_resources
|
|
443
|
-
self[:DR] ||= document.wrap({
|
|
444
|
-
type: :XXResources)
|
|
438
|
+
self[:DR] ||= document.wrap({}, type: :XXResources)
|
|
445
439
|
end
|
|
446
440
|
|
|
447
441
|
# Sets the global default appearance string using the provided values or the default values
|
|
@@ -527,7 +521,7 @@ module HexaPDF
|
|
|
527
521
|
field = Field.wrap(document, field)
|
|
528
522
|
next unless field && (calculation_action = field[:AA]&.[](:C))
|
|
529
523
|
result = JavaScriptActions.calculate(self, calculation_action)
|
|
530
|
-
field.
|
|
524
|
+
field.field_value = result if result
|
|
531
525
|
end
|
|
532
526
|
end
|
|
533
527
|
|
|
@@ -561,13 +555,11 @@ module HexaPDF
|
|
|
561
555
|
# Applies the given variable field properties to the field.
|
|
562
556
|
def apply_variable_text_properties(field, font: nil, font_options: nil, font_size: nil,
|
|
563
557
|
font_color: nil, align: nil)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
end
|
|
570
|
-
field.text_alignment(align) if align
|
|
558
|
+
field.set_default_appearance_string(font: font || 'Helvetica',
|
|
559
|
+
font_options: font_options || {},
|
|
560
|
+
font_size: font_size || 0,
|
|
561
|
+
font_color: font_color || 0)
|
|
562
|
+
field.text_alignment(align || :left)
|
|
571
563
|
end
|
|
572
564
|
|
|
573
565
|
def perform_validation # :nodoc:
|
|
@@ -625,8 +617,6 @@ module HexaPDF
|
|
|
625
617
|
if font_name && !(self[:DR][:Font] && self[:DR][:Font][font_name])
|
|
626
618
|
yield("The font specified in /DA is not in the /DR resource dictionary")
|
|
627
619
|
end
|
|
628
|
-
else
|
|
629
|
-
set_default_appearance_string
|
|
630
620
|
end
|
|
631
621
|
|
|
632
622
|
create_appearances if document.config['acro_form.create_appearances']
|