hexapdf 0.47.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -16
  3. data/lib/hexapdf/composer.rb +7 -0
  4. data/lib/hexapdf/configuration.rb +2 -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/font/cmap/writer.rb +58 -4
  11. data/lib/hexapdf/font/cmap.rb +7 -0
  12. data/lib/hexapdf/font/true_type_wrapper.rb +41 -16
  13. data/lib/hexapdf/layout/text_fragment.rb +2 -1
  14. data/lib/hexapdf/object.rb +1 -1
  15. data/lib/hexapdf/parser.rb +1 -1
  16. data/lib/hexapdf/reference.rb +1 -1
  17. data/lib/hexapdf/task/merge_acro_form.rb +164 -0
  18. data/lib/hexapdf/task.rb +1 -0
  19. data/lib/hexapdf/tokenizer.rb +2 -0
  20. data/lib/hexapdf/type/acro_form/form.rb +14 -27
  21. data/lib/hexapdf/type/acro_form/signature_field.rb +16 -6
  22. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  23. data/lib/hexapdf/type/actions/go_to.rb +1 -0
  24. data/lib/hexapdf/type/actions/go_to_r.rb +1 -0
  25. data/lib/hexapdf/type/actions/launch.rb +5 -1
  26. data/lib/hexapdf/type/annotation.rb +6 -1
  27. data/lib/hexapdf/type/annotations/markup_annotation.rb +14 -1
  28. data/lib/hexapdf/type/catalog.rb +3 -0
  29. data/lib/hexapdf/type/cid_font.rb +4 -1
  30. data/lib/hexapdf/type/file_specification.rb +17 -14
  31. data/lib/hexapdf/type/font_descriptor.rb +4 -3
  32. data/lib/hexapdf/type/font_simple.rb +3 -1
  33. data/lib/hexapdf/type/font_true_type.rb +2 -0
  34. data/lib/hexapdf/type/font_type0.rb +1 -1
  35. data/lib/hexapdf/type/font_type1.rb +7 -0
  36. data/lib/hexapdf/type/font_type3.rb +0 -1
  37. data/lib/hexapdf/type/form.rb +5 -2
  38. data/lib/hexapdf/type/graphics_state_parameter.rb +7 -4
  39. data/lib/hexapdf/type/image.rb +8 -4
  40. data/lib/hexapdf/type/info.rb +2 -2
  41. data/lib/hexapdf/type/mark_information.rb +2 -2
  42. data/lib/hexapdf/type/optional_content_configuration.rb +1 -1
  43. data/lib/hexapdf/type/optional_content_membership.rb +1 -1
  44. data/lib/hexapdf/type/page.rb +5 -3
  45. data/lib/hexapdf/type/resources.rb +6 -6
  46. data/lib/hexapdf/type/viewer_preferences.rb +4 -3
  47. data/lib/hexapdf/version.rb +1 -1
  48. data/test/hexapdf/common_tokenizer_tests.rb +5 -0
  49. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +6 -0
  50. data/test/hexapdf/digital_signature/test_cms_handler.rb +12 -7
  51. data/test/hexapdf/digital_signature/test_signature.rb +7 -0
  52. data/test/hexapdf/digital_signature/test_signatures.rb +8 -3
  53. data/test/hexapdf/font/cmap/test_writer.rb +73 -16
  54. data/test/hexapdf/font/test_true_type_wrapper.rb +17 -3
  55. data/test/hexapdf/layout/test_list_box.rb +7 -7
  56. data/test/hexapdf/layout/test_text_fragment.rb +3 -3
  57. data/test/hexapdf/layout/test_text_layouter.rb +4 -2
  58. data/test/hexapdf/task/test_merge_acro_form.rb +104 -0
  59. data/test/hexapdf/test_composer.rb +8 -0
  60. data/test/hexapdf/test_document.rb +9 -0
  61. data/test/hexapdf/test_parser.rb +7 -0
  62. data/test/hexapdf/test_writer.rb +8 -3
  63. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +18 -18
  64. data/test/hexapdf/type/acro_form/test_form.rb +7 -3
  65. data/test/hexapdf/type/actions/test_launch.rb +6 -2
  66. data/test/hexapdf/type/test_font_type1.rb +5 -0
  67. data/test/hexapdf/type/test_form.rb +1 -1
  68. data/test/hexapdf/type/test_page.rb +7 -1
  69. metadata +4 -2
@@ -61,10 +61,10 @@ module HexaPDF
61
61
  define_field :ColorSpace, type: [Symbol, PDFArray]
62
62
  define_field :BitsPerComponent, type: Integer
63
63
  define_field :Intent, type: Symbol, version: '1.1',
64
- allowed_values: [HexaPDF::Content::RenderingIntent::ABSOLUTE_COLORIMETRIC,
65
- HexaPDF::Content::RenderingIntent::RELATIVE_COLORIMETRIC,
66
- HexaPDF::Content::RenderingIntent::SATURATION,
67
- HexaPDF::Content::RenderingIntent::PERCEPTUAL]
64
+ allowed_values: [HexaPDF::Content::RenderingIntent::ABSOLUTE_COLORIMETRIC,
65
+ HexaPDF::Content::RenderingIntent::RELATIVE_COLORIMETRIC,
66
+ HexaPDF::Content::RenderingIntent::SATURATION,
67
+ HexaPDF::Content::RenderingIntent::PERCEPTUAL]
68
68
  define_field :ImageMask, type: Boolean, default: false
69
69
  define_field :Mask, type: [Stream, PDFArray], version: '1.3'
70
70
  define_field :Decode, type: PDFArray
@@ -72,11 +72,15 @@ module HexaPDF
72
72
  define_field :Alternates, type: PDFArray, version: '1.3'
73
73
  define_field :SMask, type: Stream, version: '1.4'
74
74
  define_field :SMaskInData, type: Integer, version: '1.5', allowed_values: [0, 1, 2]
75
+ define_field :Name, type: Symbol
75
76
  define_field :StructParent, type: Integer, version: '1.3'
76
77
  define_field :ID, type: PDFByteString, version: '1.3'
77
78
  define_field :OPI, type: Dictionary, version: '1.2'
78
79
  define_field :Metadata, type: Stream, version: '1.4'
79
80
  define_field :OC, type: Dictionary, version: '1.5'
81
+ define_field :AF, type: PDFArray, version: '2.0'
82
+ define_field :Measure, type: Dictionary, version: '2.0'
83
+ define_field :PtData, type: Dictionary, version: '2.0'
80
84
 
81
85
  # Returns the source path that was used when creating the image object.
82
86
  #
@@ -57,8 +57,8 @@ module HexaPDF
57
57
  define_field :Producer, type: String
58
58
  define_field :CreationDate, type: PDFDate
59
59
  define_field :ModDate, type: PDFDate
60
- define_field :Trapped, type: Symbol, version: '1.3',
61
- allowed_values: [:True, :False, :Unknown]
60
+ define_field :Trapped, type: Symbol, version: '1.3', default: :Unknown,
61
+ allowed_values: [:True, :False, :Unknown]
62
62
 
63
63
  # Info dictionaries must always be indirect.
64
64
  def must_be_indirect?
@@ -48,8 +48,8 @@ module HexaPDF
48
48
  define_type :XXMarkInformation
49
49
 
50
50
  define_field :Marked, type: Boolean, default: false
51
- define_field :UserProperties, type: Boolean, default: false
52
- define_field :Suspects, type: Boolean, default: false
51
+ define_field :UserProperties, type: Boolean, default: false, version: '1.6'
52
+ define_field :Suspects, type: Boolean, default: false, version: '1.6'
53
53
 
54
54
  end
55
55
 
@@ -76,7 +76,7 @@ module HexaPDF
76
76
  define_field :AS, type: PDFArray
77
77
  define_field :Order, type: PDFArray
78
78
  define_field :ListMode, type: Symbol, default: :AllPages,
79
- allowed_values: [:AllPages, :VisiblePages]
79
+ allowed_values: [:AllPages, :VisiblePages]
80
80
  define_field :RBGroups, type: PDFArray
81
81
  define_field :Locked, type: PDFArray, default: []
82
82
 
@@ -54,7 +54,7 @@ module HexaPDF
54
54
  define_field :Type, type: Symbol, required: true, default: type
55
55
  define_field :OCGs, type: [:OCG, PDFArray]
56
56
  define_field :P, type: Symbol, default: :AnyOn,
57
- allowed_values: [:AllOn, :AnyOn, :AnyOff, :AllOff]
57
+ allowed_values: [:AllOn, :AnyOn, :AnyOff, :AllOff]
58
58
  define_field :VE, type: PDFArray
59
59
 
60
60
  end
@@ -159,6 +159,9 @@ module HexaPDF
159
159
  define_field :PresSteps, type: Dictionary, version: '1.5'
160
160
  define_field :UserUnit, type: Numeric, version: '1.6'
161
161
  define_field :VP, type: PDFArray, version: '1.6'
162
+ define_field :AF, type: PDFArray, version: '2.0'
163
+ define_field :OutputIntents, type: PDFArray, version: '2.0'
164
+ define_field :DPart, type: Dictionary, version: '2.0'
162
165
 
163
166
  # Returns +true+ since page objects must always be indirect.
164
167
  def must_be_indirect?
@@ -358,7 +361,7 @@ module HexaPDF
358
361
  def contents
359
362
  Array(self[:Contents]).each_with_object("".b) do |content_stream, content|
360
363
  content << " " unless content.empty?
361
- content << content_stream.stream
364
+ content << content_stream.stream if content_stream.kind_of?(Stream)
362
365
  end
363
366
  end
364
367
 
@@ -380,8 +383,7 @@ module HexaPDF
380
383
  # Returns the, possibly inherited, resource dictionary which is automatically created if it
381
384
  # doesn't exist.
382
385
  def resources
383
- self[:Resources] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
384
- type: :XXResources)
386
+ self[:Resources] ||= document.wrap({}, type: :XXResources)
385
387
  end
386
388
 
387
389
  # Processes the content streams associated with the page with the given processor object.
@@ -49,13 +49,13 @@ module HexaPDF
49
49
 
50
50
  define_type :XXResources
51
51
 
52
- define_field :ExtGState, type: Dictionary
52
+ define_field :ExtGState, type: Dictionary
53
53
  define_field :ColorSpace, type: Dictionary
54
- define_field :Pattern, type: Dictionary
55
- define_field :Shading, type: Dictionary, version: '1.3'
56
- define_field :XObject, type: Dictionary
57
- define_field :Font, type: Dictionary
58
- define_field :ProcSet, type: PDFArray
54
+ define_field :Pattern, type: Dictionary
55
+ define_field :Shading, type: Dictionary, version: '1.3'
56
+ define_field :XObject, type: Dictionary
57
+ define_field :Font, type: Dictionary
58
+ define_field :ProcSet, type: PDFArray
59
59
  define_field :Properties, type: Dictionary, version: '1.2'
60
60
 
61
61
  # Returns the color space stored under the given name.
@@ -56,19 +56,20 @@ module HexaPDF
56
56
  define_field :CenterWindow, type: Boolean, default: false
57
57
  define_field :DisplayDocTitle, type: Boolean, default: false, version: '1.4'
58
58
  define_field :NonFullScreenPageMode, type: Symbol, default: :UseNone,
59
- allowed_values: [:UseNone, :UseOutlines, :UseThumbs, :UseOC]
59
+ allowed_values: [:UseNone, :UseOutlines, :UseThumbs, :UseOC]
60
60
  define_field :Direction, type: Symbol, default: :L2R, version: '1.3',
61
- allowed_values: [:L2R, :R2L]
61
+ allowed_values: [:L2R, :R2L]
62
62
  define_field :ViewArea, type: Symbol, default: :CropBox, version: '1.4'
63
63
  define_field :ViewClip, type: Symbol, default: :CropBox, version: '1.4'
64
64
  define_field :PrintArea, type: Symbol, default: :CropBox, version: '1.4'
65
65
  define_field :PrintClip, type: Symbol, default: :CropBox, version: '1.4'
66
66
  define_field :PrintScaling, type: Symbol, default: :AppDefault, version: '1.6'
67
67
  define_field :Duplex, type: Symbol, version: '1.7',
68
- allowed_values: [:Simplex, :DuplexFlipShortEdge, :DuplexFlipLongEdge]
68
+ allowed_values: [:Simplex, :DuplexFlipShortEdge, :DuplexFlipLongEdge]
69
69
  define_field :PickTrayByPDFSize, type: Boolean, version: '1.7'
70
70
  define_field :PrintPageRange, type: PDFArray, version: '1.7'
71
71
  define_field :NumCopies, type: Integer, version: '1.7'
72
+ define_field :Enforce, type: PDFArray, version: '2.0'
72
73
 
73
74
  end
74
75
 
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.47.0'
40
+ VERSION = '1.0.0'
41
41
 
42
42
  end
@@ -104,6 +104,11 @@ module CommonTokenizerTests
104
104
  assert_raises(HexaPDF::MalformedPDFError) { @tokenizer.next_token }
105
105
  end
106
106
 
107
+ it "next_token: fails on a closing parenthesis that is not part of a literal string" do
108
+ create_tokenizer(" )")
109
+ assert_raises(HexaPDF::MalformedPDFError) { @tokenizer.next_token }
110
+ end
111
+
107
112
  it "next_token: fails on a missing greater than sign in a hex string" do
108
113
  create_tokenizer("<ABCD")
109
114
  assert_raises(HexaPDF::MalformedPDFError) { @tokenizer.next_token }
@@ -157,6 +157,12 @@ describe HexaPDF::DigitalSignature::Signing::DefaultHandler do
157
157
  assert_same(@obj, @doc.catalog[:Perms][:DocMDP])
158
158
  end
159
159
 
160
+ it "updates the document version if :pades signing is used" do
161
+ @handler.signature_type = :pades
162
+ @handler.finalize_objects(@field, @obj)
163
+ assert_equal('2.0', @doc.version)
164
+ end
165
+
160
166
  it "fails if DocMDP should be set but there is already a signature" do
161
167
  @handler.doc_mdp_permissions = :no_changes
162
168
  2.times do
@@ -62,7 +62,7 @@ describe HexaPDF::DigitalSignature::CMSHandler do
62
62
  @dict.contents = @pkcs7.to_der
63
63
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
64
64
  result = @handler.verify(@store)
65
- assert_equal(2, result.messages.size)
65
+ assert_equal(3, result.messages.size)
66
66
  assert_equal(:error, result.messages.first.type)
67
67
  assert_match(/Exactly one signer needed/, result.messages.first.content)
68
68
  end
@@ -100,13 +100,13 @@ describe HexaPDF::DigitalSignature::CMSHandler do
100
100
 
101
101
  it "verifies the signature itself" do
102
102
  result = @handler.verify(@store)
103
- assert_equal(:info, result.messages.last.type)
104
- assert_match(/Signature valid/, result.messages.last.content)
103
+ assert_equal(:info, result.messages[-2].type)
104
+ assert_match(/Signature valid/, result.messages[-2].content)
105
105
 
106
106
  @dict.signed_data = 'other data'
107
107
  result = @handler.verify(@store)
108
- assert_equal(:error, result.messages.last.type)
109
- assert_match(/Signature verification failed/, result.messages.last.content)
108
+ assert_equal(:error, result.messages[-2].type)
109
+ assert_match(/Signature verification failed/, result.messages[-2].content)
110
110
  end
111
111
 
112
112
  it "verifies a timestamp signature" do
@@ -125,8 +125,13 @@ describe HexaPDF::DigitalSignature::CMSHandler do
125
125
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
126
126
 
127
127
  result = @handler.verify(@store)
128
- assert_equal(:info, result.messages.last.type)
129
- assert_match(/Signature valid/, result.messages.last.content)
128
+ assert_equal(:info, result.messages[-2].type)
129
+ assert_match(/Signature valid/, result.messages[-2].content)
130
+ end
131
+
132
+ it "provides information on the certificate chain" do
133
+ result = @handler.verify(@store)
134
+ assert_match(/RSA signer -> HexaPDF Test Root CA/, result.messages.last.content)
130
135
  end
131
136
  end
132
137
 
@@ -111,6 +111,13 @@ describe HexaPDF::DigitalSignature::Signature do
111
111
  assert_equal((MINIMAL_PDF[0, 400] << MINIMAL_PDF[500, 333]).b, @sig.signed_data)
112
112
  end
113
113
 
114
+ it "works for invalid offsets" do
115
+ doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
116
+ @sig.document = doc
117
+ @sig[:ByteRange] = [0, 400, 9000, 333]
118
+ assert_equal(MINIMAL_PDF[0, 400], @sig.signed_data)
119
+ end
120
+
114
121
  it "fails if the document isn't associated with an existing PDF file" do
115
122
  assert_raises(HexaPDF::Error) { @sig.signed_data }
116
123
  end
@@ -70,7 +70,8 @@ describe HexaPDF::DigitalSignature::Signatures do
70
70
  end
71
71
  @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
72
72
  sig = @doc.signatures.first
73
- assert_equal([0, 925, 925 + sig[:Contents].size * 2 + 2, 2501], sig[:ByteRange].value)
73
+ assert_equal([0, 925, 925 + sig[:Contents].size * 2 + 2, 2455 + HexaPDF::VERSION.length],
74
+ sig[:ByteRange].value)
74
75
  assert_equal(:sig, sig[:key])
75
76
  assert_equal(:sig_field, @doc.acro_form.each_field.first[:key])
76
77
  assert(sig.key?(:Contents))
@@ -132,14 +133,18 @@ describe HexaPDF::DigitalSignature::Signatures do
132
133
  it "handles different xref section types correctly when determing the offsets" do
133
134
  @doc.delete(7)
134
135
  sig = @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
135
- assert_equal([0, 1036, 1036 + sig[:Contents].size * 2 + 2, 2483], sig[:ByteRange].value)
136
+ l1 = 1030 + HexaPDF::VERSION.length
137
+ assert_equal([0, l1, l1 + sig[:Contents].size * 2 + 2, 2437 + HexaPDF::VERSION.length],
138
+ sig[:ByteRange].value)
136
139
  end
137
140
 
138
141
  it "works if the signature object is the last object of the xref section" do
139
142
  field = @doc.acro_form(create: true).create_signature_field('Signature2')
140
143
  field.create_widget(@doc.pages[0], Rect: [0, 0, 0, 0])
141
144
  sig = @doc.signatures.add(@io, @handler, signature: field, write_options: {update_fields: false})
142
- assert_equal([0, 3143, 3143 + sig[:Contents].size * 2 + 2, 380], sig[:ByteRange].value)
145
+ l1 = 3097 + HexaPDF::VERSION.length
146
+ assert_equal([0, l1, l1 + sig[:Contents].size * 2 + 2, 374 + HexaPDF::VERSION.length],
147
+ sig[:ByteRange].value)
143
148
  end
144
149
 
145
150
  it "allows writing to a file in addition to writing to an IO" do
@@ -5,7 +5,7 @@ require 'hexapdf/font/cmap/writer'
5
5
 
6
6
  describe HexaPDF::Font::CMap::Writer do
7
7
  before do
8
- @cmap_data = <<~EOF
8
+ @to_unicode_cmap_data = <<~EOF
9
9
  /CIDInit /ProcSet findresource begin
10
10
  12 dict begin
11
11
  begincmap
@@ -32,35 +32,92 @@ describe HexaPDF::Font::CMap::Writer do
32
32
  end
33
33
  end
34
34
  EOF
35
- @mapping = []
36
- 0x00.upto(0x5e) {|i| @mapping << [i, 0x20 + i] }
37
- @mapping << [0x60, 0x90]
38
- 0x1379.upto(0x137B) {|i| @mapping << [i, 0x90FE + i - 0x1379] }
39
- @mapping << [0x3A51, 0x2003E]
35
+ @cid_cmap_data = <<~EOF
36
+ %!PS-Adobe-3.0 Resource-CMap
37
+ %%DocumentNeededResources: ProcSet (CIDInit)
38
+ %%IncludeResource: ProcSet (CIDInit)
39
+ %%BeginResource: CMap (Custom)
40
+ %%Title: (Custom Adobe Identity 0)
41
+ %%Version: 1
42
+ /CIDInit /ProcSet findresource begin
43
+ 12 dict begin
44
+ begincmap
45
+ /CIDSystemInfo 3 dict dup begin
46
+ /Registry (Adobe) def
47
+ /Ordering (Identity) def
48
+ /Supplement 0 def
49
+ end def
50
+ /CMapName /Custom def
51
+ /CMapType 1 def
52
+ /CMapVersion 1 def
53
+ /WMode 0 def
54
+ 1 begincodespacerange
55
+ <0000> <FFFF>
56
+ endcodespacerange
57
+ 1 begincidchar
58
+ <0060> 144
59
+ endcidchar
60
+ 1 begincidrange
61
+ <0000><005E> 32
62
+ endcidrange
63
+ endcmap
64
+ CMapName currentdict /CMap defineresource pop
65
+ end
66
+ end
67
+ %%EndResource
68
+ %%EOF
69
+ EOF
70
+
71
+ @to_unicode_mapping = []
72
+ @cid_mapping = []
73
+ 0x00.upto(0x5e) do |i|
74
+ @to_unicode_mapping << [i, 0x20 + i]
75
+ @cid_mapping << [i, 0x20 + i]
76
+ end
77
+ @to_unicode_mapping << [0x60, 0x90]
78
+ @cid_mapping << [0x60, 0x90]
79
+ 0x1379.upto(0x137B) do |i|
80
+ @to_unicode_mapping << [i, 0x90FE + i - 0x1379]
81
+ end
82
+ @to_unicode_mapping << [0x3A51, 0x2003E]
40
83
  end
41
84
 
42
85
  describe "create_to_unicode_cmap" do
43
86
  it "creates a correct CMap file" do
44
- assert_equal(@cmap_data, HexaPDF::Font::CMap.create_to_unicode_cmap(@mapping))
87
+ assert_equal(@to_unicode_cmap_data,
88
+ HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
45
89
  end
46
90
 
47
91
  it "works if the last item is a range" do
48
- @mapping.pop
49
- @cmap_data.sub!(/2 beginbfchar/, '1 beginbfchar')
50
- @cmap_data.sub!(/<3A51><d840dc3e>\n/, '')
51
- assert_equal(@cmap_data, HexaPDF::Font::CMap.create_to_unicode_cmap(@mapping))
92
+ @to_unicode_mapping.pop
93
+ @to_unicode_cmap_data.sub!(/2 beginbfchar/, '1 beginbfchar')
94
+ @to_unicode_cmap_data.sub!(/<3A51><d840dc3e>\n/, '')
95
+ assert_equal(@to_unicode_cmap_data,
96
+ HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
52
97
  end
53
98
 
54
99
  it "works with only ranges" do
55
- @mapping.delete_at(-1)
56
- @mapping.delete_at(0x5f)
57
- @cmap_data.sub!(/\n2 beginbfchar.*endbfchar/m, '')
58
- assert_equal(@cmap_data, HexaPDF::Font::CMap.create_to_unicode_cmap(@mapping))
100
+ @to_unicode_mapping.delete_at(-1)
101
+ @to_unicode_mapping.delete_at(0x5f)
102
+ @to_unicode_cmap_data.sub!(/\n2 beginbfchar.*endbfchar/m, '')
103
+ assert_equal(@to_unicode_cmap_data,
104
+ HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
59
105
  end
60
106
 
61
107
  it "returns an empty CMap if the mapping is empty" do
62
- assert_equal(@cmap_data.sub(/\d+ beginbfchar.*endbfrange/m, ''),
108
+ assert_equal(@to_unicode_cmap_data.sub(/\d+ beginbfchar.*endbfrange/m, ''),
63
109
  HexaPDF::Font::CMap.create_to_unicode_cmap([]))
64
110
  end
65
111
  end
112
+
113
+ describe "create_cid_cmap" do
114
+ it "creates a correct CMap file" do
115
+ assert_equal(@cid_cmap_data, HexaPDF::Font::CMap.create_cid_cmap(@cid_mapping))
116
+ end
117
+
118
+ it "returns an empty CMap if the mapping is empty" do
119
+ assert_equal(@cid_cmap_data.sub(/\d+ begincidchar.*endcidrange/m, ''),
120
+ HexaPDF::Font::CMap.create_cid_cmap([]))
121
+ end
122
+ end
66
123
  end
@@ -71,6 +71,12 @@ describe HexaPDF::Font::TrueTypeWrapper do
71
71
  glyph.inspect)
72
72
  end
73
73
 
74
+ it "caches glyphs based on the id and string" do
75
+ glyph = @font_wrapper.glyph(17)
76
+ assert_same(glyph, @font_wrapper.glyph(17))
77
+ refute_same(glyph, @font_wrapper.glyph(17, "1"))
78
+ end
79
+
74
80
  it "invokes font.on_missing_glyph for missing glyphs" do
75
81
  glyph = @font_wrapper.glyph(9999)
76
82
  assert_kind_of(HexaPDF::Font::InvalidGlyph, glyph)
@@ -99,14 +105,18 @@ describe HexaPDF::Font::TrueTypeWrapper do
99
105
  assert_equal([1].pack('n'), code)
100
106
  code = @font_wrapper.encode(@font_wrapper.glyph(10))
101
107
  assert_equal([2].pack('n'), code)
108
+ code = @font_wrapper.encode(@font_wrapper.glyph(10, "o"))
109
+ assert_equal([3].pack('n'), code)
102
110
  end
103
111
 
104
112
  it "returns the encoded glyph ID for fonts that are not subset" do
105
113
  @font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(@doc, @font, subset: false)
106
114
  code = @font_wrapper.encode(@font_wrapper.glyph(3))
107
- assert_equal([3].pack('n'), code)
115
+ assert_equal([1].pack('n'), code)
108
116
  code = @font_wrapper.encode(@font_wrapper.glyph(10))
109
- assert_equal([10].pack('n'), code)
117
+ assert_equal([2].pack('n'), code)
118
+ code = @font_wrapper.encode(@font_wrapper.glyph(10, "o"))
119
+ assert_equal([3].pack('n'), code)
110
120
  end
111
121
 
112
122
  it "raises an error if an InvalidGlyph is encoded" do
@@ -180,14 +190,18 @@ describe HexaPDF::Font::TrueTypeWrapper do
180
190
  it "with fonts that are not subset (only differences to other case)" do
181
191
  @font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(@doc, @font, subset: false)
182
192
  @font_wrapper.encode(@font_wrapper.glyph(3))
193
+ @font_wrapper.encode(@font_wrapper.glyph(3, "-"))
183
194
  glyph = @font_wrapper.decode_utf8('H').first
184
195
  @font_wrapper.encode(glyph)
185
196
  @doc.dispatch_message(:complete_objects)
186
197
 
187
198
  dict = @font_wrapper.pdf_object
188
199
 
189
- assert_equal(HexaPDF::Font::CMap.create_to_unicode_cmap([[3, ' '.ord], [glyph.id, 'H'.ord]]),
200
+ assert_equal(HexaPDF::Font::CMap.create_to_unicode_cmap([[1, ' '.ord], [2, '-'.ord],
201
+ [3, 'H'.ord]]),
190
202
  dict[:ToUnicode].stream)
203
+ assert_equal(HexaPDF::Font::CMap.create_cid_cmap([[1, 3], [2, 3], [3, glyph.id]]),
204
+ dict[:Encoding].stream)
191
205
  assert_equal([glyph.id, [glyph.width]], dict[:DescendantFonts][0][:W].value)
192
206
  end
193
207
  end
@@ -185,7 +185,7 @@ describe HexaPDF::Layout::ListBox do
185
185
  [:set_font_and_size, [:F1, 11]],
186
186
  [:set_device_gray_non_stroking_color, [0.5]],
187
187
  [:begin_text],
188
- [:set_text_matrix, [1, 0, 0, 1, 1.15, 92.487]],
188
+ [:move_text, [1.15, 92.487]],
189
189
  [:show_text, ["\x95".b]],
190
190
  [:end_text],
191
191
  [:restore_graphics_state],
@@ -197,7 +197,7 @@ describe HexaPDF::Layout::ListBox do
197
197
  [:set_font_and_size, [:F1, 11]],
198
198
  [:set_device_gray_non_stroking_color, [0.5]],
199
199
  [:begin_text],
200
- [:set_text_matrix, [1, 0, 0, 1, 1.15, 82.487]],
200
+ [:move_text, [1.15, 82.487]],
201
201
  [:show_text, ["\x95".b]],
202
202
  [:end_text],
203
203
  [:restore_graphics_state],
@@ -219,7 +219,7 @@ describe HexaPDF::Layout::ListBox do
219
219
  [:set_text_rise, [-6.111111]],
220
220
  [:set_device_gray_non_stroking_color, [0.5]],
221
221
  [:begin_text],
222
- [:set_text_matrix, [1, 0, 0, 1, 0.1985, 100]],
222
+ [:move_text, [0.1985, 100]],
223
223
  [:show_text, ["m".b]],
224
224
  [:end_text],
225
225
  [:restore_graphics_state],
@@ -241,7 +241,7 @@ describe HexaPDF::Layout::ListBox do
241
241
  [:set_text_rise, [-6.111111]],
242
242
  [:set_device_gray_non_stroking_color, [0.5]],
243
243
  [:begin_text],
244
- [:set_text_matrix, [1, 0, 0, 1, 0.8145, 100]],
244
+ [:move_text, [0.8145, 100]],
245
245
  [:show_text, ["n".b]],
246
246
  [:end_text],
247
247
  [:restore_graphics_state],
@@ -263,7 +263,7 @@ describe HexaPDF::Layout::ListBox do
263
263
  [:set_font_and_size, [:F1, 11]],
264
264
  [:set_device_gray_non_stroking_color, [0.5]],
265
265
  [:begin_text],
266
- [:set_text_matrix, [1, 0, 0, 1, 6.75, 92.487]],
266
+ [:move_text, [6.75, 92.487]],
267
267
  [:show_text, ["1.".b]],
268
268
  [:end_text],
269
269
  [:restore_graphics_state],
@@ -275,7 +275,7 @@ describe HexaPDF::Layout::ListBox do
275
275
  [:set_font_and_size, [:F1, 11]],
276
276
  [:set_device_gray_non_stroking_color, [0.5]],
277
277
  [:begin_text],
278
- [:set_text_matrix, [1, 0, 0, 1, 6.75, 82.487]],
278
+ [:move_text, [6.75, 82.487]],
279
279
  [:show_text, ["2.".b]],
280
280
  [:end_text],
281
281
  [:restore_graphics_state],
@@ -314,7 +314,7 @@ describe HexaPDF::Layout::ListBox do
314
314
  [:save_graphics_state],
315
315
  [:set_font_and_size, [:F1, 10]],
316
316
  [:begin_text],
317
- [:set_text_matrix, [1, 0, 0, 1, 1.5, 93.17]],
317
+ [:move_text, [1.5, 93.17]],
318
318
  [:show_text, ["\x95".b]],
319
319
  [:end_text],
320
320
  [:restore_graphics_state],
@@ -139,7 +139,7 @@ describe HexaPDF::Layout::TextFragment do
139
139
  [:set_text_rise, [2]],
140
140
  *middle,
141
141
  [:begin_text],
142
- [:set_text_matrix, [1, 0, 0, 1, 10, 15]],
142
+ [:move_text, [10, 15]],
143
143
  [:show_text, ['!']],
144
144
  *back,
145
145
  ].compact
@@ -156,7 +156,7 @@ describe HexaPDF::Layout::TextFragment do
156
156
  @canvas = @doc.pages.add.canvas
157
157
  @fragment.draw(@canvas, 10, 15, ignore_text_properties: true)
158
158
  assert_operators(@canvas.contents, [[:begin_text],
159
- [:set_text_matrix, [1, 0, 0, 1, 10, 15]]])
159
+ [:move_text, [10, 15]]])
160
160
  end
161
161
 
162
162
  describe "uses an appropriate text position setter" do
@@ -188,7 +188,7 @@ describe HexaPDF::Layout::TextFragment do
188
188
  it "horizontal and vertical movement" do
189
189
  @fragment.draw(@canvas, 10, 10, ignore_text_properties: true)
190
190
  assert_operators(@canvas.contents, [[:begin_text],
191
- [:set_text_matrix, [1, 0, 0, 1, 10, 10]]])
191
+ [:move_text, [10, 10]]])
192
192
  end
193
193
  end
194
194
 
@@ -743,10 +743,12 @@ describe HexaPDF::Layout::TextLayouter do
743
743
  result = processor.recorded_ops
744
744
  leading = (result.select {|name, _| name == :set_leading } || [0]).map(&:last).flatten.first
745
745
  pos = [0, 0]
746
- result.select! {|name, _| name == :set_text_matrix || name == :move_text_next_line }.
747
- map! do |name, ops|
746
+ result.select! do |name, _|
747
+ name == :set_text_matrix || name == :move_text || name == :move_text_next_line
748
+ end.map! do |name, ops|
748
749
  case name
749
750
  when :set_text_matrix then pos = ops[-2, 2]
751
+ when :move_text then pos = ops
750
752
  when :move_text_next_line then pos[1] -= leading
751
753
  end
752
754
  pos.dup
@@ -0,0 +1,104 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/task/merge_acro_form'
6
+
7
+ describe HexaPDF::Task::MergeAcroForm do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @doc.pages.add
11
+ @doc.pages.add
12
+ form = @doc.acro_form(create: true)
13
+ field = form.create_text_field("Text")
14
+ field.create_widget(@doc.pages[0], Rect: [0, 0, 0, 0])
15
+ field.create_widget(@doc.pages[1], Rect: [0, 0, 0, 0])
16
+
17
+ form.create_text_field("Calc.Field a")
18
+ form.create_text_field("Calc.Field b")
19
+ field = form.create_text_field('Other.Calculation 1')
20
+ field.set_calculate_action(:sum, fields: ["Calc.Field a", "Calc.Field b"])
21
+ field.create_widget(@doc.pages[1])
22
+ field = form.create_text_field('Other.Calculation 2')
23
+ field.set_calculate_action(:sfn, fields: "Calc.Field\\ a + Calc.Field\\ b")
24
+ field.create_widget(@doc.pages[1])
25
+
26
+ @root_fields = @doc.acro_form.root_fields
27
+
28
+ @doc.dispatch_message(:complete_objects)
29
+ @doc.validate
30
+ @doc1 = @doc.duplicate
31
+ @pages = []
32
+ @pages << @doc.pages.add(@doc.import(@doc1.pages[0]))
33
+ @pages << @doc.pages.add(@doc.import(@doc1.pages[1]))
34
+ end
35
+
36
+ it "selects a unique name for the root field" do
37
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
38
+ assert_equal('merged_1', @root_fields[3][:T])
39
+
40
+ @root_fields << @doc.wrap({T: 'merged_23'})
41
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
42
+ assert_equal('merged_24', @root_fields[5][:T])
43
+ end
44
+
45
+ it "merges the /DR entry of the main AcroForm dictionary" do
46
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
47
+ assert(@doc.acro_form.default_resources[:Font].key?(:F2))
48
+ end
49
+
50
+ it "updates the /SigFlags if necessary" do
51
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
52
+ refute(@doc.acro_form.signature_flag?(:signatures_exist))
53
+
54
+ @pages[0][:Annots][0].form_field[:FT] = :Sig
55
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
56
+ refute(@doc.acro_form.signature_flag?(:signatures_exist))
57
+
58
+ @doc1.acro_form.signature_flag(:signatures_exist)
59
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
60
+ assert(@doc.acro_form.signature_flag?(:signatures_exist))
61
+ end
62
+
63
+ it "applies the /DA and /Q entries of the source AcroForm to the created root field" do
64
+ @doc1.acro_form.set_default_appearance_string
65
+ @doc1.acro_form[:Q] = @doc1.add(5)
66
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
67
+ assert_equal('0.0 g /F2 0 Tf', @root_fields[3][:DA])
68
+ assert_equal(5, @root_fields[3][:Q])
69
+ end
70
+
71
+ it "merges only the fields references in the given pages" do
72
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
73
+ assert_equal('merged_1', @root_fields[3][:T])
74
+ assert_equal(1, @root_fields[3][:Kids].size)
75
+ end
76
+
77
+ it "only merges fields that have at least one widget" do
78
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
79
+ assert_equal(2, @root_fields[3][:Kids].size)
80
+ assert_nil(@doc.acro_form.field_by_name('merged_1.Calc'))
81
+ end
82
+
83
+ it "updates the /DA entries of widgets and fields" do
84
+ @pages[0][:Annots][0][:DA] = '/F1 10 Tf'
85
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
86
+ field = @doc.acro_form.field_by_name('merged_1.Text')
87
+ assert_equal('0.0 g /F2 0 Tf', field[:DA])
88
+ assert_equal('/F2 10 Tf', field.each_widget.to_a[0][:DA])
89
+ end
90
+
91
+ it "doesn't update the calculation actions if no field with one is merged" do
92
+ @doc.task(:merge_acro_form, source: @doc1, pages: [@pages[0]])
93
+ assert_equal(2, @doc.acro_form[:CO].size)
94
+ end
95
+
96
+ it "updates the field names in known calculation actions" do
97
+ @doc.task(:merge_acro_form, source: @doc1, pages: @pages)
98
+ assert_equal(4, @doc.acro_form[:CO].size)
99
+ js = @doc.acro_form.field_by_name('merged_1.Other.Calculation 1')[:AA][:C][:JS]
100
+ assert_match(/merged_1.Calc.Field a/, js)
101
+ js = @doc.acro_form.field_by_name('merged_1.Other.Calculation 2')[:AA][:C][:JS]
102
+ assert_match(/merged_1.Calc.Field a/, js)
103
+ end
104
+ end