hexapdf 0.46.0 → 1.0.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 +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']
|