combine_pdf 0.2.34 → 0.2.35
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/combine_pdf/decrypt.rb +35 -32
- data/lib/combine_pdf/page_methods.rb +8 -2
- data/lib/combine_pdf/version.rb +1 -1
- data/test/automated +24 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9fcaee4b0d0bc2b6991c41c570b300120038e6bd
|
4
|
+
data.tar.gz: 9045bd35b291cd33fb9d8f5a1e5611e64c4b81bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a82447b8e8caf898149b868818aa5c61f99f30f974297d8ed19e2afdb3ebb2f48f86f03909cbd03a698b23288455ec2743ab6a3fec73a489e73e3c47a6b46769
|
7
|
+
data.tar.gz: f1d0d133c2a84c82cfe7920b626ae56cf8a58e40a73da5ca5bff5fb5c9c07574e404fe5ebcb913fcc14464ca9029cba971c7f581a60af891d0cb555e0be9abb0
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,21 @@
|
|
2
2
|
|
3
3
|
***
|
4
4
|
|
5
|
+
Change log v.0.2.35 (Release Candidate)
|
6
|
+
|
7
|
+
**Update**: Updated / upgraded our RC4 and AES PDF encryption support (for non-password protected PDFs). Credit to Gyuchang Jun (@gyuchang) for his work on providing CombinePDF with this extra encryption support. I have no idea what magic he used to make this happen, but it's beautiful!
|
8
|
+
|
9
|
+
**Release**: This gem had been using a development versioning scheme for far too long. The API is stable enough to switch to a production versioning scheme. This version is expected to be the last 0.x version. Assuming this version will be stable enough, it is expected to be re-released as v.1.0.
|
10
|
+
|
11
|
+
|
12
|
+
***
|
13
|
+
|
14
|
+
Change log v.0.2.34
|
15
|
+
|
16
|
+
**Fix**: issue #44 for wkhtmltopdf compatibility and PDF v.1.2 use of named destinations. Credit to Devin Wadsworth (@daymun) for exposing the issue.
|
17
|
+
|
18
|
+
***
|
19
|
+
|
5
20
|
Change log v.0.2.33
|
6
21
|
|
7
22
|
**Update**: Fix #97 to allow javascript support for interactive objects. Credit to @joshirashmics for exposing the issue.
|
data/lib/combine_pdf/decrypt.rb
CHANGED
@@ -31,8 +31,7 @@ module CombinePDF
|
|
31
31
|
0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
|
32
32
|
0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
|
33
33
|
0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]
|
34
|
-
|
35
|
-
@encryption_iv = nil
|
34
|
+
|
36
35
|
change_references_to_actual_values @encryption_dictionary
|
37
36
|
end
|
38
37
|
|
@@ -45,20 +44,24 @@ module CombinePDF
|
|
45
44
|
# raise_encrypted_error
|
46
45
|
_perform_decrypt_proc_ @objects, method(:decrypt_RC4)
|
47
46
|
when 4
|
48
|
-
# raise unsupported error for now
|
49
|
-
raise_encrypted_error
|
50
47
|
# make sure CF is a Hash (as required by the PDF standard for this type of encryption).
|
51
48
|
raise_encrypted_error unless actual_object(@encryption_dictionary[:CF]).is_a?(Hash)
|
52
49
|
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
# attempt to decrypy all streams
|
58
|
-
# attempt to decrypt all embeded files?
|
50
|
+
# support trivial case for now
|
51
|
+
# - same filter for streams (Stmf) and strings(Strf)
|
52
|
+
# - AND :CFM == :V2 (use algorithm 1)
|
53
|
+
raise_encrypted_error unless (@encryption_dictionary[:StmF] == @encryption_dictionary[:StrF])
|
59
54
|
|
60
|
-
|
61
|
-
raise_encrypted_error
|
55
|
+
cfilter = actual_object(@encryption_dictionary[:CF])[@encryption_dictionary[:StrF]]
|
56
|
+
raise_encrypted_error unless cfilter
|
57
|
+
raise_encrypted_error unless (cfilter[:AuthEvent] == :DocOpen)
|
58
|
+
if (cfilter[:CFM] == :V2)
|
59
|
+
_perform_decrypt_proc_ @objects, method(:decrypt_RC4)
|
60
|
+
elsif (cfilter[:CFM] == :AESV2)
|
61
|
+
_perform_decrypt_proc_ @objects, method(:decrypt_AES)
|
62
|
+
else
|
63
|
+
raise_encrypted_error
|
64
|
+
end
|
62
65
|
end
|
63
66
|
# rebuild stream lengths?
|
64
67
|
@objects
|
@@ -85,11 +88,9 @@ module CombinePDF
|
|
85
88
|
# # 4(a) (Security handlers of revision 4 or greater)
|
86
89
|
# # if document metadata is not being encrypted, add 4 bytes with the value 0xFFFFFFFF.
|
87
90
|
if actual_object(@encryption_dictionary[:R]) >= 4
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
"\x00\x00\x00\x00"
|
92
|
-
end
|
91
|
+
if actual_object(@encryption_dictionary)[:EncryptMetadata] == false
|
92
|
+
key << "\xFF\xFF\xFF\xFF".force_encoding(Encoding::ASCII_8BIT)
|
93
|
+
end
|
93
94
|
end
|
94
95
|
# 5) pass everything as a MD5 hash
|
95
96
|
key = Digest::MD5.digest(key)
|
@@ -132,22 +133,24 @@ module CombinePDF
|
|
132
133
|
end
|
133
134
|
|
134
135
|
def decrypt_AES(encrypted, encrypted_id, encrypted_generation, _encrypted_filter)
|
135
|
-
## extract encryption_iv if it wasn't extracted yet
|
136
|
-
unless @encryption_iv
|
137
|
-
@encryption_iv = encrypted[0..15].to_i
|
138
|
-
# raise "Tryed decrypting using AES and couldn't extract iv" if @encryption_iv == 0
|
139
|
-
@encryption_iv = 0.chr * 16
|
140
|
-
# encrypted = encrypted[16..-1]
|
141
|
-
end
|
142
136
|
## start decryption using padding strings
|
143
137
|
object_key = @key.dup
|
144
|
-
|
145
|
-
|
146
|
-
object_key << 'sAlT'
|
138
|
+
object_key << [encrypted_id].pack('i')[0..2]
|
139
|
+
object_key << [encrypted_generation].pack('i')[0..1]
|
140
|
+
object_key << 'sAlT'.force_encoding(Encoding::ASCII_8BIT)
|
147
141
|
key_length = object_key.length < 16 ? object_key.length : 16
|
148
|
-
|
149
|
-
|
150
|
-
|
142
|
+
|
143
|
+
begin
|
144
|
+
cipher = OpenSSL::Cipher.new("aes-#{key_length << 3}-cbc")
|
145
|
+
cipher.decrypt
|
146
|
+
cipher.key = Digest::MD5.digest(object_key)[(0...key_length)]
|
147
|
+
cipher.iv = encrypted[0..15]
|
148
|
+
cipher.padding = 0
|
149
|
+
cipher.update(encrypted[16..-1]) + cipher.final
|
150
|
+
rescue StandardError => e
|
151
|
+
# puts e.class.name
|
152
|
+
encrypted
|
153
|
+
end
|
151
154
|
end
|
152
155
|
|
153
156
|
protected
|
@@ -159,14 +162,14 @@ module CombinePDF
|
|
159
162
|
encrypted_id ||= actual_object(object[:indirect_reference_id])
|
160
163
|
encrypted_generation ||= actual_object(object[:indirect_generation_number])
|
161
164
|
encrypted_filter ||= actual_object(object[:Filter])
|
162
|
-
if object[:raw_stream_content]
|
165
|
+
if object[:raw_stream_content] && !object[:raw_stream_content].empty?
|
163
166
|
stream_length = actual_object(object[:Length])
|
164
167
|
actual_length = object[:raw_stream_content].bytesize
|
165
168
|
# p stream_length
|
166
169
|
# p actual_length
|
167
170
|
# p object[:Length]
|
168
171
|
# p object
|
169
|
-
warn "Stream
|
172
|
+
warn "Stream registered length was #{object[:Length]} and the actual length was #{actual_length}." if actual_length < stream_length
|
170
173
|
length = [stream_length, actual_length].min
|
171
174
|
object[:raw_stream_content] = decrypt_proc.call((object[:raw_stream_content][0...length]), encrypted_id, encrypted_generation, encrypted_filter)
|
172
175
|
end
|
@@ -182,6 +182,7 @@ module CombinePDF
|
|
182
182
|
# border_width:: border width in PDF units. defaults to nil (none).
|
183
183
|
# box_radius:: border radius in PDF units. defaults to 0 (no corner rounding).
|
184
184
|
# opacity:: textbox opacity, a float between 0 (transparent) and 1 (opaque)
|
185
|
+
# ctm:: A PDF complient CTM data array that will manipulate the axis and allow transformations. i.e. `[1,0,0,1,0,0]`
|
185
186
|
def textbox(text, properties = {})
|
186
187
|
options = {
|
187
188
|
x: 0,
|
@@ -392,6 +393,8 @@ module CombinePDF
|
|
392
393
|
# This method moves the Page[:Rotate] property into the page's data stream, so that
|
393
394
|
# "what you see is what you get".
|
394
395
|
#
|
396
|
+
# After using thie method, {#orientation} should return the absolute orientation rather than only the data's orientation (unless `:Rotate` is changed).
|
397
|
+
#
|
395
398
|
# This is usful in cases where there might be less control over the source PDF files,
|
396
399
|
# and the user assums that the PDF page's data is the same as the PDF's pages
|
397
400
|
# on screen display (Rotate rotates a page but leaves the data in the original orientation).
|
@@ -441,7 +444,7 @@ module CombinePDF
|
|
441
444
|
y_ratio = 1.0 * (new_size[3] - new_size[1]) / (c_size[3]) #-c_size[1])
|
442
445
|
x_move = new_size[0] - c_size[0]
|
443
446
|
y_move = new_size[1] - c_size[1]
|
444
|
-
puts "ctm will be: #{x_ratio.round(4)} 0 0 #{y_ratio.round(4)} #{x_move} #{y_move}"
|
447
|
+
# puts "ctm will be: #{x_ratio.round(4)} 0 0 #{y_ratio.round(4)} #{x_move} #{y_move}"
|
445
448
|
self[:MediaBox] = [(c_mediabox[0] + x_move), (c_mediabox[1] + y_move), ((c_mediabox[2] * x_ratio) + x_move), ((c_mediabox[3] * y_ratio) + y_move)]
|
446
449
|
self[:CropBox] = [(c_cropbox[0] + x_move), (c_cropbox[1] + y_move), ((c_cropbox[2] * x_ratio) + x_move), ((c_cropbox[3] * y_ratio) + y_move)] if c_cropbox
|
447
450
|
x_ratio = y_ratio = [x_ratio, y_ratio].min if conserve_aspect_ratio
|
@@ -506,7 +509,10 @@ module CombinePDF
|
|
506
509
|
fix_rotation
|
507
510
|
end
|
508
511
|
|
509
|
-
# get or set (by clockwise rotation) the page's orientation
|
512
|
+
# get or set (by clockwise rotation) the page's data orientation.
|
513
|
+
#
|
514
|
+
# note that the data's orientation is the way data is oriented on the page.
|
515
|
+
# The display orientati0n (which might different) is controlled by the `:Rotate` property. see {#fix_orientation} for more details.
|
510
516
|
#
|
511
517
|
# accepts one optional parameter:
|
512
518
|
# force:: to get the orientation, pass nil. to set the orientatiom, set fource to either :portrait or :landscape. defaults to nil (get orientation).
|
data/lib/combine_pdf/version.rb
CHANGED
data/test/automated
CHANGED
@@ -57,16 +57,37 @@ CombinePDF.load("./Ruby/test\ pdfs/Scribus-unknown_err3.pdf").save '08_3-unknown
|
|
57
57
|
|
58
58
|
CombinePDF.load("/Users/2Be/Ruby/test\ pdfs/nil_object.pdf").save('09_nil_in_parsed_array.pdf')
|
59
59
|
|
60
|
+
encrypted = [ "./Ruby/test\ pdfs/pdf-reader/encrypted_version4_revision4_128bit_aes_user_pass_apples_enc_metadata.pdf",
|
61
|
+
"./Ruby/test\ pdfs/AESv2\ encrypted.pdf",
|
62
|
+
"./Ruby/test\ pdfs/pdf-reader/encrypted_version2_revision3_128bit_rc4_blank_user_pass.pdf",
|
63
|
+
"./Ruby/test\ pdfs/AES\ enc.pdf",
|
64
|
+
"./Ruby/test\ pdfs/RC4\ enc.pdf"]
|
65
|
+
|
66
|
+
encrypted.length.times do |i|
|
67
|
+
fname = File.basename encrypted[i]
|
68
|
+
begin
|
69
|
+
CombinePDF.load(encrypted[i]).save "10_#{i}_#{fname}"
|
70
|
+
rescue => e
|
71
|
+
puts e.class.name, e.message
|
72
|
+
if(i == 0)
|
73
|
+
puts "CombinePDF expected to fail to read AESv2 #{fname}"
|
74
|
+
else
|
75
|
+
puts "ERROR: CombinePDF failed to open #{fname}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
60
80
|
require 'prawn'
|
61
|
-
IO.binwrite '
|
81
|
+
IO.binwrite '11_prawn.pdf', (Prawn::Document.new { text 'Hello World!' }).render
|
62
82
|
page = CombinePDF.parse((Prawn::Document.new { text 'Hello World!' }).render)
|
63
83
|
pdf = CombinePDF.new
|
64
84
|
pdf << page
|
65
|
-
pdf.save '
|
85
|
+
pdf.save '11_parsed_from_prawn.pdf'
|
66
86
|
pdf = CombinePDF.new
|
67
87
|
pdf << page << page
|
68
|
-
pdf.save('
|
88
|
+
pdf.save('11_AcrobatReader_is_unique_page.pdf')
|
69
89
|
|
90
|
+
puts GC.stat.inspect
|
70
91
|
# unify = [
|
71
92
|
# "./Ruby/test\ pdfs/AESv2\ encrypted.pdf",
|
72
93
|
# "./Ruby/test\ pdfs/data-in-comment.pdf",
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: combine_pdf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.35
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Boaz Segev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-rc4
|