First public commit
This commit is contained in:
5
PdfFileTransformer/PyPDF2/__init__.py
Normal file
5
PdfFileTransformer/PyPDF2/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .pdf import PdfFileReader, PdfFileWriter
|
||||||
|
from .merger import PdfFileMerger
|
||||||
|
from .pagerange import PageRange, parse_filename_page_ranges
|
||||||
|
from ._version import __version__
|
||||||
|
__all__ = ["pdf", "PdfFileMerger"]
|
||||||
1
PdfFileTransformer/PyPDF2/_version.py
Normal file
1
PdfFileTransformer/PyPDF2/_version.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = '1.26.0'
|
||||||
424
PdfFileTransformer/PyPDF2/filters.py
Normal file
424
PdfFileTransformer/PyPDF2/filters.py
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
# vim: sw=4:expandtab:foldmethod=marker
|
||||||
|
#
|
||||||
|
# Copyright (c) 2006, Mathieu Fenniak
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# * The name of the author may not be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Implementation of stream filters for PDF.
|
||||||
|
"""
|
||||||
|
__author__ = "Mathieu Fenniak"
|
||||||
|
__author_email__ = "biziqe@mathieu.fenniak.net"
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from .utils import PdfReadError, ord_, chr_, paethPredictor
|
||||||
|
from sys import version_info
|
||||||
|
if version_info < ( 3, 0 ):
|
||||||
|
from cStringIO import StringIO
|
||||||
|
else:
|
||||||
|
from io import StringIO
|
||||||
|
import struct
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
def decompress(data):
|
||||||
|
return zlib.decompress(data)
|
||||||
|
|
||||||
|
def compress(data):
|
||||||
|
return zlib.compress(data)
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# Unable to import zlib. Attempt to use the System.IO.Compression
|
||||||
|
# library from the .NET framework. (IronPython only)
|
||||||
|
import System
|
||||||
|
from System import IO, Collections, Array
|
||||||
|
|
||||||
|
def _string_to_bytearr(buf):
|
||||||
|
retval = Array.CreateInstance(System.Byte, len(buf))
|
||||||
|
for i in range(len(buf)):
|
||||||
|
retval[i] = ord(buf[i])
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def _bytearr_to_string(bytes):
|
||||||
|
retval = ""
|
||||||
|
for i in range(bytes.Length):
|
||||||
|
retval += chr(bytes[i])
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def _read_bytes(stream):
|
||||||
|
ms = IO.MemoryStream()
|
||||||
|
buf = Array.CreateInstance(System.Byte, 2048)
|
||||||
|
while True:
|
||||||
|
bytes = stream.Read(buf, 0, buf.Length)
|
||||||
|
if bytes == 0:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
ms.Write(buf, 0, bytes)
|
||||||
|
retval = ms.ToArray()
|
||||||
|
ms.Close()
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def decompress(data):
|
||||||
|
bytes = _string_to_bytearr(data)
|
||||||
|
ms = IO.MemoryStream()
|
||||||
|
ms.Write(bytes, 0, bytes.Length)
|
||||||
|
ms.Position = 0 # fseek 0
|
||||||
|
gz = IO.Compression.DeflateStream(ms, IO.Compression.CompressionMode.Decompress)
|
||||||
|
bytes = _read_bytes(gz)
|
||||||
|
retval = _bytearr_to_string(bytes)
|
||||||
|
gz.Close()
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def compress(data):
|
||||||
|
bytes = _string_to_bytearr(data)
|
||||||
|
ms = IO.MemoryStream()
|
||||||
|
gz = IO.Compression.DeflateStream(ms, IO.Compression.CompressionMode.Compress, True)
|
||||||
|
gz.Write(bytes, 0, bytes.Length)
|
||||||
|
gz.Close()
|
||||||
|
ms.Position = 0 # fseek 0
|
||||||
|
bytes = ms.ToArray()
|
||||||
|
retval = _bytearr_to_string(bytes)
|
||||||
|
ms.Close()
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
class FlateDecode(object):
|
||||||
|
def decode(data, decodeParms):
|
||||||
|
data = decompress(data)
|
||||||
|
predictor = 1
|
||||||
|
if decodeParms:
|
||||||
|
try:
|
||||||
|
predictor = decodeParms.get("/Predictor", 1)
|
||||||
|
except AttributeError:
|
||||||
|
pass # usually an array with a null object was read
|
||||||
|
|
||||||
|
# predictor 1 == no predictor
|
||||||
|
if predictor != 1:
|
||||||
|
columns = decodeParms["/Columns"]
|
||||||
|
# PNG prediction:
|
||||||
|
if predictor >= 10 and predictor <= 15:
|
||||||
|
output = StringIO()
|
||||||
|
# PNG prediction can vary from row to row
|
||||||
|
rowlength = columns + 1
|
||||||
|
assert len(data) % rowlength == 0
|
||||||
|
prev_rowdata = (0,) * rowlength
|
||||||
|
for row in range(len(data) // rowlength):
|
||||||
|
rowdata = [ord_(x) for x in data[(row*rowlength):((row+1)*rowlength)]]
|
||||||
|
filterByte = rowdata[0]
|
||||||
|
if filterByte == 0:
|
||||||
|
pass
|
||||||
|
elif filterByte == 1:
|
||||||
|
for i in range(2, rowlength):
|
||||||
|
rowdata[i] = (rowdata[i] + rowdata[i-1]) % 256
|
||||||
|
elif filterByte == 2:
|
||||||
|
for i in range(1, rowlength):
|
||||||
|
rowdata[i] = (rowdata[i] + prev_rowdata[i]) % 256
|
||||||
|
elif filterByte == 3:
|
||||||
|
for i in range(1, rowlength):
|
||||||
|
left = rowdata[i-1] if i > 1 else 0
|
||||||
|
floor = math.floor(left + prev_rowdata[i])/2
|
||||||
|
rowdata[i] = (rowdata[i] + int(floor)) % 256
|
||||||
|
elif filterByte == 4:
|
||||||
|
for i in range(1, rowlength):
|
||||||
|
left = rowdata[i - 1] if i > 1 else 0
|
||||||
|
up = prev_rowdata[i]
|
||||||
|
up_left = prev_rowdata[i - 1] if i > 1 else 0
|
||||||
|
paeth = paethPredictor(left, up, up_left)
|
||||||
|
rowdata[i] = (rowdata[i] + paeth) % 256
|
||||||
|
else:
|
||||||
|
# unsupported PNG filter
|
||||||
|
raise PdfReadError("Unsupported PNG filter %r" % filterByte)
|
||||||
|
prev_rowdata = rowdata
|
||||||
|
output.write(''.join([chr(x) for x in rowdata[1:]]))
|
||||||
|
data = output.getvalue()
|
||||||
|
else:
|
||||||
|
# unsupported predictor
|
||||||
|
raise PdfReadError("Unsupported flatedecode predictor %r" % predictor)
|
||||||
|
return data
|
||||||
|
decode = staticmethod(decode)
|
||||||
|
|
||||||
|
def encode(data):
|
||||||
|
return compress(data)
|
||||||
|
encode = staticmethod(encode)
|
||||||
|
|
||||||
|
|
||||||
|
class ASCIIHexDecode(object):
|
||||||
|
def decode(data, decodeParms=None):
|
||||||
|
retval = ""
|
||||||
|
char = ""
|
||||||
|
x = 0
|
||||||
|
while True:
|
||||||
|
c = data[x]
|
||||||
|
if c == ">":
|
||||||
|
break
|
||||||
|
elif c.isspace():
|
||||||
|
x += 1
|
||||||
|
continue
|
||||||
|
char += c
|
||||||
|
if len(char) == 2:
|
||||||
|
retval += chr(int(char, base=16))
|
||||||
|
char = ""
|
||||||
|
x += 1
|
||||||
|
assert char == ""
|
||||||
|
return retval
|
||||||
|
decode = staticmethod(decode)
|
||||||
|
|
||||||
|
|
||||||
|
class LZWDecode(object):
|
||||||
|
"""Taken from:
|
||||||
|
http://www.java2s.com/Open-Source/Java-Document/PDF/PDF-Renderer/com/sun/pdfview/decode/LZWDecode.java.htm
|
||||||
|
"""
|
||||||
|
class decoder(object):
|
||||||
|
def __init__(self, data):
|
||||||
|
self.STOP=257
|
||||||
|
self.CLEARDICT=256
|
||||||
|
self.data=data
|
||||||
|
self.bytepos=0
|
||||||
|
self.bitpos=0
|
||||||
|
self.dict=[""]*4096
|
||||||
|
for i in range(256):
|
||||||
|
self.dict[i]=chr(i)
|
||||||
|
self.resetDict()
|
||||||
|
|
||||||
|
def resetDict(self):
|
||||||
|
self.dictlen=258
|
||||||
|
self.bitspercode=9
|
||||||
|
|
||||||
|
def nextCode(self):
|
||||||
|
fillbits=self.bitspercode
|
||||||
|
value=0
|
||||||
|
while fillbits>0 :
|
||||||
|
if self.bytepos >= len(self.data):
|
||||||
|
return -1
|
||||||
|
nextbits=ord_(self.data[self.bytepos])
|
||||||
|
bitsfromhere=8-self.bitpos
|
||||||
|
if bitsfromhere>fillbits:
|
||||||
|
bitsfromhere=fillbits
|
||||||
|
value |= (((nextbits >> (8-self.bitpos-bitsfromhere)) &
|
||||||
|
(0xff >> (8-bitsfromhere))) <<
|
||||||
|
(fillbits-bitsfromhere))
|
||||||
|
fillbits -= bitsfromhere
|
||||||
|
self.bitpos += bitsfromhere
|
||||||
|
if self.bitpos >=8:
|
||||||
|
self.bitpos=0
|
||||||
|
self.bytepos = self.bytepos+1
|
||||||
|
return value
|
||||||
|
|
||||||
|
def decode(self):
|
||||||
|
""" algorithm derived from:
|
||||||
|
http://www.rasip.fer.hr/research/compress/algorithms/fund/lz/lzw.html
|
||||||
|
and the PDFReference
|
||||||
|
"""
|
||||||
|
cW = self.CLEARDICT;
|
||||||
|
baos=""
|
||||||
|
while True:
|
||||||
|
pW = cW;
|
||||||
|
cW = self.nextCode();
|
||||||
|
if cW == -1:
|
||||||
|
raise PdfReadError("Missed the stop code in LZWDecode!")
|
||||||
|
if cW == self.STOP:
|
||||||
|
break;
|
||||||
|
elif cW == self.CLEARDICT:
|
||||||
|
self.resetDict();
|
||||||
|
elif pW == self.CLEARDICT:
|
||||||
|
baos+=self.dict[cW]
|
||||||
|
else:
|
||||||
|
if cW < self.dictlen:
|
||||||
|
baos += self.dict[cW]
|
||||||
|
p=self.dict[pW]+self.dict[cW][0]
|
||||||
|
self.dict[self.dictlen]=p
|
||||||
|
self.dictlen+=1
|
||||||
|
else:
|
||||||
|
p=self.dict[pW]+self.dict[pW][0]
|
||||||
|
baos+=p
|
||||||
|
self.dict[self.dictlen] = p;
|
||||||
|
self.dictlen+=1
|
||||||
|
if (self.dictlen >= (1 << self.bitspercode) - 1 and
|
||||||
|
self.bitspercode < 12):
|
||||||
|
self.bitspercode+=1
|
||||||
|
return baos
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode(data,decodeParams=None):
|
||||||
|
return LZWDecode.decoder(data).decode()
|
||||||
|
|
||||||
|
|
||||||
|
class ASCII85Decode(object):
|
||||||
|
def decode(data, decodeParms=None):
|
||||||
|
if version_info < ( 3, 0 ):
|
||||||
|
retval = ""
|
||||||
|
group = []
|
||||||
|
x = 0
|
||||||
|
hitEod = False
|
||||||
|
# remove all whitespace from data
|
||||||
|
data = [y for y in data if not (y in ' \n\r\t')]
|
||||||
|
while not hitEod:
|
||||||
|
c = data[x]
|
||||||
|
if len(retval) == 0 and c == "<" and data[x+1] == "~":
|
||||||
|
x += 2
|
||||||
|
continue
|
||||||
|
#elif c.isspace():
|
||||||
|
# x += 1
|
||||||
|
# continue
|
||||||
|
elif c == 'z':
|
||||||
|
assert len(group) == 0
|
||||||
|
retval += '\x00\x00\x00\x00'
|
||||||
|
x += 1
|
||||||
|
continue
|
||||||
|
elif c == "~" and data[x+1] == ">":
|
||||||
|
if len(group) != 0:
|
||||||
|
# cannot have a final group of just 1 char
|
||||||
|
assert len(group) > 1
|
||||||
|
cnt = len(group) - 1
|
||||||
|
group += [ 85, 85, 85 ]
|
||||||
|
hitEod = cnt
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
c = ord(c) - 33
|
||||||
|
assert c >= 0 and c < 85
|
||||||
|
group += [ c ]
|
||||||
|
if len(group) >= 5:
|
||||||
|
b = group[0] * (85**4) + \
|
||||||
|
group[1] * (85**3) + \
|
||||||
|
group[2] * (85**2) + \
|
||||||
|
group[3] * 85 + \
|
||||||
|
group[4]
|
||||||
|
assert b < (2**32 - 1)
|
||||||
|
c4 = chr((b >> 0) % 256)
|
||||||
|
c3 = chr((b >> 8) % 256)
|
||||||
|
c2 = chr((b >> 16) % 256)
|
||||||
|
c1 = chr(b >> 24)
|
||||||
|
retval += (c1 + c2 + c3 + c4)
|
||||||
|
if hitEod:
|
||||||
|
retval = retval[:-4+hitEod]
|
||||||
|
group = []
|
||||||
|
x += 1
|
||||||
|
return retval
|
||||||
|
else:
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode('ascii')
|
||||||
|
n = b = 0
|
||||||
|
out = bytearray()
|
||||||
|
for c in data:
|
||||||
|
if ord('!') <= c and c <= ord('u'):
|
||||||
|
n += 1
|
||||||
|
b = b*85+(c-33)
|
||||||
|
if n == 5:
|
||||||
|
out += struct.pack(b'>L',b)
|
||||||
|
n = b = 0
|
||||||
|
elif c == ord('z'):
|
||||||
|
assert n == 0
|
||||||
|
out += b'\0\0\0\0'
|
||||||
|
elif c == ord('~'):
|
||||||
|
if n:
|
||||||
|
for _ in range(5-n):
|
||||||
|
b = b*85+84
|
||||||
|
out += struct.pack(b'>L',b)[:n-1]
|
||||||
|
break
|
||||||
|
return bytes(out)
|
||||||
|
decode = staticmethod(decode)
|
||||||
|
|
||||||
|
class DCTDecode(object):
|
||||||
|
def decode(data, decodeParms=None):
|
||||||
|
return data
|
||||||
|
decode = staticmethod(decode)
|
||||||
|
|
||||||
|
class JPXDecode(object):
|
||||||
|
def decode(data, decodeParms=None):
|
||||||
|
return data
|
||||||
|
decode = staticmethod(decode)
|
||||||
|
|
||||||
|
class CCITTFaxDecode(object):
|
||||||
|
def decode(data, decodeParms=None, height=0):
|
||||||
|
if decodeParms:
|
||||||
|
if decodeParms.get("/K", 1) == -1:
|
||||||
|
CCITTgroup = 4
|
||||||
|
else:
|
||||||
|
CCITTgroup = 3
|
||||||
|
|
||||||
|
width = decodeParms["/Columns"]
|
||||||
|
imgSize = len(data)
|
||||||
|
tiff_header_struct = '<' + '2s' + 'h' + 'l' + 'h' + 'hhll' * 8 + 'h'
|
||||||
|
tiffHeader = struct.pack(tiff_header_struct,
|
||||||
|
b'II', # Byte order indication: Little endian
|
||||||
|
42, # Version number (always 42)
|
||||||
|
8, # Offset to first IFD
|
||||||
|
8, # Number of tags in IFD
|
||||||
|
256, 4, 1, width, # ImageWidth, LONG, 1, width
|
||||||
|
257, 4, 1, height, # ImageLength, LONG, 1, length
|
||||||
|
258, 3, 1, 1, # BitsPerSample, SHORT, 1, 1
|
||||||
|
259, 3, 1, CCITTgroup, # Compression, SHORT, 1, 4 = CCITT Group 4 fax encoding
|
||||||
|
262, 3, 1, 0, # Thresholding, SHORT, 1, 0 = WhiteIsZero
|
||||||
|
273, 4, 1, struct.calcsize(tiff_header_struct), # StripOffsets, LONG, 1, length of header
|
||||||
|
278, 4, 1, height, # RowsPerStrip, LONG, 1, length
|
||||||
|
279, 4, 1, imgSize, # StripByteCounts, LONG, 1, size of image
|
||||||
|
0 # last IFD
|
||||||
|
)
|
||||||
|
|
||||||
|
return tiffHeader + data
|
||||||
|
|
||||||
|
decode = staticmethod(decode)
|
||||||
|
|
||||||
|
def decodeStreamData(stream):
|
||||||
|
from .generic import NameObject
|
||||||
|
filters = stream.get("/Filter", ())
|
||||||
|
|
||||||
|
if len(filters) and not isinstance(filters[0], NameObject):
|
||||||
|
# we have a single filter instance
|
||||||
|
filters = (filters,)
|
||||||
|
data = stream._data
|
||||||
|
# If there is not data to decode we should not try to decode the data.
|
||||||
|
if data:
|
||||||
|
for filterType in filters:
|
||||||
|
if filterType == "/FlateDecode" or filterType == "/Fl":
|
||||||
|
data = FlateDecode.decode(data, stream.get("/DecodeParms"))
|
||||||
|
elif filterType == "/ASCIIHexDecode" or filterType == "/AHx":
|
||||||
|
data = ASCIIHexDecode.decode(data)
|
||||||
|
elif filterType == "/LZWDecode" or filterType == "/LZW":
|
||||||
|
data = LZWDecode.decode(data, stream.get("/DecodeParms"))
|
||||||
|
elif filterType == "/ASCII85Decode" or filterType == "/A85":
|
||||||
|
data = ASCII85Decode.decode(data)
|
||||||
|
elif filterType == "/DCTDecode":
|
||||||
|
data = DCTDecode.decode(data)
|
||||||
|
elif filterType == "/JPXDecode":
|
||||||
|
data = JPXDecode.decode(data)
|
||||||
|
elif filterType == "/CCITTFaxDecode":
|
||||||
|
height = stream.get("/Height", ())
|
||||||
|
data = CCITTFaxDecode.decode(data, stream.get("/DecodeParms"), height)
|
||||||
|
elif filterType == "/Crypt":
|
||||||
|
decodeParams = stream.get("/DecodeParams", {})
|
||||||
|
if "/Name" not in decodeParams and "/Type" not in decodeParams:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("/Crypt filter with /Name or /Type not supported yet")
|
||||||
|
else:
|
||||||
|
# unsupported filter
|
||||||
|
raise NotImplementedError("unsupported filter %s" % filterType)
|
||||||
|
return data
|
||||||
1228
PdfFileTransformer/PyPDF2/generic.py
Normal file
1228
PdfFileTransformer/PyPDF2/generic.py
Normal file
File diff suppressed because it is too large
Load Diff
553
PdfFileTransformer/PyPDF2/merger.py
Normal file
553
PdfFileTransformer/PyPDF2/merger.py
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
# vim: sw=4:expandtab:foldmethod=marker
|
||||||
|
#
|
||||||
|
# Copyright (c) 2006, Mathieu Fenniak
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# * The name of the author may not be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from .generic import *
|
||||||
|
from .utils import isString, str_
|
||||||
|
from .pdf import PdfFileReader, PdfFileWriter
|
||||||
|
from .pagerange import PageRange
|
||||||
|
from sys import version_info
|
||||||
|
if version_info < ( 3, 0 ):
|
||||||
|
from cStringIO import StringIO
|
||||||
|
StreamIO = StringIO
|
||||||
|
else:
|
||||||
|
from io import BytesIO
|
||||||
|
from io import FileIO as file
|
||||||
|
StreamIO = BytesIO
|
||||||
|
|
||||||
|
|
||||||
|
class _MergedPage(object):
|
||||||
|
"""
|
||||||
|
_MergedPage is used internally by PdfFileMerger to collect necessary
|
||||||
|
information on each page that is being merged.
|
||||||
|
"""
|
||||||
|
def __init__(self, pagedata, src, id):
|
||||||
|
self.src = src
|
||||||
|
self.pagedata = pagedata
|
||||||
|
self.out_pagedata = None
|
||||||
|
self.id = id
|
||||||
|
|
||||||
|
|
||||||
|
class PdfFileMerger(object):
|
||||||
|
"""
|
||||||
|
Initializes a PdfFileMerger object. PdfFileMerger merges multiple PDFs
|
||||||
|
into a single PDF. It can concatenate, slice, insert, or any combination
|
||||||
|
of the above.
|
||||||
|
|
||||||
|
See the functions :meth:`merge()<merge>` (or :meth:`append()<append>`)
|
||||||
|
and :meth:`write()<write>` for usage information.
|
||||||
|
|
||||||
|
:param bool strict: Determines whether user should be warned of all
|
||||||
|
problems and also causes some correctable problems to be fatal.
|
||||||
|
Defaults to ``True``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, strict=True):
|
||||||
|
self.inputs = []
|
||||||
|
self.pages = []
|
||||||
|
self.output = PdfFileWriter()
|
||||||
|
self.bookmarks = []
|
||||||
|
self.named_dests = []
|
||||||
|
self.id_count = 0
|
||||||
|
self.strict = strict
|
||||||
|
|
||||||
|
def merge(self, position, fileobj, bookmark=None, pages=None, import_bookmarks=True):
|
||||||
|
"""
|
||||||
|
Merges the pages from the given file into the output file at the
|
||||||
|
specified page number.
|
||||||
|
|
||||||
|
:param int position: The *page number* to insert this file. File will
|
||||||
|
be inserted after the given number.
|
||||||
|
|
||||||
|
:param fileobj: A File Object or an object that supports the standard read
|
||||||
|
and seek methods similar to a File Object. Could also be a
|
||||||
|
string representing a path to a PDF file.
|
||||||
|
|
||||||
|
:param str bookmark: Optionally, you may specify a bookmark to be applied at
|
||||||
|
the beginning of the included file by supplying the text of the bookmark.
|
||||||
|
|
||||||
|
:param pages: can be a :ref:`Page Range <page-range>` or a ``(start, stop[, step])`` tuple
|
||||||
|
to merge only the specified range of pages from the source
|
||||||
|
document into the output document.
|
||||||
|
|
||||||
|
:param bool import_bookmarks: You may prevent the source document's bookmarks
|
||||||
|
from being imported by specifying this as ``False``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This parameter is passed to self.inputs.append and means
|
||||||
|
# that the stream used was created in this method.
|
||||||
|
my_file = False
|
||||||
|
|
||||||
|
# If the fileobj parameter is a string, assume it is a path
|
||||||
|
# and create a file object at that location. If it is a file,
|
||||||
|
# copy the file's contents into a BytesIO (or StreamIO) stream object; if
|
||||||
|
# it is a PdfFileReader, copy that reader's stream into a
|
||||||
|
# BytesIO (or StreamIO) stream.
|
||||||
|
# If fileobj is none of the above types, it is not modified
|
||||||
|
decryption_key = None
|
||||||
|
if isString(fileobj):
|
||||||
|
fileobj = file(fileobj, 'rb')
|
||||||
|
my_file = True
|
||||||
|
elif hasattr(fileobj, "seek") and hasattr(fileobj, "read"):
|
||||||
|
fileobj.seek(0)
|
||||||
|
filecontent = fileobj.read()
|
||||||
|
fileobj = StreamIO(filecontent)
|
||||||
|
my_file = True
|
||||||
|
elif isinstance(fileobj, PdfFileReader):
|
||||||
|
orig_tell = fileobj.stream.tell()
|
||||||
|
fileobj.stream.seek(0)
|
||||||
|
filecontent = StreamIO(fileobj.stream.read())
|
||||||
|
fileobj.stream.seek(orig_tell) # reset the stream to its original location
|
||||||
|
fileobj = filecontent
|
||||||
|
if hasattr(fileobj, '_decryption_key'):
|
||||||
|
decryption_key = fileobj._decryption_key
|
||||||
|
my_file = True
|
||||||
|
|
||||||
|
# Create a new PdfFileReader instance using the stream
|
||||||
|
# (either file or BytesIO or StringIO) created above
|
||||||
|
pdfr = PdfFileReader(fileobj, strict=self.strict)
|
||||||
|
if decryption_key is not None:
|
||||||
|
pdfr._decryption_key = decryption_key
|
||||||
|
|
||||||
|
# Find the range of pages to merge.
|
||||||
|
if pages == None:
|
||||||
|
pages = (0, pdfr.getNumPages())
|
||||||
|
elif isinstance(pages, PageRange):
|
||||||
|
pages = pages.indices(pdfr.getNumPages())
|
||||||
|
elif not isinstance(pages, tuple):
|
||||||
|
raise TypeError('"pages" must be a tuple of (start, stop[, step])')
|
||||||
|
|
||||||
|
srcpages = []
|
||||||
|
if bookmark:
|
||||||
|
bookmark = Bookmark(TextStringObject(bookmark), NumberObject(self.id_count), NameObject('/Fit'))
|
||||||
|
|
||||||
|
outline = []
|
||||||
|
if import_bookmarks:
|
||||||
|
outline = pdfr.getOutlines()
|
||||||
|
outline = self._trim_outline(pdfr, outline, pages)
|
||||||
|
|
||||||
|
if bookmark:
|
||||||
|
self.bookmarks += [bookmark, outline]
|
||||||
|
else:
|
||||||
|
self.bookmarks += outline
|
||||||
|
|
||||||
|
dests = pdfr.namedDestinations
|
||||||
|
dests = self._trim_dests(pdfr, dests, pages)
|
||||||
|
self.named_dests += dests
|
||||||
|
|
||||||
|
# Gather all the pages that are going to be merged
|
||||||
|
for i in range(*pages):
|
||||||
|
pg = pdfr.getPage(i)
|
||||||
|
|
||||||
|
id = self.id_count
|
||||||
|
self.id_count += 1
|
||||||
|
|
||||||
|
mp = _MergedPage(pg, pdfr, id)
|
||||||
|
|
||||||
|
srcpages.append(mp)
|
||||||
|
|
||||||
|
self._associate_dests_to_pages(srcpages)
|
||||||
|
self._associate_bookmarks_to_pages(srcpages)
|
||||||
|
|
||||||
|
# Slice to insert the pages at the specified position
|
||||||
|
self.pages[position:position] = srcpages
|
||||||
|
|
||||||
|
# Keep track of our input files so we can close them later
|
||||||
|
self.inputs.append((fileobj, pdfr, my_file))
|
||||||
|
|
||||||
|
def append(self, fileobj, bookmark=None, pages=None, import_bookmarks=True):
|
||||||
|
"""
|
||||||
|
Identical to the :meth:`merge()<merge>` method, but assumes you want to concatenate
|
||||||
|
all pages onto the end of the file instead of specifying a position.
|
||||||
|
|
||||||
|
:param fileobj: A File Object or an object that supports the standard read
|
||||||
|
and seek methods similar to a File Object. Could also be a
|
||||||
|
string representing a path to a PDF file.
|
||||||
|
|
||||||
|
:param str bookmark: Optionally, you may specify a bookmark to be applied at
|
||||||
|
the beginning of the included file by supplying the text of the bookmark.
|
||||||
|
|
||||||
|
:param pages: can be a :ref:`Page Range <page-range>` or a ``(start, stop[, step])`` tuple
|
||||||
|
to merge only the specified range of pages from the source
|
||||||
|
document into the output document.
|
||||||
|
|
||||||
|
:param bool import_bookmarks: You may prevent the source document's bookmarks
|
||||||
|
from being imported by specifying this as ``False``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.merge(len(self.pages), fileobj, bookmark, pages, import_bookmarks)
|
||||||
|
|
||||||
|
def write(self, fileobj):
|
||||||
|
"""
|
||||||
|
Writes all data that has been merged to the given output file.
|
||||||
|
|
||||||
|
:param fileobj: Output file. Can be a filename or any kind of
|
||||||
|
file-like object.
|
||||||
|
"""
|
||||||
|
my_file = False
|
||||||
|
if isString(fileobj):
|
||||||
|
fileobj = file(fileobj, 'wb')
|
||||||
|
my_file = True
|
||||||
|
|
||||||
|
# Add pages to the PdfFileWriter
|
||||||
|
# The commented out line below was replaced with the two lines below it to allow PdfFileMerger to work with PyPdf 1.13
|
||||||
|
for page in self.pages:
|
||||||
|
self.output.addPage(page.pagedata)
|
||||||
|
page.out_pagedata = self.output.getReference(self.output._pages.getObject()["/Kids"][-1].getObject())
|
||||||
|
#idnum = self.output._objects.index(self.output._pages.getObject()["/Kids"][-1].getObject()) + 1
|
||||||
|
#page.out_pagedata = IndirectObject(idnum, 0, self.output)
|
||||||
|
|
||||||
|
# Once all pages are added, create bookmarks to point at those pages
|
||||||
|
self._write_dests()
|
||||||
|
self._write_bookmarks()
|
||||||
|
|
||||||
|
# Write the output to the file
|
||||||
|
self.output.write(fileobj)
|
||||||
|
|
||||||
|
if my_file:
|
||||||
|
fileobj.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Shuts all file descriptors (input and output) and clears all memory
|
||||||
|
usage.
|
||||||
|
"""
|
||||||
|
self.pages = []
|
||||||
|
for fo, pdfr, mine in self.inputs:
|
||||||
|
if mine:
|
||||||
|
fo.close()
|
||||||
|
|
||||||
|
self.inputs = []
|
||||||
|
self.output = None
|
||||||
|
|
||||||
|
def addMetadata(self, infos):
|
||||||
|
"""
|
||||||
|
Add custom metadata to the output.
|
||||||
|
|
||||||
|
:param dict infos: a Python dictionary where each key is a field
|
||||||
|
and each value is your new metadata.
|
||||||
|
Example: ``{u'/Title': u'My title'}``
|
||||||
|
"""
|
||||||
|
self.output.addMetadata(infos)
|
||||||
|
|
||||||
|
def setPageLayout(self, layout):
|
||||||
|
"""
|
||||||
|
Set the page layout
|
||||||
|
|
||||||
|
:param str layout: The page layout to be used
|
||||||
|
|
||||||
|
Valid layouts are:
|
||||||
|
/NoLayout Layout explicitly not specified
|
||||||
|
/SinglePage Show one page at a time
|
||||||
|
/OneColumn Show one column at a time
|
||||||
|
/TwoColumnLeft Show pages in two columns, odd-numbered pages on the left
|
||||||
|
/TwoColumnRight Show pages in two columns, odd-numbered pages on the right
|
||||||
|
/TwoPageLeft Show two pages at a time, odd-numbered pages on the left
|
||||||
|
/TwoPageRight Show two pages at a time, odd-numbered pages on the right
|
||||||
|
"""
|
||||||
|
self.output.setPageLayout(layout)
|
||||||
|
|
||||||
|
def setPageMode(self, mode):
|
||||||
|
"""
|
||||||
|
Set the page mode.
|
||||||
|
|
||||||
|
:param str mode: The page mode to use.
|
||||||
|
|
||||||
|
Valid modes are:
|
||||||
|
/UseNone Do not show outlines or thumbnails panels
|
||||||
|
/UseOutlines Show outlines (aka bookmarks) panel
|
||||||
|
/UseThumbs Show page thumbnails panel
|
||||||
|
/FullScreen Fullscreen view
|
||||||
|
/UseOC Show Optional Content Group (OCG) panel
|
||||||
|
/UseAttachments Show attachments panel
|
||||||
|
"""
|
||||||
|
self.output.setPageMode(mode)
|
||||||
|
|
||||||
|
def _trim_dests(self, pdf, dests, pages):
|
||||||
|
"""
|
||||||
|
Removes any named destinations that are not a part of the specified
|
||||||
|
page set.
|
||||||
|
"""
|
||||||
|
new_dests = []
|
||||||
|
prev_header_added = True
|
||||||
|
for k, o in list(dests.items()):
|
||||||
|
for j in range(*pages):
|
||||||
|
if pdf.getPage(j).getObject() == o['/Page'].getObject():
|
||||||
|
o[NameObject('/Page')] = o['/Page'].getObject()
|
||||||
|
assert str_(k) == str_(o['/Title'])
|
||||||
|
new_dests.append(o)
|
||||||
|
break
|
||||||
|
return new_dests
|
||||||
|
|
||||||
|
def _trim_outline(self, pdf, outline, pages):
|
||||||
|
"""
|
||||||
|
Removes any outline/bookmark entries that are not a part of the
|
||||||
|
specified page set.
|
||||||
|
"""
|
||||||
|
new_outline = []
|
||||||
|
prev_header_added = True
|
||||||
|
for i, o in enumerate(outline):
|
||||||
|
if isinstance(o, list):
|
||||||
|
sub = self._trim_outline(pdf, o, pages)
|
||||||
|
if sub:
|
||||||
|
if not prev_header_added:
|
||||||
|
new_outline.append(outline[i-1])
|
||||||
|
new_outline.append(sub)
|
||||||
|
else:
|
||||||
|
prev_header_added = False
|
||||||
|
for j in range(*pages):
|
||||||
|
if pdf.getPage(j).getObject() == o['/Page'].getObject():
|
||||||
|
o[NameObject('/Page')] = o['/Page'].getObject()
|
||||||
|
new_outline.append(o)
|
||||||
|
prev_header_added = True
|
||||||
|
break
|
||||||
|
return new_outline
|
||||||
|
|
||||||
|
def _write_dests(self):
|
||||||
|
dests = self.named_dests
|
||||||
|
|
||||||
|
for v in dests:
|
||||||
|
pageno = None
|
||||||
|
pdf = None
|
||||||
|
if '/Page' in v:
|
||||||
|
for i, p in enumerate(self.pages):
|
||||||
|
if p.id == v['/Page']:
|
||||||
|
v[NameObject('/Page')] = p.out_pagedata
|
||||||
|
pageno = i
|
||||||
|
pdf = p.src
|
||||||
|
break
|
||||||
|
if pageno != None:
|
||||||
|
self.output.addNamedDestinationObject(v)
|
||||||
|
|
||||||
|
def _write_bookmarks(self, bookmarks=None, parent=None):
|
||||||
|
|
||||||
|
if bookmarks == None:
|
||||||
|
bookmarks = self.bookmarks
|
||||||
|
|
||||||
|
last_added = None
|
||||||
|
for b in bookmarks:
|
||||||
|
if isinstance(b, list):
|
||||||
|
self._write_bookmarks(b, last_added)
|
||||||
|
continue
|
||||||
|
|
||||||
|
pageno = None
|
||||||
|
pdf = None
|
||||||
|
if '/Page' in b:
|
||||||
|
for i, p in enumerate(self.pages):
|
||||||
|
if p.id == b['/Page']:
|
||||||
|
#b[NameObject('/Page')] = p.out_pagedata
|
||||||
|
args = [NumberObject(p.id), NameObject(b['/Type'])]
|
||||||
|
#nothing more to add
|
||||||
|
#if b['/Type'] == '/Fit' or b['/Type'] == '/FitB'
|
||||||
|
if b['/Type'] == '/FitH' or b['/Type'] == '/FitBH':
|
||||||
|
if '/Top' in b and not isinstance(b['/Top'], NullObject):
|
||||||
|
args.append(FloatObject(b['/Top']))
|
||||||
|
else:
|
||||||
|
args.append(FloatObject(0))
|
||||||
|
del b['/Top']
|
||||||
|
elif b['/Type'] == '/FitV' or b['/Type'] == '/FitBV':
|
||||||
|
if '/Left' in b and not isinstance(b['/Left'], NullObject):
|
||||||
|
args.append(FloatObject(b['/Left']))
|
||||||
|
else:
|
||||||
|
args.append(FloatObject(0))
|
||||||
|
del b['/Left']
|
||||||
|
elif b['/Type'] == '/XYZ':
|
||||||
|
if '/Left' in b and not isinstance(b['/Left'], NullObject):
|
||||||
|
args.append(FloatObject(b['/Left']))
|
||||||
|
else:
|
||||||
|
args.append(FloatObject(0))
|
||||||
|
if '/Top' in b and not isinstance(b['/Top'], NullObject):
|
||||||
|
args.append(FloatObject(b['/Top']))
|
||||||
|
else:
|
||||||
|
args.append(FloatObject(0))
|
||||||
|
if '/Zoom' in b and not isinstance(b['/Zoom'], NullObject):
|
||||||
|
args.append(FloatObject(b['/Zoom']))
|
||||||
|
else:
|
||||||
|
args.append(FloatObject(0))
|
||||||
|
del b['/Top'], b['/Zoom'], b['/Left']
|
||||||
|
elif b['/Type'] == '/FitR':
|
||||||
|
if '/Left' in b and not isinstance(b['/Left'], NullObject):
|
||||||
|
args.append(FloatObject(b['/Left']))
|
||||||
|
else:
|
||||||
|
args.append(FloatObject(0))
|
||||||
|
if '/Bottom' in b and not isinstance(b['/Bottom'], NullObject):
|
||||||
|
args.append(FloatObject(b['/Bottom']))
|
||||||
|
else:
|
||||||
|
args.append(FloatObject(0))
|
||||||
|
if '/Right' in b and not isinstance(b['/Right'], NullObject):
|
||||||
|
args.append(FloatObject(b['/Right']))
|
||||||
|
else:
|
||||||
|
args.append(FloatObject(0))
|
||||||
|
if '/Top' in b and not isinstance(b['/Top'], NullObject):
|
||||||
|
args.append(FloatObject(b['/Top']))
|
||||||
|
else:
|
||||||
|
args.append(FloatObject(0))
|
||||||
|
del b['/Left'], b['/Right'], b['/Bottom'], b['/Top']
|
||||||
|
|
||||||
|
b[NameObject('/A')] = DictionaryObject({NameObject('/S'): NameObject('/GoTo'), NameObject('/D'): ArrayObject(args)})
|
||||||
|
|
||||||
|
pageno = i
|
||||||
|
pdf = p.src
|
||||||
|
break
|
||||||
|
if pageno != None:
|
||||||
|
del b['/Page'], b['/Type']
|
||||||
|
last_added = self.output.addBookmarkDict(b, parent)
|
||||||
|
|
||||||
|
def _associate_dests_to_pages(self, pages):
|
||||||
|
for nd in self.named_dests:
|
||||||
|
pageno = None
|
||||||
|
np = nd['/Page']
|
||||||
|
|
||||||
|
if isinstance(np, NumberObject):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for p in pages:
|
||||||
|
if np.getObject() == p.pagedata.getObject():
|
||||||
|
pageno = p.id
|
||||||
|
|
||||||
|
if pageno != None:
|
||||||
|
nd[NameObject('/Page')] = NumberObject(pageno)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unresolved named destination '%s'" % (nd['/Title'],))
|
||||||
|
|
||||||
|
def _associate_bookmarks_to_pages(self, pages, bookmarks=None):
|
||||||
|
if bookmarks == None:
|
||||||
|
bookmarks = self.bookmarks
|
||||||
|
|
||||||
|
for b in bookmarks:
|
||||||
|
if isinstance(b, list):
|
||||||
|
self._associate_bookmarks_to_pages(pages, b)
|
||||||
|
continue
|
||||||
|
|
||||||
|
pageno = None
|
||||||
|
bp = b['/Page']
|
||||||
|
|
||||||
|
if isinstance(bp, NumberObject):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for p in pages:
|
||||||
|
if bp.getObject() == p.pagedata.getObject():
|
||||||
|
pageno = p.id
|
||||||
|
|
||||||
|
if pageno != None:
|
||||||
|
b[NameObject('/Page')] = NumberObject(pageno)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unresolved bookmark '%s'" % (b['/Title'],))
|
||||||
|
|
||||||
|
def findBookmark(self, bookmark, root=None):
|
||||||
|
if root == None:
|
||||||
|
root = self.bookmarks
|
||||||
|
|
||||||
|
for i, b in enumerate(root):
|
||||||
|
if isinstance(b, list):
|
||||||
|
res = self.findBookmark(bookmark, b)
|
||||||
|
if res:
|
||||||
|
return [i] + res
|
||||||
|
elif b == bookmark or b['/Title'] == bookmark:
|
||||||
|
return [i]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def addBookmark(self, title, pagenum, parent=None):
|
||||||
|
"""
|
||||||
|
Add a bookmark to this PDF file.
|
||||||
|
|
||||||
|
:param str title: Title to use for this bookmark.
|
||||||
|
:param int pagenum: Page number this bookmark will point to.
|
||||||
|
:param parent: A reference to a parent bookmark to create nested
|
||||||
|
bookmarks.
|
||||||
|
"""
|
||||||
|
if parent == None:
|
||||||
|
iloc = [len(self.bookmarks)-1]
|
||||||
|
elif isinstance(parent, list):
|
||||||
|
iloc = parent
|
||||||
|
else:
|
||||||
|
iloc = self.findBookmark(parent)
|
||||||
|
|
||||||
|
dest = Bookmark(TextStringObject(title), NumberObject(pagenum), NameObject('/FitH'), NumberObject(826))
|
||||||
|
|
||||||
|
if parent == None:
|
||||||
|
self.bookmarks.append(dest)
|
||||||
|
else:
|
||||||
|
bmparent = self.bookmarks
|
||||||
|
for i in iloc[:-1]:
|
||||||
|
bmparent = bmparent[i]
|
||||||
|
npos = iloc[-1]+1
|
||||||
|
if npos < len(bmparent) and isinstance(bmparent[npos], list):
|
||||||
|
bmparent[npos].append(dest)
|
||||||
|
else:
|
||||||
|
bmparent.insert(npos, [dest])
|
||||||
|
return dest
|
||||||
|
|
||||||
|
def addNamedDestination(self, title, pagenum):
|
||||||
|
"""
|
||||||
|
Add a destination to the output.
|
||||||
|
|
||||||
|
:param str title: Title to use
|
||||||
|
:param int pagenum: Page number this destination points at.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dest = Destination(TextStringObject(title), NumberObject(pagenum), NameObject('/FitH'), NumberObject(826))
|
||||||
|
self.named_dests.append(dest)
|
||||||
|
|
||||||
|
|
||||||
|
class OutlinesObject(list):
|
||||||
|
def __init__(self, pdf, tree, parent=None):
|
||||||
|
list.__init__(self)
|
||||||
|
self.tree = tree
|
||||||
|
self.pdf = pdf
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def remove(self, index):
|
||||||
|
obj = self[index]
|
||||||
|
del self[index]
|
||||||
|
self.tree.removeChild(obj)
|
||||||
|
|
||||||
|
def add(self, title, pagenum):
|
||||||
|
pageRef = self.pdf.getObject(self.pdf._pages)['/Kids'][pagenum]
|
||||||
|
action = DictionaryObject()
|
||||||
|
action.update({
|
||||||
|
NameObject('/D') : ArrayObject([pageRef, NameObject('/FitH'), NumberObject(826)]),
|
||||||
|
NameObject('/S') : NameObject('/GoTo')
|
||||||
|
})
|
||||||
|
actionRef = self.pdf._addObject(action)
|
||||||
|
bookmark = TreeObject()
|
||||||
|
|
||||||
|
bookmark.update({
|
||||||
|
NameObject('/A'): actionRef,
|
||||||
|
NameObject('/Title'): createStringObject(title),
|
||||||
|
})
|
||||||
|
|
||||||
|
self.pdf._addObject(bookmark)
|
||||||
|
|
||||||
|
self.tree.addChild(bookmark)
|
||||||
|
|
||||||
|
def removeAll(self):
|
||||||
|
for child in [x for x in self.tree.children()]:
|
||||||
|
self.tree.removeChild(child)
|
||||||
|
self.pop()
|
||||||
152
PdfFileTransformer/PyPDF2/pagerange.py
Normal file
152
PdfFileTransformer/PyPDF2/pagerange.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Representation and utils for ranges of PDF file pages.
|
||||||
|
|
||||||
|
Copyright (c) 2014, Steve Witham <switham_github@mac-guyver.com>.
|
||||||
|
All rights reserved. This software is available under a BSD license;
|
||||||
|
see https://github.com/mstamy2/PyPDF2/blob/master/LICENSE
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from .utils import isString
|
||||||
|
|
||||||
|
_INT_RE = r"(0|-?[1-9]\d*)" # A decimal int, don't allow "-0".
|
||||||
|
PAGE_RANGE_RE = "^({int}|({int}?(:{int}?(:{int}?)?)))$".format(int=_INT_RE)
|
||||||
|
# groups: 12 34 5 6 7 8
|
||||||
|
|
||||||
|
|
||||||
|
class ParseError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
PAGE_RANGE_HELP = """Remember, page indices start with zero.
|
||||||
|
Page range expression examples:
|
||||||
|
: all pages. -1 last page.
|
||||||
|
22 just the 23rd page. :-1 all but the last page.
|
||||||
|
0:3 the first three pages. -2 second-to-last page.
|
||||||
|
:3 the first three pages. -2: last two pages.
|
||||||
|
5: from the sixth page onward. -3:-1 third & second to last.
|
||||||
|
The third, "stride" or "step" number is also recognized.
|
||||||
|
::2 0 2 4 ... to the end. 3:0:-1 3 2 1 but not 0.
|
||||||
|
1:10:2 1 3 5 7 9 2::-1 2 1 0.
|
||||||
|
::-1 all pages in reverse order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PageRange(object):
|
||||||
|
"""
|
||||||
|
A slice-like representation of a range of page indices,
|
||||||
|
i.e. page numbers, only starting at zero.
|
||||||
|
The syntax is like what you would put between brackets [ ].
|
||||||
|
The slice is one of the few Python types that can't be subclassed,
|
||||||
|
but this class converts to and from slices, and allows similar use.
|
||||||
|
o PageRange(str) parses a string representing a page range.
|
||||||
|
o PageRange(slice) directly "imports" a slice.
|
||||||
|
o to_slice() gives the equivalent slice.
|
||||||
|
o str() and repr() allow printing.
|
||||||
|
o indices(n) is like slice.indices(n).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, arg):
|
||||||
|
"""
|
||||||
|
Initialize with either a slice -- giving the equivalent page range,
|
||||||
|
or a PageRange object -- making a copy,
|
||||||
|
or a string like
|
||||||
|
"int", "[int]:[int]" or "[int]:[int]:[int]",
|
||||||
|
where the brackets indicate optional ints.
|
||||||
|
{page_range_help}
|
||||||
|
Note the difference between this notation and arguments to slice():
|
||||||
|
slice(3) means the first three pages;
|
||||||
|
PageRange("3") means the range of only the fourth page.
|
||||||
|
However PageRange(slice(3)) means the first three pages.
|
||||||
|
"""
|
||||||
|
if isinstance(arg, slice):
|
||||||
|
self._slice = arg
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(arg, PageRange):
|
||||||
|
self._slice = arg.to_slice()
|
||||||
|
return
|
||||||
|
|
||||||
|
m = isString(arg) and re.match(PAGE_RANGE_RE, arg)
|
||||||
|
if not m:
|
||||||
|
raise ParseError(arg)
|
||||||
|
elif m.group(2):
|
||||||
|
# Special case: just an int means a range of one page.
|
||||||
|
start = int(m.group(2))
|
||||||
|
stop = start + 1 if start != -1 else None
|
||||||
|
self._slice = slice(start, stop)
|
||||||
|
else:
|
||||||
|
self._slice = slice(*[int(g) if g else None
|
||||||
|
for g in m.group(4, 6, 8)])
|
||||||
|
|
||||||
|
# Just formatting this when there is __doc__ for __init__
|
||||||
|
if __init__.__doc__:
|
||||||
|
__init__.__doc__ = __init__.__doc__.format(page_range_help=PAGE_RANGE_HELP)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def valid(input):
|
||||||
|
""" True if input is a valid initializer for a PageRange. """
|
||||||
|
return isinstance(input, slice) or \
|
||||||
|
isinstance(input, PageRange) or \
|
||||||
|
(isString(input)
|
||||||
|
and bool(re.match(PAGE_RANGE_RE, input)))
|
||||||
|
|
||||||
|
def to_slice(self):
|
||||||
|
""" Return the slice equivalent of this page range. """
|
||||||
|
return self._slice
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
""" A string like "1:2:3". """
|
||||||
|
s = self._slice
|
||||||
|
if s.step == None:
|
||||||
|
if s.start != None and s.stop == s.start + 1:
|
||||||
|
return str(s.start)
|
||||||
|
|
||||||
|
indices = s.start, s.stop
|
||||||
|
else:
|
||||||
|
indices = s.start, s.stop, s.step
|
||||||
|
return ':'.join("" if i == None else str(i) for i in indices)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
""" A string like "PageRange('1:2:3')". """
|
||||||
|
return "PageRange(" + repr(str(self)) + ")"
|
||||||
|
|
||||||
|
def indices(self, n):
|
||||||
|
"""
|
||||||
|
n is the length of the list of pages to choose from.
|
||||||
|
Returns arguments for range(). See help(slice.indices).
|
||||||
|
"""
|
||||||
|
return self._slice.indices(n)
|
||||||
|
|
||||||
|
|
||||||
|
PAGE_RANGE_ALL = PageRange(":") # The range of all pages.
|
||||||
|
|
||||||
|
|
||||||
|
def parse_filename_page_ranges(args):
|
||||||
|
"""
|
||||||
|
Given a list of filenames and page ranges, return a list of
|
||||||
|
(filename, page_range) pairs.
|
||||||
|
First arg must be a filename; other ags are filenames, page-range
|
||||||
|
expressions, slice objects, or PageRange objects.
|
||||||
|
A filename not followed by a page range indicates all pages of the file.
|
||||||
|
"""
|
||||||
|
pairs = []
|
||||||
|
pdf_filename = None
|
||||||
|
did_page_range = False
|
||||||
|
for arg in args + [None]:
|
||||||
|
if PageRange.valid(arg):
|
||||||
|
if not pdf_filename:
|
||||||
|
raise ValueError("The first argument must be a filename, " \
|
||||||
|
"not a page range.")
|
||||||
|
|
||||||
|
pairs.append( (pdf_filename, PageRange(arg)) )
|
||||||
|
did_page_range = True
|
||||||
|
else:
|
||||||
|
# New filename or end of list--do all of the previous file?
|
||||||
|
if pdf_filename and not did_page_range:
|
||||||
|
pairs.append( (pdf_filename, PAGE_RANGE_ALL) )
|
||||||
|
|
||||||
|
pdf_filename = arg
|
||||||
|
did_page_range = False
|
||||||
|
return pairs
|
||||||
3074
PdfFileTransformer/PyPDF2/pdf.py
Normal file
3074
PdfFileTransformer/PyPDF2/pdf.py
Normal file
File diff suppressed because it is too large
Load Diff
309
PdfFileTransformer/PyPDF2/utils.py
Normal file
309
PdfFileTransformer/PyPDF2/utils.py
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# Copyright (c) 2006, Mathieu Fenniak
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
# * The name of the author may not be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Utility functions for PDF library.
|
||||||
|
"""
|
||||||
|
__author__ = "Mathieu Fenniak"
|
||||||
|
__author_email__ = "biziqe@mathieu.fenniak.net"
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import __builtin__ as builtins
|
||||||
|
except ImportError: # Py3
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
|
||||||
|
xrange_fn = getattr(builtins, "xrange", range)
|
||||||
|
_basestring = getattr(builtins, "basestring", str)
|
||||||
|
|
||||||
|
bytes_type = type(bytes()) # Works the same in Python 2.X and 3.X
|
||||||
|
string_type = getattr(builtins, "unicode", str)
|
||||||
|
int_types = (int, long) if sys.version_info[0] < 3 else (int,)
|
||||||
|
|
||||||
|
|
||||||
|
# Make basic type tests more consistent
|
||||||
|
def isString(s):
|
||||||
|
"""Test if arg is a string. Compatible with Python 2 and 3."""
|
||||||
|
return isinstance(s, _basestring)
|
||||||
|
|
||||||
|
|
||||||
|
def isInt(n):
|
||||||
|
"""Test if arg is an int. Compatible with Python 2 and 3."""
|
||||||
|
return isinstance(n, int_types)
|
||||||
|
|
||||||
|
|
||||||
|
def isBytes(b):
|
||||||
|
"""Test if arg is a bytes instance. Compatible with Python 2 and 3."""
|
||||||
|
return isinstance(b, bytes_type)
|
||||||
|
|
||||||
|
|
||||||
|
#custom implementation of warnings.formatwarning
|
||||||
|
def formatWarning(message, category, filename, lineno, line=None):
|
||||||
|
file = filename.replace("/", "\\").rsplit("\\", 1)[1] # find the file name
|
||||||
|
return "%s: %s [%s:%s]\n" % (category.__name__, message, file, lineno)
|
||||||
|
|
||||||
|
|
||||||
|
def readUntilWhitespace(stream, maxchars=None):
|
||||||
|
"""
|
||||||
|
Reads non-whitespace characters and returns them.
|
||||||
|
Stops upon encountering whitespace or when maxchars is reached.
|
||||||
|
"""
|
||||||
|
txt = b_("")
|
||||||
|
while True:
|
||||||
|
tok = stream.read(1)
|
||||||
|
if tok.isspace() or not tok:
|
||||||
|
break
|
||||||
|
txt += tok
|
||||||
|
if len(txt) == maxchars:
|
||||||
|
break
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
|
def readNonWhitespace(stream):
|
||||||
|
"""
|
||||||
|
Finds and reads the next non-whitespace character (ignores whitespace).
|
||||||
|
"""
|
||||||
|
tok = WHITESPACES[0]
|
||||||
|
while tok in WHITESPACES:
|
||||||
|
tok = stream.read(1)
|
||||||
|
return tok
|
||||||
|
|
||||||
|
|
||||||
|
def skipOverWhitespace(stream):
|
||||||
|
"""
|
||||||
|
Similar to readNonWhitespace, but returns a Boolean if more than
|
||||||
|
one whitespace character was read.
|
||||||
|
"""
|
||||||
|
tok = WHITESPACES[0]
|
||||||
|
cnt = 0;
|
||||||
|
while tok in WHITESPACES:
|
||||||
|
tok = stream.read(1)
|
||||||
|
cnt+=1
|
||||||
|
return (cnt > 1)
|
||||||
|
|
||||||
|
|
||||||
|
def skipOverComment(stream):
|
||||||
|
tok = stream.read(1)
|
||||||
|
stream.seek(-1, 1)
|
||||||
|
if tok == b_('%'):
|
||||||
|
while tok not in (b_('\n'), b_('\r')):
|
||||||
|
tok = stream.read(1)
|
||||||
|
|
||||||
|
|
||||||
|
def readUntilRegex(stream, regex, ignore_eof=False):
|
||||||
|
"""
|
||||||
|
Reads until the regular expression pattern matched (ignore the match)
|
||||||
|
Raise PdfStreamError on premature end-of-file.
|
||||||
|
:param bool ignore_eof: If true, ignore end-of-line and return immediately
|
||||||
|
"""
|
||||||
|
name = b_('')
|
||||||
|
while True:
|
||||||
|
tok = stream.read(16)
|
||||||
|
if not tok:
|
||||||
|
# stream has truncated prematurely
|
||||||
|
if ignore_eof == True:
|
||||||
|
return name
|
||||||
|
else:
|
||||||
|
raise PdfStreamError("Stream has ended unexpectedly")
|
||||||
|
m = regex.search(tok)
|
||||||
|
if m is not None:
|
||||||
|
name += tok[:m.start()]
|
||||||
|
stream.seek(m.start()-len(tok), 1)
|
||||||
|
break
|
||||||
|
name += tok
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertFunctionsToVirtualList(object):
|
||||||
|
def __init__(self, lengthFunction, getFunction):
|
||||||
|
self.lengthFunction = lengthFunction
|
||||||
|
self.getFunction = getFunction
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.lengthFunction()
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
if isinstance(index, slice):
|
||||||
|
indices = xrange_fn(*index.indices(len(self)))
|
||||||
|
cls = type(self)
|
||||||
|
return cls(indices.__len__, lambda idx: self[indices[idx]])
|
||||||
|
if not isInt(index):
|
||||||
|
raise TypeError("sequence indices must be integers")
|
||||||
|
len_self = len(self)
|
||||||
|
if index < 0:
|
||||||
|
# support negative indexes
|
||||||
|
index = len_self + index
|
||||||
|
if index < 0 or index >= len_self:
|
||||||
|
raise IndexError("sequence index out of range")
|
||||||
|
return self.getFunction(index)
|
||||||
|
|
||||||
|
|
||||||
|
def RC4_encrypt(key, plaintext):
|
||||||
|
S = [i for i in range(256)]
|
||||||
|
j = 0
|
||||||
|
for i in range(256):
|
||||||
|
j = (j + S[i] + ord_(key[i % len(key)])) % 256
|
||||||
|
S[i], S[j] = S[j], S[i]
|
||||||
|
i, j = 0, 0
|
||||||
|
retval = []
|
||||||
|
for x in range(len(plaintext)):
|
||||||
|
i = (i + 1) % 256
|
||||||
|
j = (j + S[i]) % 256
|
||||||
|
S[i], S[j] = S[j], S[i]
|
||||||
|
t = S[(S[i] + S[j]) % 256]
|
||||||
|
retval.append(b_(chr(ord_(plaintext[x]) ^ t)))
|
||||||
|
return b_("").join(retval)
|
||||||
|
|
||||||
|
|
||||||
|
def matrixMultiply(a, b):
|
||||||
|
return [[sum([float(i)*float(j)
|
||||||
|
for i, j in zip(row, col)]
|
||||||
|
) for col in zip(*b)]
|
||||||
|
for row in a]
|
||||||
|
|
||||||
|
|
||||||
|
def markLocation(stream):
|
||||||
|
"""Creates text file showing current location in context."""
|
||||||
|
# Mainly for debugging
|
||||||
|
RADIUS = 5000
|
||||||
|
stream.seek(-RADIUS, 1)
|
||||||
|
outputDoc = open('PyPDF2_pdfLocation.txt', 'w')
|
||||||
|
outputDoc.write(stream.read(RADIUS))
|
||||||
|
outputDoc.write('HERE')
|
||||||
|
outputDoc.write(stream.read(RADIUS))
|
||||||
|
outputDoc.close()
|
||||||
|
stream.seek(-RADIUS, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class PyPdfError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PdfReadError(PyPdfError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PageSizeNotDefinedError(PyPdfError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PdfReadWarning(UserWarning):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PdfStreamError(PdfReadError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
def b_(s):
|
||||||
|
return s
|
||||||
|
else:
|
||||||
|
B_CACHE = {}
|
||||||
|
|
||||||
|
def b_(s):
|
||||||
|
bc = B_CACHE
|
||||||
|
if s in bc:
|
||||||
|
return bc[s]
|
||||||
|
if type(s) == bytes:
|
||||||
|
return s
|
||||||
|
else:
|
||||||
|
r = s.encode('latin-1')
|
||||||
|
if len(s) < 2:
|
||||||
|
bc[s] = r
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def u_(s):
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
return unicode(s, 'unicode_escape')
|
||||||
|
else:
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def str_(b):
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
return b
|
||||||
|
else:
|
||||||
|
if type(b) == bytes:
|
||||||
|
return b.decode('latin-1')
|
||||||
|
else:
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def ord_(b):
|
||||||
|
if sys.version_info[0] < 3 or type(b) == str:
|
||||||
|
return ord(b)
|
||||||
|
else:
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def chr_(c):
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
return c
|
||||||
|
else:
|
||||||
|
return chr(c)
|
||||||
|
|
||||||
|
|
||||||
|
def barray(b):
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
return b
|
||||||
|
else:
|
||||||
|
return bytearray(b)
|
||||||
|
|
||||||
|
|
||||||
|
def hexencode(b):
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
return b.encode('hex')
|
||||||
|
else:
|
||||||
|
import codecs
|
||||||
|
coder = codecs.getencoder('hex_codec')
|
||||||
|
return coder(b)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def hexStr(num):
|
||||||
|
return hex(num).replace('L', '')
|
||||||
|
|
||||||
|
|
||||||
|
WHITESPACES = [b_(x) for x in [' ', '\n', '\r', '\t', '\x00']]
|
||||||
|
|
||||||
|
|
||||||
|
def paethPredictor(left, up, up_left):
|
||||||
|
p = left + up - up_left
|
||||||
|
dist_left = abs(p - left)
|
||||||
|
dist_up = abs(p - up)
|
||||||
|
dist_up_left = abs(p - up_left)
|
||||||
|
|
||||||
|
if dist_left <= dist_up and dist_left <= dist_up_left:
|
||||||
|
return left
|
||||||
|
elif dist_up <= dist_up_left:
|
||||||
|
return up
|
||||||
|
else:
|
||||||
|
return up_left
|
||||||
358
PdfFileTransformer/PyPDF2/xmp.py
Normal file
358
PdfFileTransformer/PyPDF2/xmp.py
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
import decimal
|
||||||
|
from .generic import PdfObject
|
||||||
|
from xml.dom import getDOMImplementation
|
||||||
|
from xml.dom.minidom import parseString
|
||||||
|
from .utils import u_
|
||||||
|
|
||||||
|
RDF_NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
DC_NAMESPACE = "http://purl.org/dc/elements/1.1/"
|
||||||
|
XMP_NAMESPACE = "http://ns.adobe.com/xap/1.0/"
|
||||||
|
PDF_NAMESPACE = "http://ns.adobe.com/pdf/1.3/"
|
||||||
|
XMPMM_NAMESPACE = "http://ns.adobe.com/xap/1.0/mm/"
|
||||||
|
|
||||||
|
# What is the PDFX namespace, you might ask? I might ask that too. It's
|
||||||
|
# a completely undocumented namespace used to place "custom metadata"
|
||||||
|
# properties, which are arbitrary metadata properties with no semantic or
|
||||||
|
# documented meaning. Elements in the namespace are key/value-style storage,
|
||||||
|
# where the element name is the key and the content is the value. The keys
|
||||||
|
# are transformed into valid XML identifiers by substituting an invalid
|
||||||
|
# identifier character with \u2182 followed by the unicode hex ID of the
|
||||||
|
# original character. A key like "my car" is therefore "my\u21820020car".
|
||||||
|
#
|
||||||
|
# \u2182, in case you're wondering, is the unicode character
|
||||||
|
# \u{ROMAN NUMERAL TEN THOUSAND}, a straightforward and obvious choice for
|
||||||
|
# escaping characters.
|
||||||
|
#
|
||||||
|
# Intentional users of the pdfx namespace should be shot on sight. A
|
||||||
|
# custom data schema and sensical XML elements could be used instead, as is
|
||||||
|
# suggested by Adobe's own documentation on XMP (under "Extensibility of
|
||||||
|
# Schemas").
|
||||||
|
#
|
||||||
|
# Information presented here on the /pdfx/ schema is a result of limited
|
||||||
|
# reverse engineering, and does not constitute a full specification.
|
||||||
|
PDFX_NAMESPACE = "http://ns.adobe.com/pdfx/1.3/"
|
||||||
|
|
||||||
|
iso8601 = re.compile("""
|
||||||
|
(?P<year>[0-9]{4})
|
||||||
|
(-
|
||||||
|
(?P<month>[0-9]{2})
|
||||||
|
(-
|
||||||
|
(?P<day>[0-9]+)
|
||||||
|
(T
|
||||||
|
(?P<hour>[0-9]{2}):
|
||||||
|
(?P<minute>[0-9]{2})
|
||||||
|
(:(?P<second>[0-9]{2}(.[0-9]+)?))?
|
||||||
|
(?P<tzd>Z|[-+][0-9]{2}:[0-9]{2})
|
||||||
|
)?
|
||||||
|
)?
|
||||||
|
)?
|
||||||
|
""", re.VERBOSE)
|
||||||
|
|
||||||
|
|
||||||
|
class XmpInformation(PdfObject):
|
||||||
|
"""
|
||||||
|
An object that represents Adobe XMP metadata.
|
||||||
|
Usually accessed by :meth:`getXmpMetadata()<PyPDF2.PdfFileReader.getXmpMetadata>`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
docRoot = parseString(self.stream.getData())
|
||||||
|
self.rdfRoot = docRoot.getElementsByTagNameNS(RDF_NAMESPACE, "RDF")[0]
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
def writeToStream(self, stream, encryption_key):
|
||||||
|
self.stream.writeToStream(stream, encryption_key)
|
||||||
|
|
||||||
|
def getElement(self, aboutUri, namespace, name):
|
||||||
|
for desc in self.rdfRoot.getElementsByTagNameNS(RDF_NAMESPACE, "Description"):
|
||||||
|
if desc.getAttributeNS(RDF_NAMESPACE, "about") == aboutUri:
|
||||||
|
attr = desc.getAttributeNodeNS(namespace, name)
|
||||||
|
if attr != None:
|
||||||
|
yield attr
|
||||||
|
for element in desc.getElementsByTagNameNS(namespace, name):
|
||||||
|
yield element
|
||||||
|
|
||||||
|
def getNodesInNamespace(self, aboutUri, namespace):
|
||||||
|
for desc in self.rdfRoot.getElementsByTagNameNS(RDF_NAMESPACE, "Description"):
|
||||||
|
if desc.getAttributeNS(RDF_NAMESPACE, "about") == aboutUri:
|
||||||
|
for i in range(desc.attributes.length):
|
||||||
|
attr = desc.attributes.item(i)
|
||||||
|
if attr.namespaceURI == namespace:
|
||||||
|
yield attr
|
||||||
|
for child in desc.childNodes:
|
||||||
|
if child.namespaceURI == namespace:
|
||||||
|
yield child
|
||||||
|
|
||||||
|
def _getText(self, element):
|
||||||
|
text = ""
|
||||||
|
for child in element.childNodes:
|
||||||
|
if child.nodeType == child.TEXT_NODE:
|
||||||
|
text += child.data
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _converter_string(value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _converter_date(value):
|
||||||
|
m = iso8601.match(value)
|
||||||
|
year = int(m.group("year"))
|
||||||
|
month = int(m.group("month") or "1")
|
||||||
|
day = int(m.group("day") or "1")
|
||||||
|
hour = int(m.group("hour") or "0")
|
||||||
|
minute = int(m.group("minute") or "0")
|
||||||
|
second = decimal.Decimal(m.group("second") or "0")
|
||||||
|
seconds = second.to_integral(decimal.ROUND_FLOOR)
|
||||||
|
milliseconds = (second - seconds) * 1000000
|
||||||
|
tzd = m.group("tzd") or "Z"
|
||||||
|
dt = datetime.datetime(year, month, day, hour, minute, seconds, milliseconds)
|
||||||
|
if tzd != "Z":
|
||||||
|
tzd_hours, tzd_minutes = [int(x) for x in tzd.split(":")]
|
||||||
|
tzd_hours *= -1
|
||||||
|
if tzd_hours < 0:
|
||||||
|
tzd_minutes *= -1
|
||||||
|
dt = dt + datetime.timedelta(hours=tzd_hours, minutes=tzd_minutes)
|
||||||
|
return dt
|
||||||
|
_test_converter_date = staticmethod(_converter_date)
|
||||||
|
|
||||||
|
def _getter_bag(namespace, name, converter):
|
||||||
|
def get(self):
|
||||||
|
cached = self.cache.get(namespace, {}).get(name)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
retval = []
|
||||||
|
for element in self.getElement("", namespace, name):
|
||||||
|
bags = element.getElementsByTagNameNS(RDF_NAMESPACE, "Bag")
|
||||||
|
if len(bags):
|
||||||
|
for bag in bags:
|
||||||
|
for item in bag.getElementsByTagNameNS(RDF_NAMESPACE, "li"):
|
||||||
|
value = self._getText(item)
|
||||||
|
value = converter(value)
|
||||||
|
retval.append(value)
|
||||||
|
ns_cache = self.cache.setdefault(namespace, {})
|
||||||
|
ns_cache[name] = retval
|
||||||
|
return retval
|
||||||
|
return get
|
||||||
|
|
||||||
|
def _getter_seq(namespace, name, converter):
|
||||||
|
def get(self):
|
||||||
|
cached = self.cache.get(namespace, {}).get(name)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
retval = []
|
||||||
|
for element in self.getElement("", namespace, name):
|
||||||
|
seqs = element.getElementsByTagNameNS(RDF_NAMESPACE, "Seq")
|
||||||
|
if len(seqs):
|
||||||
|
for seq in seqs:
|
||||||
|
for item in seq.getElementsByTagNameNS(RDF_NAMESPACE, "li"):
|
||||||
|
value = self._getText(item)
|
||||||
|
value = converter(value)
|
||||||
|
retval.append(value)
|
||||||
|
else:
|
||||||
|
value = converter(self._getText(element))
|
||||||
|
retval.append(value)
|
||||||
|
ns_cache = self.cache.setdefault(namespace, {})
|
||||||
|
ns_cache[name] = retval
|
||||||
|
return retval
|
||||||
|
return get
|
||||||
|
|
||||||
|
def _getter_langalt(namespace, name, converter):
|
||||||
|
def get(self):
|
||||||
|
cached = self.cache.get(namespace, {}).get(name)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
retval = {}
|
||||||
|
for element in self.getElement("", namespace, name):
|
||||||
|
alts = element.getElementsByTagNameNS(RDF_NAMESPACE, "Alt")
|
||||||
|
if len(alts):
|
||||||
|
for alt in alts:
|
||||||
|
for item in alt.getElementsByTagNameNS(RDF_NAMESPACE, "li"):
|
||||||
|
value = self._getText(item)
|
||||||
|
value = converter(value)
|
||||||
|
retval[item.getAttribute("xml:lang")] = value
|
||||||
|
else:
|
||||||
|
retval["x-default"] = converter(self._getText(element))
|
||||||
|
ns_cache = self.cache.setdefault(namespace, {})
|
||||||
|
ns_cache[name] = retval
|
||||||
|
return retval
|
||||||
|
return get
|
||||||
|
|
||||||
|
def _getter_single(namespace, name, converter):
|
||||||
|
def get(self):
|
||||||
|
cached = self.cache.get(namespace, {}).get(name)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
value = None
|
||||||
|
for element in self.getElement("", namespace, name):
|
||||||
|
if element.nodeType == element.ATTRIBUTE_NODE:
|
||||||
|
value = element.nodeValue
|
||||||
|
else:
|
||||||
|
value = self._getText(element)
|
||||||
|
break
|
||||||
|
if value != None:
|
||||||
|
value = converter(value)
|
||||||
|
ns_cache = self.cache.setdefault(namespace, {})
|
||||||
|
ns_cache[name] = value
|
||||||
|
return value
|
||||||
|
return get
|
||||||
|
|
||||||
|
dc_contributor = property(_getter_bag(DC_NAMESPACE, "contributor", _converter_string))
|
||||||
|
"""
|
||||||
|
Contributors to the resource (other than the authors). An unsorted
|
||||||
|
array of names.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_coverage = property(_getter_single(DC_NAMESPACE, "coverage", _converter_string))
|
||||||
|
"""
|
||||||
|
Text describing the extent or scope of the resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_creator = property(_getter_seq(DC_NAMESPACE, "creator", _converter_string))
|
||||||
|
"""
|
||||||
|
A sorted array of names of the authors of the resource, listed in order
|
||||||
|
of precedence.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_date = property(_getter_seq(DC_NAMESPACE, "date", _converter_date))
|
||||||
|
"""
|
||||||
|
A sorted array of dates (datetime.datetime instances) of signifigance to
|
||||||
|
the resource. The dates and times are in UTC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_description = property(_getter_langalt(DC_NAMESPACE, "description", _converter_string))
|
||||||
|
"""
|
||||||
|
A language-keyed dictionary of textual descriptions of the content of the
|
||||||
|
resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_format = property(_getter_single(DC_NAMESPACE, "format", _converter_string))
|
||||||
|
"""
|
||||||
|
The mime-type of the resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_identifier = property(_getter_single(DC_NAMESPACE, "identifier", _converter_string))
|
||||||
|
"""
|
||||||
|
Unique identifier of the resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_language = property(_getter_bag(DC_NAMESPACE, "language", _converter_string))
|
||||||
|
"""
|
||||||
|
An unordered array specifying the languages used in the resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_publisher = property(_getter_bag(DC_NAMESPACE, "publisher", _converter_string))
|
||||||
|
"""
|
||||||
|
An unordered array of publisher names.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_relation = property(_getter_bag(DC_NAMESPACE, "relation", _converter_string))
|
||||||
|
"""
|
||||||
|
An unordered array of text descriptions of relationships to other
|
||||||
|
documents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_rights = property(_getter_langalt(DC_NAMESPACE, "rights", _converter_string))
|
||||||
|
"""
|
||||||
|
A language-keyed dictionary of textual descriptions of the rights the
|
||||||
|
user has to this resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_source = property(_getter_single(DC_NAMESPACE, "source", _converter_string))
|
||||||
|
"""
|
||||||
|
Unique identifier of the work from which this resource was derived.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_subject = property(_getter_bag(DC_NAMESPACE, "subject", _converter_string))
|
||||||
|
"""
|
||||||
|
An unordered array of descriptive phrases or keywrods that specify the
|
||||||
|
topic of the content of the resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_title = property(_getter_langalt(DC_NAMESPACE, "title", _converter_string))
|
||||||
|
"""
|
||||||
|
A language-keyed dictionary of the title of the resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dc_type = property(_getter_bag(DC_NAMESPACE, "type", _converter_string))
|
||||||
|
"""
|
||||||
|
An unordered array of textual descriptions of the document type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pdf_keywords = property(_getter_single(PDF_NAMESPACE, "Keywords", _converter_string))
|
||||||
|
"""
|
||||||
|
An unformatted text string representing document keywords.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pdf_pdfversion = property(_getter_single(PDF_NAMESPACE, "PDFVersion", _converter_string))
|
||||||
|
"""
|
||||||
|
The PDF file version, for example 1.0, 1.3.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pdf_producer = property(_getter_single(PDF_NAMESPACE, "Producer", _converter_string))
|
||||||
|
"""
|
||||||
|
The name of the tool that created the PDF document.
|
||||||
|
"""
|
||||||
|
|
||||||
|
xmp_createDate = property(_getter_single(XMP_NAMESPACE, "CreateDate", _converter_date))
|
||||||
|
"""
|
||||||
|
The date and time the resource was originally created. The date and
|
||||||
|
time are returned as a UTC datetime.datetime object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
xmp_modifyDate = property(_getter_single(XMP_NAMESPACE, "ModifyDate", _converter_date))
|
||||||
|
"""
|
||||||
|
The date and time the resource was last modified. The date and time
|
||||||
|
are returned as a UTC datetime.datetime object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
xmp_metadataDate = property(_getter_single(XMP_NAMESPACE, "MetadataDate", _converter_date))
|
||||||
|
"""
|
||||||
|
The date and time that any metadata for this resource was last
|
||||||
|
changed. The date and time are returned as a UTC datetime.datetime
|
||||||
|
object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
xmp_creatorTool = property(_getter_single(XMP_NAMESPACE, "CreatorTool", _converter_string))
|
||||||
|
"""
|
||||||
|
The name of the first known tool used to create the resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
xmpmm_documentId = property(_getter_single(XMPMM_NAMESPACE, "DocumentID", _converter_string))
|
||||||
|
"""
|
||||||
|
The common identifier for all versions and renditions of this resource.
|
||||||
|
"""
|
||||||
|
|
||||||
|
xmpmm_instanceId = property(_getter_single(XMPMM_NAMESPACE, "InstanceID", _converter_string))
|
||||||
|
"""
|
||||||
|
An identifier for a specific incarnation of a document, updated each
|
||||||
|
time a file is saved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def custom_properties(self):
|
||||||
|
if not hasattr(self, "_custom_properties"):
|
||||||
|
self._custom_properties = {}
|
||||||
|
for node in self.getNodesInNamespace("", PDFX_NAMESPACE):
|
||||||
|
key = node.localName
|
||||||
|
while True:
|
||||||
|
# see documentation about PDFX_NAMESPACE earlier in file
|
||||||
|
idx = key.find(u_("\u2182"))
|
||||||
|
if idx == -1:
|
||||||
|
break
|
||||||
|
key = key[:idx] + chr(int(key[idx+1:idx+5], base=16)) + key[idx+5:]
|
||||||
|
if node.nodeType == node.ATTRIBUTE_NODE:
|
||||||
|
value = node.nodeValue
|
||||||
|
else:
|
||||||
|
value = self._getText(node)
|
||||||
|
self._custom_properties[key] = value
|
||||||
|
return self._custom_properties
|
||||||
|
|
||||||
|
custom_properties = property(custom_properties)
|
||||||
|
"""
|
||||||
|
Retrieves custom metadata properties defined in the undocumented pdfx
|
||||||
|
metadata schema.
|
||||||
|
|
||||||
|
:return: a dictionary of key/value items for custom metadata properties.
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
4
PdfFileTransformer/__init__.py
Normal file
4
PdfFileTransformer/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .PyPDF2 import PdfFileReader, PdfFileWriter
|
||||||
|
from .pdf import Pdf
|
||||||
352
PdfFileTransformer/pdf.py
Normal file
352
PdfFileTransformer/pdf.py
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
from .PyPDF2 import PdfFileWriter, PdfFileReader
|
||||||
|
|
||||||
|
|
||||||
|
class Pdf:
|
||||||
|
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.filename = filename
|
||||||
|
self.buffer = bytearray()
|
||||||
|
self.objects = [] # [(7,0,b"data"), (8,0,b"data2"), ..]
|
||||||
|
self.trailer = {} # {Root: (7, 0), Info: (5, 0)}
|
||||||
|
self.translation_table = {} # {(6,0):7, (5,0): 8}, ..]
|
||||||
|
self.original_xref_offset = 0
|
||||||
|
self.original_first_obj_offset = 0
|
||||||
|
self.file_offset = 0
|
||||||
|
|
||||||
|
self.clean_and_read_pdf()
|
||||||
|
self.check_pdf_header()
|
||||||
|
self.parse_xref_offset()
|
||||||
|
self.parse_xref_table()
|
||||||
|
self.parse_objects()
|
||||||
|
self.parse_trailer()
|
||||||
|
|
||||||
|
def clean_and_read_pdf(self):
|
||||||
|
f_input = open(self.filename, "rb")
|
||||||
|
pdf_header = f_input.read(8)
|
||||||
|
f_input.seek(0)
|
||||||
|
filename_output = tempfile.mktemp()
|
||||||
|
logging.info("Use " + filename_output + " for normalisation output")
|
||||||
|
f_ouput = open(filename_output, "wb")
|
||||||
|
writer = PdfFileWriter()
|
||||||
|
reader = PdfFileReader(f_input)
|
||||||
|
info = reader.getDocumentInfo()
|
||||||
|
if info.producer is not None:
|
||||||
|
writer.addMetadata({u'/Producer': info.producer})
|
||||||
|
else:
|
||||||
|
writer.addMetadata({u'/Producer': u'TruePolyglot'})
|
||||||
|
if info.creator is not None:
|
||||||
|
writer.addMetadata({u'/Creator': info.creator})
|
||||||
|
else:
|
||||||
|
writer.addMetadata({u'/Creator': u'TruePolyglot'})
|
||||||
|
writer.appendPagesFromReader(reader)
|
||||||
|
writer.setHeader(pdf_header)
|
||||||
|
writer.write(f_ouput)
|
||||||
|
f_input.close()
|
||||||
|
f_ouput.close()
|
||||||
|
f_norm = open(filename_output, "rb")
|
||||||
|
self.buffer = bytearray(f_norm.read())
|
||||||
|
self.size = len(self.buffer)
|
||||||
|
f_norm.close()
|
||||||
|
|
||||||
|
def check_pdf_header(self):
|
||||||
|
if self.buffer[0:5] == b"%PDF-":
|
||||||
|
pdf_version = self.buffer[5:8].decode("utf-8")
|
||||||
|
logging.info("PDF Header found: " + pdf_version)
|
||||||
|
else:
|
||||||
|
raise Exception("PDF Header not found")
|
||||||
|
|
||||||
|
def parse_xref_offset(self):
|
||||||
|
r = re.compile(b'startxref\n([0-9]+)')
|
||||||
|
m = r.search(self.buffer)
|
||||||
|
if m is None:
|
||||||
|
raise Exception('Unable to find xref offset')
|
||||||
|
self.original_xref_offset = int(m.group(1))
|
||||||
|
logging.info("Xref offset found at: " + hex(self.original_xref_offset))
|
||||||
|
|
||||||
|
def parse_xref_table(self):
|
||||||
|
xref_table = []
|
||||||
|
r = re.compile(b'xref\n([0-9]+) ([0-9]+)')
|
||||||
|
offset = self.original_xref_offset
|
||||||
|
s = r.search(self.buffer[offset:offset + 32])
|
||||||
|
nb_xtable_object = int(s.group(2))
|
||||||
|
logging.info("Nb objects in Xref table: " + str(nb_xtable_object))
|
||||||
|
xref_header_size = s.end()
|
||||||
|
r = re.compile(b'([0-9]+) ([0-9]+) ([f|n])')
|
||||||
|
x = 0
|
||||||
|
for i in range(nb_xtable_object):
|
||||||
|
s = r.search(
|
||||||
|
self.buffer[self.original_xref_offset + xref_header_size + x:])
|
||||||
|
if s is not None:
|
||||||
|
x = x + s.end()
|
||||||
|
xref_table.append((int(s.group(1)),
|
||||||
|
int(s.group(2)),
|
||||||
|
s.group(3)))
|
||||||
|
logging.debug("Xref table:")
|
||||||
|
for i in xref_table:
|
||||||
|
logging.debug(str(i[0]) + " " +
|
||||||
|
str(i[1]) + " " +
|
||||||
|
i[2].decode("utf-8"))
|
||||||
|
|
||||||
|
def parse_objects(self):
|
||||||
|
r_begin = re.compile(b'([0-9]+) ([0-9]+) obj\n')
|
||||||
|
r_end = re.compile(b'\nendobj\n')
|
||||||
|
|
||||||
|
offset_buffer = 0
|
||||||
|
obj = ()
|
||||||
|
while offset_buffer < self.size:
|
||||||
|
m_begin = r_begin.match(
|
||||||
|
self.buffer[offset_buffer:offset_buffer + 32])
|
||||||
|
obj_nb_index = 0
|
||||||
|
obj_nb_offset = 0
|
||||||
|
obj_offset_start = 0
|
||||||
|
obj_offset_end = 0
|
||||||
|
if m_begin is not None:
|
||||||
|
if self.original_first_obj_offset == 0:
|
||||||
|
self.original_first_obj_offset = (offset_buffer +
|
||||||
|
m_begin.start())
|
||||||
|
obj_nb_index = int(m_begin.group(1))
|
||||||
|
obj_nb_offset = int(m_begin.group(2))
|
||||||
|
obj_data_start = m_begin.end()
|
||||||
|
obj_offset_start = offset_buffer + m_begin.start()
|
||||||
|
while offset_buffer < self.size:
|
||||||
|
m_end = r_end.match(
|
||||||
|
self.buffer[offset_buffer:offset_buffer + 8])
|
||||||
|
if m_end is not None:
|
||||||
|
obj_offset_end = offset_buffer + m_end.end() - 2
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
offset_buffer = offset_buffer + 1
|
||||||
|
else:
|
||||||
|
offset_buffer = offset_buffer + 1
|
||||||
|
|
||||||
|
if (obj_offset_start != 0 and
|
||||||
|
obj_offset_end != 0):
|
||||||
|
a = obj_offset_start + obj_data_start
|
||||||
|
b = obj_offset_end - 6
|
||||||
|
obj = (obj_nb_index, obj_nb_offset,
|
||||||
|
self.buffer[a:b])
|
||||||
|
logging.debug("Objects: (" + str(obj_nb_index) +
|
||||||
|
", " + str(obj_nb_offset) +
|
||||||
|
", " + hex(obj_offset_start) +
|
||||||
|
", " + hex(obj_offset_end))
|
||||||
|
self.objects.append(obj)
|
||||||
|
|
||||||
|
def parse_trailer(self):
|
||||||
|
r_begin = re.compile(b'trailer\n')
|
||||||
|
s_begin = r_begin.search(self.buffer[self.original_xref_offset:])
|
||||||
|
start = self.original_xref_offset + s_begin.start()
|
||||||
|
logging.info("Trailer found at:" + hex(start))
|
||||||
|
|
||||||
|
r_root = re.compile(b'/Root ([0-9]+) ([0-9]+) R')
|
||||||
|
s_root = r_root.search(self.buffer[self.original_xref_offset:])
|
||||||
|
if s_root is None:
|
||||||
|
raise Exception('Root not found')
|
||||||
|
else:
|
||||||
|
self.trailer["Root"] = (int(s_root.group(1)), int(s_root.group(2)))
|
||||||
|
|
||||||
|
r_info = re.compile(b'/Info ([0-9]+) ([0-9]+) R')
|
||||||
|
s_info = r_info.search(self.buffer[self.original_xref_offset:])
|
||||||
|
if s_info is not None:
|
||||||
|
self.trailer["Info"] = (int(s_info.group(1)), int(s_info.group(2)))
|
||||||
|
|
||||||
|
def get_file_header(self):
|
||||||
|
return self.buffer[:self.original_first_obj_offset]
|
||||||
|
|
||||||
|
def get_xref_table(self):
|
||||||
|
offset_xref = 0
|
||||||
|
buf = (b'xref\n' +
|
||||||
|
str(offset_xref).encode('utf-8') + b' ' +
|
||||||
|
str(len(self.objects) + 1).encode('utf-8') + b'\n' +
|
||||||
|
str(0).zfill(10).encode('utf-8') + b' ' +
|
||||||
|
str(65535).zfill(5).encode('utf-8') + b' f \n')
|
||||||
|
|
||||||
|
for i in range(len(self.objects)):
|
||||||
|
obj_start = self.get_object_offset(i)
|
||||||
|
logging.info("Obj %d at %d" % (self.objects[i][0], obj_start))
|
||||||
|
buf = (buf +
|
||||||
|
(str(obj_start).zfill(10)).encode('utf-8') + b' ' +
|
||||||
|
str(0).zfill(5).encode('utf-8') + b' ' +
|
||||||
|
b'n' + b' \n')
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def get_trailer(self):
|
||||||
|
trailer_data = (b"trailer\n<<\n/Size " +
|
||||||
|
str(len(self.objects) + 1).encode("utf-8") +
|
||||||
|
b"\n/Root " +
|
||||||
|
str(self.trailer["Root"][0]).encode("utf-8") +
|
||||||
|
b" " +
|
||||||
|
str(self.trailer["Root"][1]).encode("utf-8") +
|
||||||
|
b" R\n")
|
||||||
|
if "Info" in self.trailer:
|
||||||
|
trailer_data = (trailer_data +
|
||||||
|
b"/Info " +
|
||||||
|
str(self.trailer["Info"][0]).encode("utf-8") +
|
||||||
|
b" " +
|
||||||
|
str(self.trailer["Info"][1]).encode("utf-8") +
|
||||||
|
b" R\n")
|
||||||
|
trailer_data = trailer_data + b">>"
|
||||||
|
return trailer_data
|
||||||
|
|
||||||
|
def get_xref_offset(self):
|
||||||
|
return self.get_end_of_last_object() + 1
|
||||||
|
|
||||||
|
def get_eof(self):
|
||||||
|
s = (b'startxref\n' +
|
||||||
|
str(self.get_xref_offset()).encode("utf-8") +
|
||||||
|
b'\n%%EOF\n')
|
||||||
|
return s
|
||||||
|
|
||||||
|
def build_object(self, obj):
|
||||||
|
buf = (str(obj[0]).encode("utf-8") +
|
||||||
|
b' ' +
|
||||||
|
str(obj[1]).encode("utf-8") +
|
||||||
|
b' obj\n' +
|
||||||
|
obj[2] +
|
||||||
|
b'\nendobj')
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def get_build_buffer(self):
|
||||||
|
b_buffer = bytearray()
|
||||||
|
b_buffer = b_buffer + self.get_file_header()
|
||||||
|
for obj in self.objects:
|
||||||
|
b_buffer = b_buffer + self.build_object(obj) + b'\n'
|
||||||
|
b_buffer = b_buffer + self.get_xref_table()
|
||||||
|
b_buffer = b_buffer + self.get_trailer() + b'\n'
|
||||||
|
b_buffer = b_buffer + self.get_eof()
|
||||||
|
return b_buffer
|
||||||
|
|
||||||
|
def get_obj(self, nb):
|
||||||
|
for obj in self.objects:
|
||||||
|
if obj[0] == nb:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_end_of_last_object(self):
|
||||||
|
offset = self.get_last_object_offset()
|
||||||
|
offset = offset + len(self.build_object(self.objects[-1]))
|
||||||
|
return offset
|
||||||
|
|
||||||
|
def generate_stream_obj_data(self, data):
|
||||||
|
buf = (b'<<\n/Filter /FlateDecode\n/Length ' +
|
||||||
|
str(len(data)).encode("utf-8") +
|
||||||
|
b'\n>>\nstream\n' +
|
||||||
|
data +
|
||||||
|
b'\nendstream')
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def insert_new_obj_stream_at(self, position, stream_data):
|
||||||
|
'''
|
||||||
|
Return offset of stream data
|
||||||
|
'''
|
||||||
|
logging.info("Insert obj at %d" % position)
|
||||||
|
obj_nb = position
|
||||||
|
obj_off = 0
|
||||||
|
data = self.generate_stream_obj_data(stream_data)
|
||||||
|
obj = (obj_nb, obj_off, data)
|
||||||
|
|
||||||
|
obj_data = self.build_object(obj)
|
||||||
|
full_obj_size = len(obj_data)
|
||||||
|
logging.info("New object full size is: " + str(full_obj_size))
|
||||||
|
|
||||||
|
obj = (obj_nb, obj_off, data)
|
||||||
|
self.objects.insert(position, obj)
|
||||||
|
|
||||||
|
self.reorder_objects()
|
||||||
|
self.fix_trailer_ref()
|
||||||
|
|
||||||
|
def get_first_stream_offset(self):
|
||||||
|
offset = self.file_offset + len(self.get_file_header())
|
||||||
|
r = re.compile(b'stream\n')
|
||||||
|
m = r.search(self.objects[0][2])
|
||||||
|
offset = offset + len(b"1 0 obj\n") + m.end()
|
||||||
|
return offset
|
||||||
|
|
||||||
|
def get_last_stream_offset(self):
|
||||||
|
offset = self.file_offset + self.get_last_object_offset()
|
||||||
|
r = re.compile(b'stream\n')
|
||||||
|
m = r.search(self.build_object(self.objects[-1]))
|
||||||
|
return offset + m.end()
|
||||||
|
|
||||||
|
def get_object_offset(self, index):
|
||||||
|
offset = self.file_offset + len(self.get_file_header())
|
||||||
|
for obj in self.objects[:index]:
|
||||||
|
offset = offset + len(self.build_object(obj)) + 1
|
||||||
|
return offset
|
||||||
|
|
||||||
|
def get_last_object_offset(self):
|
||||||
|
offset = self.get_object_offset(len(self.objects) - 1)
|
||||||
|
return offset
|
||||||
|
|
||||||
|
def insert_new_obj_stream_at_start(self, data):
|
||||||
|
return self.insert_new_obj_stream_at(0, data)
|
||||||
|
|
||||||
|
def insert_new_obj_stream_at_end(self, data):
|
||||||
|
return self.insert_new_obj_stream_at(len(self.objects) + 1,
|
||||||
|
data)
|
||||||
|
|
||||||
|
def generate_translation_table(self):
|
||||||
|
for i in range(len(self.objects)):
|
||||||
|
self.translation_table[(self.objects[i][0],
|
||||||
|
self.objects[i][1])] = i + 1
|
||||||
|
logging.info(self.translation_table)
|
||||||
|
|
||||||
|
def replace_ref(self, ibuffer):
|
||||||
|
'''
|
||||||
|
Exemple:
|
||||||
|
in: AZERTY 6 0 R -- BGT 88 0 R HYT
|
||||||
|
out: AZERTY 77 0 R -- BGT 9 0 R HYT
|
||||||
|
'''
|
||||||
|
index = 0
|
||||||
|
obuffer = bytearray()
|
||||||
|
while True:
|
||||||
|
r = re.compile(b'([0-9]+) ([0-9]+) R')
|
||||||
|
s = r.search(ibuffer[index:])
|
||||||
|
if s is None:
|
||||||
|
obuffer = obuffer + ibuffer[index:]
|
||||||
|
break
|
||||||
|
o_old = int(s.group(1))
|
||||||
|
p_old = int(s.group(2))
|
||||||
|
o_new = self.translation_table[(o_old, p_old)]
|
||||||
|
p_new = p_old
|
||||||
|
|
||||||
|
newref = (str(o_new).encode("utf-8") +
|
||||||
|
b" " +
|
||||||
|
str(p_new).encode("utf-8") +
|
||||||
|
b" R")
|
||||||
|
|
||||||
|
nbuffer = ibuffer[index:index + s.start()] + newref
|
||||||
|
obuffer = obuffer + nbuffer
|
||||||
|
index = index + s.end()
|
||||||
|
return obuffer
|
||||||
|
|
||||||
|
def reorder_objects(self):
|
||||||
|
self.generate_translation_table()
|
||||||
|
offset_obj = len(self.get_file_header())
|
||||||
|
for i in range(len(self.objects)):
|
||||||
|
buf = self.objects[i][2]
|
||||||
|
new_buf = self.replace_ref(buf)
|
||||||
|
obj_nb = self.objects[i][0]
|
||||||
|
new_obj_nb = self.translation_table[(obj_nb, 0)]
|
||||||
|
new_obj_start = offset_obj
|
||||||
|
size_obj = len(self.build_object((new_obj_nb,
|
||||||
|
0,
|
||||||
|
new_buf)))
|
||||||
|
new_obj_end = new_obj_start + size_obj
|
||||||
|
|
||||||
|
offset_obj = new_obj_end + 1
|
||||||
|
obj = (new_obj_nb,
|
||||||
|
0,
|
||||||
|
new_buf)
|
||||||
|
self.objects[i] = obj
|
||||||
|
|
||||||
|
def fix_trailer_ref(self):
|
||||||
|
new_obj_nb = self.translation_table[self.trailer["Root"]]
|
||||||
|
self.trailer["Root"] = (new_obj_nb, 0)
|
||||||
|
|
||||||
|
if "Info" in self.trailer:
|
||||||
|
new_obj_nb = self.translation_table[self.trailer["Info"]]
|
||||||
|
self.trailer["Info"] = (new_obj_nb, 0)
|
||||||
7
PolyglotFile/__init__.py
Normal file
7
PolyglotFile/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .polyglotpdfzip import PolyglotPdfZip
|
||||||
|
from .polyglotzippdf import PolyglotZipPdf
|
||||||
|
from .polyglotszippdf import PolyglotSZipPdf
|
||||||
|
|
||||||
55
PolyglotFile/polyglotpdfzip.py
Normal file
55
PolyglotFile/polyglotpdfzip.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
'''
|
||||||
|
|-------------------------------| -
|
||||||
|
|--------- PDF Header ----------K1 | J1
|
||||||
|
|-------------------------------| -
|
||||||
|
|----- PDF OBJ 1 = ZIP Data ----K2 |
|
||||||
|
|-------------------------------| -
|
||||||
|
|---- Original PDF Ojbects -----K3 | J2
|
||||||
|
|-------------------------------| -
|
||||||
|
|--- Last OBJ = End Zip Data ---K4 |
|
||||||
|
|-------------------------------| |
|
||||||
|
|---------- Xref Table ---------| |
|
||||||
|
|-------------------------------K5 |
|
||||||
|
|----------- Trailer -----------| |
|
||||||
|
|-------------------------------| |
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class PolyglotPdfZip():
|
||||||
|
from PdfFileTransformer import Pdf
|
||||||
|
from ZipFileTransformer import Zip
|
||||||
|
|
||||||
|
def __init__(self, Pdf, Zip):
|
||||||
|
self.buffer = bytearray()
|
||||||
|
self.pdf = Pdf
|
||||||
|
self.zip = Zip
|
||||||
|
self.buffer = bytearray()
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
k2_stream = self.zip.buffer[:self.zip.end_of_data]
|
||||||
|
size_k2_stream = len(k2_stream)
|
||||||
|
self.pdf.insert_new_obj_stream_at_start(k2_stream)
|
||||||
|
offset_k2_stream = self.pdf.get_first_stream_offset()
|
||||||
|
|
||||||
|
k4_stream = self.zip.buffer[self.zip.central_dir_file_header:]
|
||||||
|
size_k4_stream = len(k4_stream)
|
||||||
|
self.pdf.insert_new_obj_stream_at_end(k4_stream)
|
||||||
|
offset_k4_stream = self.pdf.get_last_stream_offset()
|
||||||
|
|
||||||
|
pdf_buffer = self.pdf.get_build_buffer()
|
||||||
|
|
||||||
|
j1 = pdf_buffer[0:offset_k2_stream]
|
||||||
|
j2 = pdf_buffer[offset_k2_stream + size_k2_stream:offset_k4_stream]
|
||||||
|
self.zip.add_data_to_file(j1, j2, True)
|
||||||
|
|
||||||
|
k5 = pdf_buffer[offset_k4_stream + size_k4_stream:]
|
||||||
|
self.buffer = self.zip.buffer + k5
|
||||||
|
|
||||||
|
def write(self, filename):
|
||||||
|
fd = open(filename, "wb")
|
||||||
|
fd.write(self.buffer)
|
||||||
|
fd.close()
|
||||||
110
PolyglotFile/polyglotszippdf.py
Normal file
110
PolyglotFile/polyglotszippdf.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .polyglotpdfzip import PolyglotPdfZip
|
||||||
|
import logging
|
||||||
|
import tempfile
|
||||||
|
from ZipFileTransformer import ZipFile
|
||||||
|
from ZipFileTransformer import Zip
|
||||||
|
from PdfFileTransformer import Pdf
|
||||||
|
|
||||||
|
'''
|
||||||
|
|-----------------------------------| -
|
||||||
|
|--------- ZIP Data[0] = -----------| |
|
||||||
|
|- PDF Header + PDF Obj[0] Header --| |
|
||||||
|
|-----------------------------------| | K2
|
||||||
|
|------- PDF Obj[0] stream = ------| |
|
||||||
|
|--------- ZIP Data LF [1:] --------| |
|
||||||
|
|-----------------------------------| -
|
||||||
|
|------ Original PDF Ojbects -------| |
|
||||||
|
|-----------------------------------| |
|
||||||
|
|------------ Xref Table -----------| |
|
||||||
|
|-----------------------------------| | J2
|
||||||
|
|------------- Trailer -------------| |
|
||||||
|
|-----------------------------------| -
|
||||||
|
|---------- End Zip Data -----------|
|
||||||
|
|-----------------------------------|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class PolyglotSZipPdf(PolyglotPdfZip):
|
||||||
|
|
||||||
|
def __init__(self, Pdf, Zip):
|
||||||
|
super().__init__(Pdf, Zip)
|
||||||
|
|
||||||
|
def get_rebuild_zip_first_part_size(self):
|
||||||
|
|
||||||
|
zo_path = tempfile.mkstemp()[1]
|
||||||
|
logging.info("use tmp file zip: " + zo_path)
|
||||||
|
zo = ZipFile(zo_path, 'a')
|
||||||
|
zi = ZipFile(self.zip.filename, 'r')
|
||||||
|
for zipinfo in zi.infolist():
|
||||||
|
zo.writestr(zipinfo, zi.read(zipinfo))
|
||||||
|
zi.close()
|
||||||
|
zo.close()
|
||||||
|
|
||||||
|
rebuild_zip = Zip(zo_path)
|
||||||
|
|
||||||
|
p = rebuild_zip.end_of_data
|
||||||
|
k2_stream = rebuild_zip.buffer[:p]
|
||||||
|
|
||||||
|
size_k2_stream = len(k2_stream)
|
||||||
|
|
||||||
|
return size_k2_stream
|
||||||
|
|
||||||
|
def get_pdf_header(self):
|
||||||
|
return self.pdf.get_file_header()
|
||||||
|
|
||||||
|
def generate_zip_with_pdf_part(self, filename, pdf_data):
|
||||||
|
|
||||||
|
zo = ZipFile(filename, 'a')
|
||||||
|
zi = ZipFile(self.zip.filename, 'r')
|
||||||
|
zo.writestr(' ', pdf_data, 0)
|
||||||
|
for zipinfo in zi.infolist():
|
||||||
|
zo.writestr(zipinfo, zi.read(zipinfo))
|
||||||
|
zi.close()
|
||||||
|
zo.close()
|
||||||
|
|
||||||
|
def get_rebuild_pdf(self, zo_path, offset):
|
||||||
|
'''
|
||||||
|
Generate polyglot with final zip.
|
||||||
|
'''
|
||||||
|
new_zip = Zip(zo_path)
|
||||||
|
new_pdf = Pdf(self.pdf.filename)
|
||||||
|
|
||||||
|
p1 = new_zip.end_of_first_local_file_header
|
||||||
|
p2 = new_zip.end_of_data
|
||||||
|
k2_stream = new_zip.buffer[p1:p2]
|
||||||
|
|
||||||
|
size_k2_stream = len(k2_stream)
|
||||||
|
new_pdf.insert_new_obj_stream_at_start(k2_stream)
|
||||||
|
k2_stream_offset = new_pdf.get_first_stream_offset()
|
||||||
|
|
||||||
|
new_pdf.file_offset = offset
|
||||||
|
pdf_buffer = new_pdf.get_build_buffer()
|
||||||
|
j2 = pdf_buffer[k2_stream_offset + size_k2_stream:]
|
||||||
|
new_zip.add_data_to_file(b'', j2, True)
|
||||||
|
|
||||||
|
return new_zip.buffer
|
||||||
|
|
||||||
|
def get_pdf_offset(self, zipfile):
|
||||||
|
|
||||||
|
f = open(zipfile, "rb")
|
||||||
|
data = f.read()
|
||||||
|
return data.find(b"%PDF")
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
|
||||||
|
zip_stream_size = self.get_rebuild_zip_first_part_size()
|
||||||
|
pdf_header = self.get_pdf_header()
|
||||||
|
pdf_header = (pdf_header +
|
||||||
|
b'1 0 obj\n<<\n/Filter /FlateDecode\n/Length ' +
|
||||||
|
str(zip_stream_size).encode("utf-8") +
|
||||||
|
b'\n>>\nstream\n')
|
||||||
|
|
||||||
|
filename = tempfile.mkstemp()[1]
|
||||||
|
logging.info("use tmp file for new zip: " + filename)
|
||||||
|
self.generate_zip_with_pdf_part(filename, pdf_header)
|
||||||
|
|
||||||
|
pdf_offset = self.get_pdf_offset(filename)
|
||||||
|
|
||||||
|
self.buffer = self.get_rebuild_pdf(filename, pdf_offset)
|
||||||
37
PolyglotFile/polyglotzippdf.py
Normal file
37
PolyglotFile/polyglotzippdf.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .polyglotpdfzip import PolyglotPdfZip
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
|-------------------------------| -
|
||||||
|
|--------- PDF Header ----------K1 | J1
|
||||||
|
|-------------------------------| -
|
||||||
|
|----- PDF OBJ 1 = ZIP Data ----K2 |
|
||||||
|
|-------------------------------| -
|
||||||
|
|---- Original PDF Ojbects -----K3 |
|
||||||
|
|-------------------------------| |
|
||||||
|
|---------- Xref Table ---------| |
|
||||||
|
|-------------------------------K4 | J2
|
||||||
|
|----------- Trailer -----------| |
|
||||||
|
|-------------------------------| -
|
||||||
|
|-------- End Zip Data ---------| |
|
||||||
|
|-------------------------------| |
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class PolyglotZipPdf(PolyglotPdfZip):
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
k2_stream = self.zip.buffer[:self.zip.end_of_data]
|
||||||
|
size_k2_stream = len(k2_stream)
|
||||||
|
self.pdf.insert_new_obj_stream_at_start(k2_stream)
|
||||||
|
offset_k2_stream = self.pdf.get_first_stream_offset()
|
||||||
|
|
||||||
|
pdf_buffer = self.pdf.get_build_buffer()
|
||||||
|
|
||||||
|
j1 = pdf_buffer[0:offset_k2_stream]
|
||||||
|
j2 = pdf_buffer[offset_k2_stream + size_k2_stream:]
|
||||||
|
|
||||||
|
self.zip.add_data_to_file(j1, j2, True)
|
||||||
|
self.buffer = self.zip.buffer
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# TruePolyglot
|
||||||
|
|
||||||
|
See webiste at https://truepolyglot.hackade.org
|
||||||
4
ZipFileTransformer/__init__.py
Normal file
4
ZipFileTransformer/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .zip import Zip
|
||||||
|
from .zipfile import *
|
||||||
227
ZipFileTransformer/zip.py
Normal file
227
ZipFileTransformer/zip.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Zip:
|
||||||
|
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.filename = filename
|
||||||
|
self.buffer = bytearray()
|
||||||
|
self.size = 0
|
||||||
|
self.end_central_dir = 0
|
||||||
|
self.first_local_file_header = 0
|
||||||
|
self.offset_local_file = []
|
||||||
|
self.offset_central_directory = []
|
||||||
|
self.end_of_data = 0
|
||||||
|
self.end_of_first_local_file_header = 0
|
||||||
|
|
||||||
|
self.read()
|
||||||
|
self.check_header()
|
||||||
|
self.call_all_parsers()
|
||||||
|
self.check_central_directory()
|
||||||
|
self.parse_central_directories()
|
||||||
|
self.parse_local_file_headers()
|
||||||
|
|
||||||
|
def call_all_parsers(self):
|
||||||
|
self.parse_offset_end_central_dir()
|
||||||
|
self.parse_nb_of_disk()
|
||||||
|
self.parse_start_disk()
|
||||||
|
self.parse_nb_of_central_dir()
|
||||||
|
self.parse_nb_total_of_central_dir()
|
||||||
|
self.parse_size_central_dir()
|
||||||
|
self.parse_central_dir_file_header()
|
||||||
|
self.parse_comment_length()
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
with open(self.filename, 'rb') as fd:
|
||||||
|
self.buffer = bytearray(fd.read())
|
||||||
|
self.size = len(self.buffer)
|
||||||
|
logging.info("read " + str(self.size) + " bytes from Zip file")
|
||||||
|
|
||||||
|
def check_header(self):
|
||||||
|
if self.buffer[0:4] != b"PK\x03\x04":
|
||||||
|
raise Exception("Zip header not found")
|
||||||
|
|
||||||
|
def parse_offset_end_central_dir(self):
|
||||||
|
r = re.compile(b'\x06\x05KP')
|
||||||
|
s = r.search(self.buffer[::-1])
|
||||||
|
if s is None:
|
||||||
|
raise Exception("Unable to find end of central directory")
|
||||||
|
self.end_central_dir = self.size - s.end()
|
||||||
|
logging.info("Offset end of central directory: " +
|
||||||
|
hex(self.end_central_dir))
|
||||||
|
|
||||||
|
def parse_nb_of_disk(self):
|
||||||
|
self.nb_of_disk = int.from_bytes(
|
||||||
|
self.buffer[self.end_central_dir + 4:self.end_central_dir + 6],
|
||||||
|
"little")
|
||||||
|
logging.debug("Nb of disk: " + str(self.nb_of_disk))
|
||||||
|
|
||||||
|
def parse_start_disk(self):
|
||||||
|
self.start_disk = int.from_bytes(
|
||||||
|
self.buffer[self.end_central_dir + 6:self.end_central_dir + 8],
|
||||||
|
"little")
|
||||||
|
logging.debug("Start disk: " + str(self.start_disk))
|
||||||
|
|
||||||
|
def parse_nb_of_central_dir(self):
|
||||||
|
self.nb_of_central_dir = int.from_bytes(
|
||||||
|
self.buffer[self.end_central_dir + 8:self.end_central_dir + 10],
|
||||||
|
"little")
|
||||||
|
logging.info("Nb of central directory record: " +
|
||||||
|
str(self.nb_of_central_dir))
|
||||||
|
|
||||||
|
def parse_nb_total_of_central_dir(self):
|
||||||
|
self.nb_total_of_central_dir = int.from_bytes(
|
||||||
|
self.buffer[self.end_central_dir + 10:self.end_central_dir + 12],
|
||||||
|
"little")
|
||||||
|
logging.info("Nb of total central directory record: " +
|
||||||
|
str(self.nb_total_of_central_dir))
|
||||||
|
|
||||||
|
def parse_size_central_dir(self):
|
||||||
|
self.size_central_dir = int.from_bytes(
|
||||||
|
self.buffer[self.end_central_dir + 12:self.end_central_dir + 14],
|
||||||
|
"little")
|
||||||
|
logging.info("Size of central directory: " +
|
||||||
|
str(self.size_central_dir))
|
||||||
|
|
||||||
|
def parse_central_dir_file_header(self):
|
||||||
|
self.central_dir_file_header = int.from_bytes(
|
||||||
|
self.buffer[self.end_central_dir + 16:self.end_central_dir + 20],
|
||||||
|
"little")
|
||||||
|
logging.info("Central directory file header: " +
|
||||||
|
hex(self.central_dir_file_header))
|
||||||
|
|
||||||
|
def parse_comment_length(self):
|
||||||
|
self.comment_length = int.from_bytes(
|
||||||
|
self.buffer[self.end_central_dir + 20:self.end_central_dir + 22],
|
||||||
|
"little")
|
||||||
|
logging.info("Comment length: " +
|
||||||
|
str(self.comment_length))
|
||||||
|
|
||||||
|
def check_central_directory(self):
|
||||||
|
offset = self.central_dir_file_header
|
||||||
|
if (self.buffer[offset:offset + 4] !=
|
||||||
|
b'PK\x01\x02'):
|
||||||
|
raise Exception("Unable to find central directory")
|
||||||
|
logging.info("Found central directory")
|
||||||
|
|
||||||
|
def parse_central_directories(self):
|
||||||
|
if (self.buffer[self.central_dir_file_header:
|
||||||
|
self.central_dir_file_header + 4] !=
|
||||||
|
b'PK\x01\x02'):
|
||||||
|
raise Exception("Unable to find first central directory")
|
||||||
|
logging.info("Found first central directory")
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
size = 0
|
||||||
|
offset = self.central_dir_file_header
|
||||||
|
|
||||||
|
while (self.buffer[size + offset:
|
||||||
|
size + offset + 4] ==
|
||||||
|
b'PK\x01\x02'):
|
||||||
|
|
||||||
|
logging.info("Parse central directory n°" + str(i))
|
||||||
|
logging.info("Offset: " + hex(offset + size))
|
||||||
|
self.offset_central_directory.append(offset + size)
|
||||||
|
filename_length = int.from_bytes(
|
||||||
|
self.buffer[size + offset + 28:size + offset + 30],
|
||||||
|
"little")
|
||||||
|
logging.info("filename length:" + str(filename_length))
|
||||||
|
extra_field_length = int.from_bytes(
|
||||||
|
self.buffer[size + offset + 30:size + offset + 32],
|
||||||
|
"little")
|
||||||
|
logging.info("extra field length:" + str(extra_field_length))
|
||||||
|
comment_length = int.from_bytes(
|
||||||
|
self.buffer[size + offset + 32:size + offset + 34],
|
||||||
|
"little")
|
||||||
|
logging.info("comment length:" + str(comment_length))
|
||||||
|
local_file_header = int.from_bytes(
|
||||||
|
self.buffer[size + offset + 42:size + offset + 46],
|
||||||
|
"little")
|
||||||
|
if i == 0:
|
||||||
|
self.first_local_file_header = local_file_header
|
||||||
|
logging.info("local file header:" + hex(local_file_header))
|
||||||
|
|
||||||
|
i = i + 1
|
||||||
|
size = (size + filename_length +
|
||||||
|
extra_field_length + comment_length + 46)
|
||||||
|
|
||||||
|
logging.debug("parse header at:" + hex(offset + size))
|
||||||
|
|
||||||
|
def parse_local_file_headers(self):
|
||||||
|
size = 0
|
||||||
|
offset = self.first_local_file_header
|
||||||
|
for i in range(self.nb_of_central_dir):
|
||||||
|
logging.info("Parse local file n°" + str(i))
|
||||||
|
compressed_data_lenght = int.from_bytes(
|
||||||
|
self.buffer[size + offset + 18:size + offset + 22],
|
||||||
|
"little")
|
||||||
|
logging.info("compressed data length:" +
|
||||||
|
str(compressed_data_lenght))
|
||||||
|
filename_length = int.from_bytes(
|
||||||
|
self.buffer[size + offset + 26:size + offset + 28],
|
||||||
|
"little")
|
||||||
|
logging.info("filename length:" + str(filename_length))
|
||||||
|
extra_field_length = int.from_bytes(
|
||||||
|
self.buffer[size + offset + 28:size + offset + 30],
|
||||||
|
"little")
|
||||||
|
logging.info("extra field length:" + str(extra_field_length))
|
||||||
|
local_file_size = (compressed_data_lenght +
|
||||||
|
filename_length + extra_field_length + 30)
|
||||||
|
logging.info("local file length:" + hex(local_file_size))
|
||||||
|
size = size + local_file_size
|
||||||
|
logging.debug("parse header at:" + hex(offset + size))
|
||||||
|
self.offset_local_file.append(offset + size)
|
||||||
|
self.end_of_data = offset + size
|
||||||
|
if i == 0:
|
||||||
|
self.end_of_first_local_file_header = self.end_of_data
|
||||||
|
|
||||||
|
def add_data_to_file(self, data_before_local, data_after_local,
|
||||||
|
write_buffer=False):
|
||||||
|
logging.info("Add data before local lenght:" +
|
||||||
|
str(len(data_before_local)))
|
||||||
|
new_buffer = self.buffer
|
||||||
|
for i in self.offset_central_directory:
|
||||||
|
logging.info("parse central directory at: " + hex(i))
|
||||||
|
local_file_header = int.from_bytes(
|
||||||
|
self.buffer[i + 42:i + 46],
|
||||||
|
"little")
|
||||||
|
logging.info("old local file header: " + hex(local_file_header))
|
||||||
|
local_file_header = local_file_header + len(data_before_local)
|
||||||
|
logging.info("new local file header: " + hex(local_file_header))
|
||||||
|
bytes_local_file_header = local_file_header.to_bytes(4, "little")
|
||||||
|
logging.info("change value at:" + hex(i + 42))
|
||||||
|
new_buffer[i + 42:i + 46] = bytes_local_file_header
|
||||||
|
|
||||||
|
logging.info("old central directory header: " +
|
||||||
|
hex(self.central_dir_file_header))
|
||||||
|
new_central_dir_file_header = (self.central_dir_file_header +
|
||||||
|
len(data_after_local) +
|
||||||
|
len(data_before_local))
|
||||||
|
logging.info("new central directory header: " +
|
||||||
|
hex(new_central_dir_file_header))
|
||||||
|
bytes_offset = new_central_dir_file_header.to_bytes(4, "little")
|
||||||
|
new_buffer[self.end_central_dir + 16:
|
||||||
|
self.end_central_dir + 20] = bytes_offset
|
||||||
|
self.buffer = new_buffer
|
||||||
|
|
||||||
|
if write_buffer:
|
||||||
|
new_buffer = (data_before_local +
|
||||||
|
new_buffer[:self.end_of_data] +
|
||||||
|
data_after_local +
|
||||||
|
new_buffer[self.central_dir_file_header:])
|
||||||
|
self.buffer = new_buffer
|
||||||
|
|
||||||
|
def get_local_file_data(self):
|
||||||
|
return self.buffer[:self.end_of_data]
|
||||||
|
|
||||||
|
def get_data_after_central_directory(self):
|
||||||
|
return self.buffer[self.central_dir_file_header:]
|
||||||
|
|
||||||
|
def get_first_part_length(self):
|
||||||
|
return len(self.get_local_file_data())
|
||||||
|
|
||||||
|
def get_second_part_length(self):
|
||||||
|
return len(self.get_data_after_central_directory())
|
||||||
2133
ZipFileTransformer/zipfile.py
Normal file
2133
ZipFileTransformer/zipfile.py
Normal file
File diff suppressed because it is too large
Load Diff
80
pdfcat
Executable file
80
pdfcat
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Concatenate pages from pdf files into a single pdf file.
|
||||||
|
|
||||||
|
Page ranges refer to the previously-named file.
|
||||||
|
A file not followed by a page range means all the pages of the file.
|
||||||
|
|
||||||
|
PAGE RANGES are like Python slices.
|
||||||
|
{page_range_help}
|
||||||
|
EXAMPLES
|
||||||
|
pdfcat -o output.pdf head.pdf content.pdf :6 7: tail.pdf -1
|
||||||
|
Concatenate all of head.pdf, all but page seven of content.pdf,
|
||||||
|
and the last page of tail.pdf, producing output.pdf.
|
||||||
|
|
||||||
|
pdfcat chapter*.pdf >book.pdf
|
||||||
|
You can specify the output file by redirection.
|
||||||
|
|
||||||
|
pdfcat chapter?.pdf chapter10.pdf >book.pdf
|
||||||
|
In case you don't want chapter 10 before chapter 2.
|
||||||
|
"""
|
||||||
|
# Copyright (c) 2014, Steve Witham <switham_github@mac-guyver.com>.
|
||||||
|
# All rights reserved. This software is available under a BSD license;
|
||||||
|
# see https://github.com/mstamy2/PyPDF2/LICENSE
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
import argparse
|
||||||
|
from PdfFileTransformer.PyPDF2.pagerange import PAGE_RANGE_HELP
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=__doc__.format(page_range_help=PAGE_RANGE_HELP),
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
parser.add_argument("-o", "--output",
|
||||||
|
metavar="output_file")
|
||||||
|
parser.add_argument("-v", "--verbose", action="store_true",
|
||||||
|
help="show page ranges as they are being read")
|
||||||
|
parser.add_argument("first_filename", nargs=1,
|
||||||
|
metavar="filename [page range...]")
|
||||||
|
# argparse chokes on page ranges like "-2:" unless caught like this:
|
||||||
|
parser.add_argument("fn_pgrgs", nargs=argparse.REMAINDER,
|
||||||
|
metavar="filenames and/or page ranges")
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.fn_pgrgs.insert(0, args.first_filename[0])
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
from sys import stderr, stdout, exit
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from PdfFileTransformer.PyPDF2 import PdfFileMerger, parse_filename_page_ranges
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = parse_args()
|
||||||
|
filename_page_ranges = parse_filename_page_ranges(args.fn_pgrgs)
|
||||||
|
if args.output:
|
||||||
|
output = open(args.output, "wb")
|
||||||
|
else:
|
||||||
|
stdout.flush()
|
||||||
|
output = os.fdopen(stdout.fileno(), "wb")
|
||||||
|
|
||||||
|
merger = PdfFileMerger()
|
||||||
|
in_fs = dict()
|
||||||
|
try:
|
||||||
|
for (filename, page_range) in filename_page_ranges:
|
||||||
|
if args.verbose:
|
||||||
|
print(filename, page_range, file=stderr)
|
||||||
|
if filename not in in_fs:
|
||||||
|
in_fs[filename] = open(filename, "rb")
|
||||||
|
merger.append(in_fs[filename], pages=page_range)
|
||||||
|
except:
|
||||||
|
print(traceback.format_exc(), file=stderr)
|
||||||
|
print("Error while reading " + filename, file=stderr)
|
||||||
|
exit(1)
|
||||||
|
merger.write(output)
|
||||||
|
# In 3.0, input files must stay open until output is written.
|
||||||
|
# Not closing the in_fs because this script exits now.
|
||||||
7
tests/samples/descriptions.txt
Normal file
7
tests/samples/descriptions.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
== Zip files ==
|
||||||
|
|
||||||
|
test1.zip: deux fichiers et commentaire global.
|
||||||
|
|
||||||
|
== Pdf files ==
|
||||||
|
|
||||||
|
test1.pdf: fichier des impots.
|
||||||
BIN
tests/samples/test1.pdf
Normal file
BIN
tests/samples/test1.pdf
Normal file
Binary file not shown.
BIN
tests/samples/test1.zip
Normal file
BIN
tests/samples/test1.zip
Normal file
Binary file not shown.
BIN
tests/samples/test1_normalized.pdf
Normal file
BIN
tests/samples/test1_normalized.pdf
Normal file
Binary file not shown.
21
tests/test_pdf_add_data.py
Executable file
21
tests/test_pdf_add_data.py
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append("../")
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from PdfFileTransformer import Pdf
|
||||||
|
|
||||||
|
|
||||||
|
input_file = "./samples/test1.pdf"
|
||||||
|
output_file = "./samples/test1_out.pdf"
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
p = Pdf(input_file)
|
||||||
|
p.insert_new_obj_stream_at_start(b'A' * 140)
|
||||||
|
p.insert_new_obj_stream_at_end(b'B' * 120)
|
||||||
|
f = open(output_file, 'wb')
|
||||||
|
f.write(p.get_build_buffer())
|
||||||
|
f.close()
|
||||||
26
tests/test_pdf_normalisation.py
Executable file
26
tests/test_pdf_normalisation.py
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append("../")
|
||||||
|
import logging
|
||||||
|
from PdfFileTransformer.PyPDF2 import PdfFileReader, PdfFileWriter
|
||||||
|
|
||||||
|
|
||||||
|
input_file = "./samples/test1.pdf"
|
||||||
|
output_file = "./samples/test1_out.pdf"
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
f_input = open(input_file, "rb")
|
||||||
|
reader = PdfFileReader(f_input)
|
||||||
|
|
||||||
|
f_output = open(output_file, "wb")
|
||||||
|
writer = PdfFileWriter()
|
||||||
|
|
||||||
|
writer.appendPagesFromReader(reader)
|
||||||
|
writer.setHeader(b"%PDF-1.5")
|
||||||
|
writer.write(f_output)
|
||||||
|
|
||||||
|
f_input.close()
|
||||||
|
f_output.close()
|
||||||
19
tests/test_pdf_rebuild.py
Executable file
19
tests/test_pdf_rebuild.py
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append("../")
|
||||||
|
|
||||||
|
from PdfFileTransformer import Pdf
|
||||||
|
import logging
|
||||||
|
|
||||||
|
input_file = "./samples/test1.pdf"
|
||||||
|
output_file = "./samples/test1_out.pdf"
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
p = Pdf(input_file)
|
||||||
|
f = open(output_file, 'wb')
|
||||||
|
f.write(p.get_build_buffer())
|
||||||
|
f.close()
|
||||||
23
tests/test_polyglot_pdfzip.py
Executable file
23
tests/test_polyglot_pdfzip.py
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append("../")
|
||||||
|
|
||||||
|
from PdfFileTransformer import Pdf
|
||||||
|
from ZipFileTransformer import Zip
|
||||||
|
from PolyglotFile import PolyglotPdfZip
|
||||||
|
import logging
|
||||||
|
|
||||||
|
input_file_pdf = "./samples/test1.pdf"
|
||||||
|
input_file_zip = "./samples/test1.zip"
|
||||||
|
output_file = "./samples/test1_out.pdf"
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
p = Pdf(input_file_pdf)
|
||||||
|
z = Zip(input_file_zip)
|
||||||
|
a = PolyglotPdfZip(p, z)
|
||||||
|
a.generate()
|
||||||
|
a.write(output_file)
|
||||||
20
tests/test_rebuild_zip.py
Executable file
20
tests/test_rebuild_zip.py
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append("../")
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from ZipFileTransformer import Zip, ZipFile
|
||||||
|
|
||||||
|
input_file = "./samples/test1.zip"
|
||||||
|
output_file = "./samples/test1_out.zip"
|
||||||
|
|
||||||
|
zi = ZipFile(input_file,"r")
|
||||||
|
zo = ZipFile(output_file,"w")
|
||||||
|
zo.writestr(' ',b'AAAAAAAAAAAAAAAAAAAAAA',0)
|
||||||
|
for zipinfo in zi.infolist():
|
||||||
|
zo.writestr(zipinfo, zi.read(zipinfo))
|
||||||
|
zi.close()
|
||||||
|
zo.close()
|
||||||
21
tests/test_zip.py
Executable file
21
tests/test_zip.py
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append("../")
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from ZipFileTransformer import Zip
|
||||||
|
|
||||||
|
input_file = "./samples/test1.zip"
|
||||||
|
output_file = tempfile.mktemp()
|
||||||
|
print("Output: " + output_file)
|
||||||
|
|
||||||
|
z = Zip(input_file)
|
||||||
|
a = bytearray(b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
|
||||||
|
b = bytearray(b'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB')
|
||||||
|
z.add_data_to_file(a, b, False)
|
||||||
|
g = open(output_file, "wb")
|
||||||
|
g.write(a + z.get_local_file_data() + b + z.get_data_after_central_directory())
|
||||||
|
g.close()
|
||||||
74
truepolyglot
Executable file
74
truepolyglot
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from PdfFileTransformer import Pdf
|
||||||
|
from ZipFileTransformer import Zip
|
||||||
|
from PolyglotFile import PolyglotZipPdf, PolyglotPdfZip, PolyglotSZipPdf
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
description_str = ('Generate a polyglot file.\n\nFormats availables:\n' +
|
||||||
|
'* pdfzip: Generate a file valid as PDF and ZIP.' +
|
||||||
|
' The format is closest to PDF.\n' +
|
||||||
|
'* zippdf: Generate a file valid as ZIP and PDF.' +
|
||||||
|
' The format is closest to ZIP.\n' +
|
||||||
|
'* szippdf: Generate a file valid as ZIP and PDF.' +
|
||||||
|
' The format is strictly a ZIP.' +
|
||||||
|
' Archive is modified.')
|
||||||
|
usage_str = '%(prog)s format [options] output-file'
|
||||||
|
epilog_str = 'TruePolyglot v1.3'
|
||||||
|
frm = argparse.RawTextHelpFormatter
|
||||||
|
parser = argparse.ArgumentParser(description=description_str,
|
||||||
|
epilog=epilog_str,
|
||||||
|
usage=usage_str,
|
||||||
|
formatter_class=frm)
|
||||||
|
parser.add_argument('format', nargs='+', choices=["pdfzip",
|
||||||
|
"zippdf",
|
||||||
|
"szippdf"],
|
||||||
|
help='Output polyglot format')
|
||||||
|
parser.add_argument('--pdffile', dest='pdffile',
|
||||||
|
help='PDF input file')
|
||||||
|
parser.add_argument('--zipfile', dest='zipfile',
|
||||||
|
help='ZIP input file')
|
||||||
|
parser.add_argument('--verbose', dest='verbose',
|
||||||
|
help='Verbosity level (default: debug)',
|
||||||
|
default="info",
|
||||||
|
choices=["none", "error", "info", "debug"])
|
||||||
|
parser.add_argument('output_file', nargs='+',
|
||||||
|
help='Output polyglot file path')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
formats = ["pdfzip", "zippdf", "szippdf"]
|
||||||
|
if args.format[0] in formats:
|
||||||
|
if args.pdffile is None:
|
||||||
|
parser.error('pdffile is required')
|
||||||
|
if args.zipfile is None:
|
||||||
|
parser.error('zipfile is required')
|
||||||
|
|
||||||
|
if args.verbose == "none":
|
||||||
|
logging.basicConfig(level=logging.CRITICAL)
|
||||||
|
if args.verbose == "error":
|
||||||
|
logging.basicConfig(level=logging.ERROR)
|
||||||
|
if args.verbose == "info":
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
if args.verbose == "debug":
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
p = Pdf(args.pdffile)
|
||||||
|
z = Zip(args.zipfile)
|
||||||
|
if args.format[0] == "pdfzip":
|
||||||
|
a = PolyglotPdfZip(p, z)
|
||||||
|
if args.format[0] == "zippdf":
|
||||||
|
a = PolyglotZipPdf(p, z)
|
||||||
|
if args.format[0] == "szippdf":
|
||||||
|
a = PolyglotSZipPdf(p, z)
|
||||||
|
a.generate()
|
||||||
|
a.write(args.output_file[0])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
63
website/css/styles.css
Normal file
63
website/css/styles.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
html {
|
||||||
|
background-color: black;
|
||||||
|
font-family: Consolas,monaco,monospace;
|
||||||
|
color: #92D050;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
font-family: Consolas,monaco,monospace;
|
||||||
|
color: #92D050;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
background-color: black;
|
||||||
|
font-family: Consolas,monaco,monospace;
|
||||||
|
color: #92D050;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: black;
|
||||||
|
font-family: Consolas,monaco,monospace;
|
||||||
|
color: #92D050;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
a:link {
|
||||||
|
color: #47B8C7;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #47B8C7;
|
||||||
|
}
|
||||||
|
a:active {
|
||||||
|
color: #47B8C7;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid white;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #92D050;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font_reduce {
|
||||||
|
font-size: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: #ffb833;
|
||||||
|
}
|
||||||
61
website/css/styles2.css
Normal file
61
website/css/styles2.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
html {
|
||||||
|
background-color: black;
|
||||||
|
font-family: Consolas,monaco,monospace;
|
||||||
|
color: #92D050;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
font-family: Consolas,monaco,monospace;
|
||||||
|
color: #92D050;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
background-color: black;
|
||||||
|
font-family: Consolas,monaco,monospace;
|
||||||
|
color: #92D050;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: black;
|
||||||
|
font-family: Consolas,monaco,monospace;
|
||||||
|
color: #92D050;
|
||||||
|
}
|
||||||
|
a:link {
|
||||||
|
color: #47B8C7;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #47B8C7;
|
||||||
|
}
|
||||||
|
a:active {
|
||||||
|
color: #47B8C7;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid white;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #92D050;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th a:link {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
th a:visited {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
th a:active {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
BIN
website/favicon.ico
Normal file
BIN
website/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 318 B |
21
website/gen_pocs.sh
Executable file
21
website/gen_pocs.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
find -type f -name 'polyglot.pdf' -delete
|
||||||
|
|
||||||
|
mkdir -p ./samples/pdfzip/poc1/
|
||||||
|
../truepolyglot pdfzip --pdffile ./samples/pdfzip/poc1/doc.pdf --zipfile ./samples/pdfzip/poc1/archive.zip ./samples/pdfzip/poc1/polyglot.pdf
|
||||||
|
|
||||||
|
mkdir -p ./samples/pdfzip/poc2/
|
||||||
|
../truepolyglot pdfzip --pdffile ./samples/pdfzip/poc2/orwell_1984.pdf --zipfile ./samples/pdfzip/poc2/file-FILE5_32.zip ./samples/pdfzip/poc2/polyglot.pdf
|
||||||
|
|
||||||
|
mkdir -p ./samples/pdfzip/poc3/
|
||||||
|
../truepolyglot pdfzip --pdffile ./samples/pdfzip/poc3/x86asm.pdf --zipfile ./samples/pdfzip/poc3/fasmw17304.zip ./samples/pdfzip/poc3/polyglot.pdf
|
||||||
|
|
||||||
|
mkdir -p ./samples/zippdf/poc4/
|
||||||
|
../truepolyglot zippdf --pdffile ./samples/zippdf/poc4/doc.pdf --zipfile ./samples/zippdf/poc4/archive.zip ./samples/zippdf/poc4/polyglot.pdf
|
||||||
|
|
||||||
|
mkdir -p ./samples/szippdf/poc5/
|
||||||
|
../truepolyglot szippdf --pdffile ./samples/szippdf/poc5/electronics.pdf --zipfile ./samples/szippdf/poc5/hello_world.jar ./samples/szippdf/poc5/polyglot.pdf
|
||||||
|
|
||||||
|
mkdir -p ./samples/pdfzip/poc6/
|
||||||
|
../truepolyglot pdfzip --pdffile ./samples/pdfzip/poc6/hexinator.pdf --zipfile ./samples/pdfzip/poc6/eicar.zip ./samples/pdfzip/poc6/polyglot.pdf
|
||||||
249
website/index.html
Normal file
249
website/index.html
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<!doctype html>
|
||||||
|
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
|
||||||
|
<title>TruePolyglot</title>
|
||||||
|
<meta name="description" content="TruePolyglot project website">
|
||||||
|
<meta name="author" content="hackade">
|
||||||
|
<link rel="stylesheet" href="css/styles.css">
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico">
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>TruePolyglot</h1>
|
||||||
|
Truepolyglot is polyglot file generator project.
|
||||||
|
This means that the generated file is composed of several file formats. The same file can be opened as a ZIP file and as a PDF file for example.
|
||||||
|
The idea of this project comes from work of <a href="https://github.com/corkami">Ange Albertini</a>, <a href="https://www.alchemistowl.org/pocorgtfo/pocorgtfo07.pdf">International Journal of Proof-of-Concept or Get The Fuck Out</a> and <a href="https://www.troopers.de/wp-content/uploads/2011/04/TR11_Wolf_OMG_PDF.pdf">Julia Wolf</a> that explain how we can build a polyglot file.<br>
|
||||||
|
Polyglot file can be fastidious to build, even more if you want to respect correctly file format. That's why I decided to build a tool to generate them.<br>
|
||||||
|
My main motivation was the technical challenge.
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h2>Features and changelog</h2>
|
||||||
|
<div class="font_reduce">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Version</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Build a polyglot file valid as PDF and ZIP format and that can be opened with 7Zip and Windows Explorer</td>
|
||||||
|
<td>POC</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Add a stream object in PDF part</td>
|
||||||
|
<td>POC</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Polyglot file checked without warning with <a href="https://poppler.freedesktop.org/">pdftocairo</a></td>
|
||||||
|
<td> >= 1.0</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Polyglot file checked without warning with <a href="https://github.com/ANSSI-FR/caradoc">caradoc</a></td>
|
||||||
|
<td> >= 1.0</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Rebuild PDF Xref Table</td>
|
||||||
|
<td>>= 1.0</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Stream object with correct length header value</td>
|
||||||
|
<td>>= 1.0</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Format "zippdf", file without offset after Zip data</td>
|
||||||
|
<td>>= 1.1</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Polyglot file keep original PDF version</td>
|
||||||
|
<td>>= 1.1.1</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Add "szippdf" format without offset before and after Zip data</td>
|
||||||
|
<td>>= 1.2</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Fix /Length stream object value and PDF offset for szippdf format</td>
|
||||||
|
<td>>= 1.2.1</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>PDF object numbers reorder after insertion</td>
|
||||||
|
<td>>= 1.3</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Polyglot file compatibility</h2>
|
||||||
|
<div class="font_reduce">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Software</th>
|
||||||
|
<th>Formats</th>
|
||||||
|
<th>status</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Acrobat Reader</td>
|
||||||
|
<td>pdfzip, zippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Acrobat Reader</td>
|
||||||
|
<td>szippdf</td>
|
||||||
|
<td><span class="warning">KO</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Sumatra PDF</td>
|
||||||
|
<td>pdfzip, zippdf, szippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Edge</td>
|
||||||
|
<td>pdfzip, zippdf, szippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Firefox</td>
|
||||||
|
<td>pdfzip, zippdf, szippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>7zip</td>
|
||||||
|
<td>pdfzip, zippdf</td>
|
||||||
|
<td><span class="warning">OK with warning</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>7zip</td>
|
||||||
|
<td>szippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Explorer Windows</td>
|
||||||
|
<td>pdfzip, zippdf, szippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Info-ZIP (unzip)</td>
|
||||||
|
<td>pdfzip, zippdf, szippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Evince</td>
|
||||||
|
<td>pdfzip, zippdf, szippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>pdftocairo -pdf</td>
|
||||||
|
<td>pdfzip, zippdf, szippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>caradoc stats</td>
|
||||||
|
<td>pdfzip</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>java</td>
|
||||||
|
<td>szippdf</td>
|
||||||
|
<td>OK</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h2>Examples</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/samples/">Polyglot files repository</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="font_reduce">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>PDF input file</th>
|
||||||
|
<th>Zip input file</th>
|
||||||
|
<th>Format</th>
|
||||||
|
<th>Polyglot</th>
|
||||||
|
<th>Comment</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/samples/pdfzip/poc1/doc.pdf">doc.pdf</a></td>
|
||||||
|
<td><a href="/samples/pdfzip/poc1/archive.zip">archive.zip</a></td>
|
||||||
|
<td>pdfzip</td>
|
||||||
|
<td><a href="/samples/pdfzip/poc1/polyglot.pdf">polyglot.pdf</a></td>
|
||||||
|
<td>PDF/ZIP polyglot - 122 Ko</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/samples/pdfzip/poc2/orwell_1984.pdf">orwell_1984.pdf</a></td>
|
||||||
|
<td><a href="/samples/pdfzip/poc2/file-FILE5_32.zip">file-FILE5_32.zip</a></td>
|
||||||
|
<td>pdfzip</td>
|
||||||
|
<td><a href="/samples/pdfzip/poc2/polyglot.pdf">polyglot.pdf</a></td>
|
||||||
|
<td>PDF/ZIP polyglot - 1.3 Mo</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/samples/pdfzip/poc3/x86asm.pdf">x86asm.pdf</a></td>
|
||||||
|
<td><a href="/samples/pdfzip/poc3/fasmw17304.zip">fasmw17304.zip</a></td>
|
||||||
|
<td>pdfzip</td>
|
||||||
|
<td><a href="/samples/pdfzip/poc3/polyglot.pdf">polyglot.pdf</a></td>
|
||||||
|
<td>PDF/ZIP polyglot - 1.8 Mo</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/samples/zippdf/poc4/doc.pdf">doc.pdf</a></td>
|
||||||
|
<td><a href="/samples/zippdf/poc4/archive.zip">archive.zip</a></td>
|
||||||
|
<td>zippdf</td>
|
||||||
|
<td><a href="/samples/zippdf/poc4/polyglot.pdf">polyglot.pdf</a></td>
|
||||||
|
<td>PDF/ZIP polyglot - 112 Ko</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/samples/szippdf/poc5/electronics.pdf">electronics.pdf</a></td>
|
||||||
|
<td><a href="/samples/szippdf/poc5/hello_world.jar">hello_world.jar</a></td>
|
||||||
|
<td>szippdf</td>
|
||||||
|
<td><a href="/samples/szippdf/poc5/polyglot.pdf">polyglot.pdf</a></td>
|
||||||
|
<td>PDF/JAR polyglot - 778 Ko</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/samples/pdfzip/poc6/hexinator.pdf">hexinator.pdf</a></td>
|
||||||
|
<td><a href="/samples/pdfzip/poc6/eicar.zip">eicar.zip</a> (<a href="https://www.virustotal.com/#/file/2174e17e6b03bb398666c128e6ab0a27d4ad6f7d7922127fe828e07aa94ab79d/detection">scan virustotal.com</a>)</td>
|
||||||
|
<td>pdfzip</td>
|
||||||
|
<td><a href="/samples/pdfzip/poc6/polyglot.pdf">polyglot.pdf</a> (<a href="https://www.virustotal.com/#/file/883d08efc14e0cacc9a260d84fdef285b383cc9a9125366dfb0bf676ddeb0f98/detection">scan virustotal.com</a>)</td>
|
||||||
|
<td>PDF/ZIP polyglot with Eicar test in Zip - 2.9 Mo</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Manual</h2>
|
||||||
|
<pre>
|
||||||
|
usage: truepolyglot format [options] output-file
|
||||||
|
|
||||||
|
Generate a polyglot file.
|
||||||
|
|
||||||
|
Formats availables:
|
||||||
|
* pdfzip: Generate a file valid as PDF and ZIP. The format is closest to PDF.
|
||||||
|
* zippdf: Generate a file valid as ZIP and PDF. The format is closest to ZIP.
|
||||||
|
* szippdf: Generate a file valid as ZIP and PDF. The format is strictly a ZIP. Archive is modified.
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
{pdfzip,zippdf,szippdf}
|
||||||
|
Output polyglot format
|
||||||
|
output_file Output polyglot file path
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--pdffile PDFFILE PDF input file
|
||||||
|
--zipfile ZIPFILE ZIP input file
|
||||||
|
--verbose {none,error,info,debug}
|
||||||
|
Verbosity level (default: debug)
|
||||||
|
|
||||||
|
TruePolyglot v1.3
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h2>Code</h2>
|
||||||
|
|
||||||
|
<a href="https://git.hackade.org/truepolyglot.git/">Project Git repository</a>
|
||||||
|
|
||||||
|
<h2>Contact</h2>
|
||||||
|
On <a href="https://webchat.freenode.net/">IRC Freenode</a> my nickname is hackade or by mail at <a href="mailtp:truepolyglot@hackade.org">truepolyglot@hackade.org</a>.
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
website/robots.txt
Normal file
2
website/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
BIN
website/samples/pdfzip/poc1/archive.zip
Normal file
BIN
website/samples/pdfzip/poc1/archive.zip
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc1/doc.pdf
Normal file
BIN
website/samples/pdfzip/poc1/doc.pdf
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc1/polyglot.pdf
Normal file
BIN
website/samples/pdfzip/poc1/polyglot.pdf
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc2/file-FILE5_32.zip
Normal file
BIN
website/samples/pdfzip/poc2/file-FILE5_32.zip
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc2/orwell_1984.pdf
Normal file
BIN
website/samples/pdfzip/poc2/orwell_1984.pdf
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc2/polyglot.pdf
Normal file
BIN
website/samples/pdfzip/poc2/polyglot.pdf
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc3/fasmw17304.zip
Normal file
BIN
website/samples/pdfzip/poc3/fasmw17304.zip
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc3/polyglot.pdf
Normal file
BIN
website/samples/pdfzip/poc3/polyglot.pdf
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc3/x86asm.pdf
Normal file
BIN
website/samples/pdfzip/poc3/x86asm.pdf
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc6/eicar.zip
Normal file
BIN
website/samples/pdfzip/poc6/eicar.zip
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc6/hexinator.pdf
Normal file
BIN
website/samples/pdfzip/poc6/hexinator.pdf
Normal file
Binary file not shown.
BIN
website/samples/pdfzip/poc6/polyglot.pdf
Normal file
BIN
website/samples/pdfzip/poc6/polyglot.pdf
Normal file
Binary file not shown.
BIN
website/samples/szippdf/poc5/electronics.pdf
Normal file
BIN
website/samples/szippdf/poc5/electronics.pdf
Normal file
Binary file not shown.
BIN
website/samples/szippdf/poc5/hello_world.jar
Normal file
BIN
website/samples/szippdf/poc5/hello_world.jar
Normal file
Binary file not shown.
BIN
website/samples/szippdf/poc5/polyglot.pdf
Normal file
BIN
website/samples/szippdf/poc5/polyglot.pdf
Normal file
Binary file not shown.
BIN
website/samples/zippdf/poc4/archive.zip
Normal file
BIN
website/samples/zippdf/poc4/archive.zip
Normal file
Binary file not shown.
BIN
website/samples/zippdf/poc4/doc.pdf
Normal file
BIN
website/samples/zippdf/poc4/doc.pdf
Normal file
Binary file not shown.
BIN
website/samples/zippdf/poc4/polyglot.pdf
Normal file
BIN
website/samples/zippdf/poc4/polyglot.pdf
Normal file
Binary file not shown.
3
website/start_server.sh
Executable file
3
website/start_server.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "http://127.0.0.1:8000"
|
||||||
|
python -m SimpleHTTPServer 8000
|
||||||
2
website/update.sh
Executable file
2
website/update.sh
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
rsync -av --progress ./ -e ssh dragon:/var/www/html/truepolyglot/
|
||||||
Reference in New Issue
Block a user