hexapdf 0.47.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +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
|