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.
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']