hexapdf 0.46.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -16
  3. data/lib/hexapdf/composer.rb +7 -0
  4. data/lib/hexapdf/configuration.rb +13 -0
  5. data/lib/hexapdf/content/parser.rb +3 -1
  6. data/lib/hexapdf/digital_signature/cms_handler.rb +13 -0
  7. data/lib/hexapdf/digital_signature/signature.rb +1 -1
  8. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -0
  9. data/lib/hexapdf/document.rb +14 -3
  10. data/lib/hexapdf/encryption/standard_security_handler.rb +32 -26
  11. data/lib/hexapdf/font/cmap/writer.rb +58 -4
  12. data/lib/hexapdf/font/cmap.rb +7 -0
  13. data/lib/hexapdf/font/true_type_wrapper.rb +41 -16
  14. data/lib/hexapdf/importer.rb +1 -1
  15. data/lib/hexapdf/layout/table_box.rb +57 -10
  16. data/lib/hexapdf/layout/text_fragment.rb +2 -1
  17. data/lib/hexapdf/object.rb +1 -1
  18. data/lib/hexapdf/parser.rb +1 -1
  19. data/lib/hexapdf/reference.rb +1 -1
  20. data/lib/hexapdf/task/merge_acro_form.rb +164 -0
  21. data/lib/hexapdf/task/optimize.rb +4 -4
  22. data/lib/hexapdf/task.rb +1 -0
  23. data/lib/hexapdf/tokenizer.rb +2 -0
  24. data/lib/hexapdf/type/acro_form/appearance_generator.rb +8 -4
  25. data/lib/hexapdf/type/acro_form/form.rb +14 -24
  26. data/lib/hexapdf/type/acro_form/signature_field.rb +18 -7
  27. data/lib/hexapdf/type/acro_form/variable_text_field.rb +12 -4
  28. data/lib/hexapdf/type/actions/go_to.rb +1 -0
  29. data/lib/hexapdf/type/actions/go_to_r.rb +1 -0
  30. data/lib/hexapdf/type/actions/launch.rb +5 -1
  31. data/lib/hexapdf/type/annotation.rb +6 -1
  32. data/lib/hexapdf/type/annotations/markup_annotation.rb +14 -1
  33. data/lib/hexapdf/type/annotations/widget.rb +4 -2
  34. data/lib/hexapdf/type/catalog.rb +3 -0
  35. data/lib/hexapdf/type/cid_font.rb +4 -1
  36. data/lib/hexapdf/type/file_specification.rb +17 -14
  37. data/lib/hexapdf/type/font_descriptor.rb +4 -3
  38. data/lib/hexapdf/type/font_simple.rb +3 -1
  39. data/lib/hexapdf/type/font_true_type.rb +2 -0
  40. data/lib/hexapdf/type/font_type0.rb +1 -1
  41. data/lib/hexapdf/type/font_type1.rb +7 -0
  42. data/lib/hexapdf/type/font_type3.rb +0 -1
  43. data/lib/hexapdf/type/form.rb +5 -2
  44. data/lib/hexapdf/type/graphics_state_parameter.rb +7 -4
  45. data/lib/hexapdf/type/image.rb +8 -4
  46. data/lib/hexapdf/type/info.rb +2 -2
  47. data/lib/hexapdf/type/mark_information.rb +2 -2
  48. data/lib/hexapdf/type/optional_content_configuration.rb +1 -1
  49. data/lib/hexapdf/type/optional_content_membership.rb +1 -1
  50. data/lib/hexapdf/type/page.rb +5 -3
  51. data/lib/hexapdf/type/resources.rb +6 -6
  52. data/lib/hexapdf/type/viewer_preferences.rb +4 -3
  53. data/lib/hexapdf/version.rb +1 -1
  54. data/lib/hexapdf/writer.rb +1 -0
  55. data/test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf +43 -0
  56. data/test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf +44 -0
  57. data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf +43 -0
  58. data/test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf +0 -0
  59. data/test/hexapdf/common_tokenizer_tests.rb +5 -0
  60. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +6 -0
  61. data/test/hexapdf/digital_signature/test_cms_handler.rb +12 -7
  62. data/test/hexapdf/digital_signature/test_signature.rb +7 -0
  63. data/test/hexapdf/digital_signature/test_signatures.rb +12 -7
  64. data/test/hexapdf/encryption/test_standard_security_handler.rb +5 -2
  65. data/test/hexapdf/font/cmap/test_writer.rb +73 -16
  66. data/test/hexapdf/font/test_true_type_wrapper.rb +17 -3
  67. data/test/hexapdf/layout/test_list_box.rb +7 -7
  68. data/test/hexapdf/layout/test_table_box.rb +52 -0
  69. data/test/hexapdf/layout/test_text_fragment.rb +3 -3
  70. data/test/hexapdf/layout/test_text_layouter.rb +4 -2
  71. data/test/hexapdf/task/test_merge_acro_form.rb +104 -0
  72. data/test/hexapdf/task/test_optimize.rb +2 -0
  73. data/test/hexapdf/test_composer.rb +8 -0
  74. data/test/hexapdf/test_document.rb +12 -3
  75. data/test/hexapdf/test_importer.rb +7 -0
  76. data/test/hexapdf/test_parser.rb +7 -0
  77. data/test/hexapdf/test_writer.rb +19 -5
  78. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +40 -23
  79. data/test/hexapdf/type/acro_form/test_form.rb +7 -8
  80. data/test/hexapdf/type/acro_form/test_signature_field.rb +3 -1
  81. data/test/hexapdf/type/acro_form/test_variable_text_field.rb +14 -1
  82. data/test/hexapdf/type/actions/test_launch.rb +6 -2
  83. data/test/hexapdf/type/annotations/test_widget.rb +4 -0
  84. data/test/hexapdf/type/test_font_type1.rb +5 -0
  85. data/test/hexapdf/type/test_form.rb +1 -1
  86. data/test/hexapdf/type/test_page.rb +7 -1
  87. 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. Only use it if
185
- # it is known,
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.id] ||=
237
+ (@encoded_glyphs[glyph] ||=
232
238
  begin
233
239
  raise HexaPDF::MissingGlyphError.new(glyph) if glyph.kind_of?(InvalidGlyph)
234
- if @subsetter
235
- [[@subsetter.use_glyph(glyph.id)].pack('n'), glyph]
236
- else
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
- Encoding: :'Identity-H', DescendantFonts: [cid_font]})
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) {|(id, v), s| s << id.to_s << v[0] }
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 {|_, v| v[1].width == default_width }.map do |id, v|
340
- [(@subsetter ? @subsetter.subset_glyph_id(id) : id), v[1].width]
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.keys.map! do |id|
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
- [(@subsetter ? @subsetter.subset_glyph_id(id) : id), @cmap.gid_to_code(id) || 0xFFFD]
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
@@ -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.left = column_info[cell.column].first
400
- cell.top = height - available_height
401
- row_height = cell.preferred_height if row_height < cell.preferred_height
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 row_fit
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
 
@@ -372,7 +372,7 @@ module HexaPDF
372
372
 
373
373
  # Computes the hash value based on the object and generation numbers.
374
374
  def hash
375
- oid.hash ^ gen.hash
375
+ [oid, gen].hash
376
376
  end
377
377
 
378
378
  def inspect #:nodoc:
@@ -184,7 +184,7 @@ module HexaPDF
184
184
  length = if object[:Length].kind_of?(Integer)
185
185
  object[:Length]
186
186
  elsif object[:Length].kind_of?(Reference)
187
- @document.deref(object[:Length]).value
187
+ @document.deref(object[:Length])&.value || 0
188
188
  else
189
189
  0
190
190
  end
@@ -87,7 +87,7 @@ module HexaPDF
87
87
 
88
88
  # Computes the hash value based on the object and generation numbers.
89
89
  def hash
90
- oid.hash ^ gen.hash
90
+ [oid, gen].hash
91
91
  end
92
92
 
93
93
  # Returns the object identifier as "oid,gen".
@@ -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 to their default
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? && field.default? &&
223
- value == field.default
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
@@ -65,6 +65,7 @@ module HexaPDF
65
65
  autoload(:Optimize, 'hexapdf/task/optimize')
66
66
  autoload(:Dereference, 'hexapdf/task/dereference')
67
67
  autoload(:PDFA, 'hexapdf/task/pdfa')
68
+ autoload(:MergeAcroForm, 'hexapdf/task/merge_acro_form')
68
69
 
69
70
  end
70
71
 
@@ -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[:V] == :Off ? :Yes : @field[:V]] = nil
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
- raise HexaPDF::Error, "Widget of button field doesn't define name for on state"
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 Form object
230
- form = @document.wrap(form, type: :XObject, subtype: :Form) unless form[:Subtype] == :Form
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 +font_size+, +font_options+
186
- # or +font_color+ is specified but +font+ isn't, the font Helvetica is used.
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 +font+, +font_options+ or +font_color+ is
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 +font+, +font_options+ or +font_size+ is
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({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
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.form_field.field_value = result if result
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
- if font || font_options || font_size || font_color
565
- field.set_default_appearance_string(font: font || 'Helvetica',
566
- font_options: font_options || {},
567
- font_size: font_size || 0,
568
- font_color: font_color || 0)
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']