First public commit
This commit is contained in:
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = '1.26.0'
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
"""
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .PyPDF2 import PdfFileReader, PdfFileWriter
|
||||
from .pdf import Pdf
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .polyglotpdfzip import PolyglotPdfZip
|
||||
from .polyglotzippdf import PolyglotZipPdf
|
||||
from .polyglotszippdf import PolyglotSZipPdf
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
# TruePolyglot
|
||||
|
||||
See webiste at https://truepolyglot.hackade.org
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .zip import Zip
|
||||
from .zipfile import *
|
||||
@@ -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())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
== Zip files ==
|
||||
|
||||
test1.zip: deux fichiers et commentaire global.
|
||||
|
||||
== Pdf files ==
|
||||
|
||||
test1.pdf: fichier des impots.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+21
@@ -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()
|
||||
Executable
+26
@@ -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()
|
||||
Executable
+19
@@ -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()
|
||||
Executable
+23
@@ -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)
|
||||
Executable
+20
@@ -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()
|
||||
Executable
+21
@@ -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()
|
||||
Executable
+74
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 318 B |
Executable
+21
@@ -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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
echo "http://127.0.0.1:8000"
|
||||
python -m SimpleHTTPServer 8000
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
rsync -av --progress ./ -e ssh dragon:/var/www/html/truepolyglot/
|
||||
Reference in New Issue
Block a user