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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -16
- data/lib/hexapdf/composer.rb +7 -0
- data/lib/hexapdf/configuration.rb +2 -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/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/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.rb +1 -0
- data/lib/hexapdf/tokenizer.rb +2 -0
- data/lib/hexapdf/type/acro_form/form.rb +14 -27
- data/lib/hexapdf/type/acro_form/signature_field.rb +16 -6
- data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
- 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/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/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 +8 -3
- 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_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/test_composer.rb +8 -0
- data/test/hexapdf/test_document.rb +9 -0
- data/test/hexapdf/test_parser.rb +7 -0
- data/test/hexapdf/test_writer.rb +8 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +18 -18
- data/test/hexapdf/type/acro_form/test_form.rb +7 -3
- data/test/hexapdf/type/actions/test_launch.rb +6 -2
- 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 +4 -2
data/lib/hexapdf/type/image.rb
CHANGED
@@ -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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
#
|
data/lib/hexapdf/type/info.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
57
|
+
allowed_values: [:AllOn, :AnyOn, :AnyOff, :AllOff]
|
58
58
|
define_field :VE, type: PDFArray
|
59
59
|
|
60
60
|
end
|
data/lib/hexapdf/type/page.rb
CHANGED
@@ -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({
|
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,
|
52
|
+
define_field :ExtGState, type: Dictionary
|
53
53
|
define_field :ColorSpace, type: Dictionary
|
54
|
-
define_field :Pattern,
|
55
|
-
define_field :Shading,
|
56
|
-
define_field :XObject,
|
57
|
-
define_field :Font,
|
58
|
-
define_field :ProcSet,
|
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
|
-
|
59
|
+
allowed_values: [:UseNone, :UseOutlines, :UseThumbs, :UseOC]
|
60
60
|
define_field :Direction, type: Symbol, default: :L2R, version: '1.3',
|
61
|
-
|
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
|
-
|
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
|
|
data/lib/hexapdf/version.rb
CHANGED
@@ -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(
|
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.
|
104
|
-
assert_match(/Signature valid/, result.messages.
|
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.
|
109
|
-
assert_match(/Signature verification failed/, result.messages.
|
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.
|
129
|
-
assert_match(/Signature valid/, result.messages.
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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
|
-
@
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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(@
|
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
|
-
@
|
49
|
-
@
|
50
|
-
@
|
51
|
-
assert_equal(@
|
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
|
-
@
|
56
|
-
@
|
57
|
-
@
|
58
|
-
assert_equal(@
|
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(@
|
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([
|
115
|
+
assert_equal([1].pack('n'), code)
|
108
116
|
code = @font_wrapper.encode(@font_wrapper.glyph(10))
|
109
|
-
assert_equal([
|
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([[
|
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
|
-
[:
|
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
|
-
[:
|
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
|
-
[:
|
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
|
-
[:
|
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
|
-
[:
|
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
|
-
[:
|
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
|
-
[:
|
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
|
-
[:
|
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
|
-
[:
|
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
|
-
[:
|
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!
|
747
|
-
|
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
|