Implement #631 (Feature Request: forcing metadata from filenames)

This commit is contained in:
Kovid Goyal 2008-09-29 23:12:36 -07:00
parent 4bb44bd1b6
commit 2904bfdcb2
11 changed files with 688 additions and 198 deletions

View File

@ -20,6 +20,7 @@ from calibre.ebooks.lrf.meta import set_metadata as set_lrf_metadata
from calibre.ebooks.metadata.epub import set_metadata as set_epub_metadata
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import prefs
_METADATA_PRIORITIES = [
'html', 'htm', 'xhtml', 'xhtm',
@ -59,7 +60,7 @@ def metadata_from_formats(formats):
def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
if stream_type: stream_type = stream_type.lower()
if stream_type in ('html', 'html', 'xhtml', 'xhtm'):
if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
stream_type = 'html'
if stream_type in ('mobi', 'prc'):
stream_type = 'mobi'
@ -73,18 +74,20 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
if use_libprs_metadata and getattr(opf, 'application_id', None) is not None:
return opf
mi = MetaInformation(None, None)
if prefs['read_file_metadata']:
try:
func = eval(stream_type + '_metadata')
mi = func(stream)
except NameError:
mi = MetaInformation(None, None)
pass
name = os.path.basename(getattr(stream, 'name', ''))
base = metadata_from_filename(name)
if not base.authors:
base.authors = ['Unknown']
base.authors = [_('Unknown')]
if not base.title:
base.title = 'Unknown'
base.title = _('Unknown')
base.smart_update(mi)
if opf is not None:
base.smart_update(opf)

View File

@ -5,12 +5,11 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.pyPdf import PdfFileReader
from pyPdf import PdfFileReader
def get_metadata(stream):
""" Return metadata as a L{MetaInfo} object """
title = 'Unknown'
mi = MetaInformation(title, ['Unknown'])
mi = MetaInformation(_('Unknown'), [_('Unknown')])
stream.seek(0)
try:
info = PdfFileReader(stream).getDocumentInfo()

View File

@ -80,6 +80,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.language.addItem(item[1], QVariant(item[0]))
self.output_format.setCurrentIndex(0 if prefs['output_format'] == 'LRF' else 1)
self.pdf_metadata.setChecked(prefs['read_file_metadata'])
@ -113,6 +114,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
config['confirm_delete'] = bool(self.confirm_delete.isChecked())
pattern = self.filename_pattern.commit()
prefs['filename_pattern'] = pattern
prefs['read_file_metadata'] = bool(self.pdf_metadata.isChecked())
config['save_to_disk_single_format'] = BOOK_EXTENSIONS[self.single_format.currentIndex()]
config['cover_flow_queue_length'] = self.cover_browse.value()
prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString())

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>709</width>
<height>723</height>
<height>750</height>
</rect>
</property>
<property name="windowTitle" >
@ -158,6 +158,19 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="pdf_metadata" >
<property name="toolTip" >
<string>If you disable this setting, metadatas is guessed from the filename instead. This can be configured in the Advanced section.</string>
</property>
<property name="text" >
<string>Read &amp;metadata from files</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2" >
<item row="1" column="0" >

View File

@ -524,6 +524,8 @@ def _prefs():
help=_('The language in which to display the user interface'))
c.add_opt('output_format', default='LRF',
help=_('The default output format for ebook conversions.'))
c.add_opt('read_file_metadata', default=True,
help=_('Read metadata from files'))
c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.')
return c

View File

@ -34,6 +34,11 @@ Implementation of stream filters for PDF.
__author__ = "Mathieu Fenniak"
__author_email__ = "biziqe@mathieu.fenniak.net"
from utils import PdfReadError
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
try:
import zlib
@ -100,32 +105,33 @@ class FlateDecode(object):
# predictor 1 == no predictor
if predictor != 1:
columns = decodeParms["/Columns"]
if predictor >= 10:
newdata = ""
# 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 = "\x00"*rowlength
for row in range(len(data) / rowlength):
rowdata = list(data[(row*rowlength):((row+1)*rowlength)])
filterByte = ord(rowdata[0])
prev_rowdata = (0,) * rowlength
for row in xrange(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] = chr((ord(rowdata[i]) + ord(rowdata[i-1])) % 256)
rowdata[i] = (rowdata[i] + rowdata[i-1]) % 256
elif filterByte == 2:
for i in range(1, rowlength):
rowdata[i] = chr((ord(rowdata[i]) + ord(prev_rowdata[i])) % 256)
rowdata[i] = (rowdata[i] + prev_rowdata[i]) % 256
else:
# unsupported PNG filter
assert False
raise PdfReadError("Unsupported PNG filter %r" % filterByte)
prev_rowdata = rowdata
newdata += ''.join(rowdata[1:])
data = newdata
output.write(''.join([chr(x) for x in rowdata[1:]]))
data = output.getvalue()
else:
# unsupported predictor
assert False
raise PdfReadError("Unsupported flatedecode predictor %r" % predictor)
return data
decode = staticmethod(decode)
@ -220,9 +226,15 @@ def decodeStreamData(stream):
data = ASCIIHexDecode.decode(data)
elif filterType == "/ASCII85Decode":
data = ASCII85Decode.decode(data)
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
assert False
raise NotImplementedError("unsupported filter %s" % filterType)
return data
if __name__ == "__main__":
@ -237,3 +249,4 @@ if __name__ == "__main__":
"""
ascii85_originalText="Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure."
assert ASCII85Decode.decode(ascii85Test) == ascii85_originalText

View File

@ -203,6 +203,10 @@ class IndirectObject(PdfObject):
class FloatObject(decimal.Decimal, PdfObject):
def __new__(cls, value="0", context=None):
return decimal.Decimal.__new__(cls, str(value), context)
def __repr__(self):
return str(self)
def writeToStream(self, stream, encryption_key):
stream.write(str(self))
@ -419,8 +423,73 @@ class NameObject(str, PdfObject):
class DictionaryObject(dict, PdfObject):
def __init__(self):
pass
def __init__(self, *args, **kwargs):
if len(args) == 0:
self.update(kwargs)
elif len(args) == 1:
arr = args[0]
# If we're passed a list/tuple, make a dict out of it
if not hasattr(arr, "iteritems"):
newarr = {}
for k, v in arr:
newarr[k] = v
arr = newarr
self.update(arr)
else:
raise TypeError("dict expected at most 1 argument, got 3")
def update(self, arr):
# note, a ValueError halfway through copying values
# will leave half the values in this dict.
for k, v in arr.iteritems():
self.__setitem__(k, v)
def raw_get(self, key):
return dict.__getitem__(self, key)
def __setitem__(self, key, value):
if not isinstance(key, PdfObject):
raise ValueError("key must be PdfObject")
if not isinstance(value, PdfObject):
raise ValueError("value must be PdfObject")
return dict.__setitem__(self, key, value)
def setdefault(self, key, value=None):
if not isinstance(key, PdfObject):
raise ValueError("key must be PdfObject")
if not isinstance(value, PdfObject):
raise ValueError("value must be PdfObject")
return dict.setdefault(self, key, value)
def __getitem__(self, key):
return dict.__getitem__(self, key).getObject()
##
# Retrieves XMP (Extensible Metadata Platform) data relevant to the
# this object, if available.
# <p>
# Stability: Added in v1.12, will exist for all future v1.x releases.
# @return Returns a {@link #xmp.XmpInformation XmlInformation} instance
# that can be used to access XMP metadata from the document. Can also
# return None if no metadata was found on the document root.
def getXmpMetadata(self):
metadata = self.get("/Metadata", None)
if metadata == None:
return None
metadata = metadata.getObject()
import xmp
if not isinstance(metadata, xmp.XmpInformation):
metadata = xmp.XmpInformation(metadata)
self[NameObject("/Metadata")] = metadata
return metadata
##
# Read-only property that accesses the {@link
# #DictionaryObject.getXmpData getXmpData} function.
# <p>
# Stability: Added in v1.12, will exist for all future v1.x releases.
xmpMetadata = property(lambda self: self.getXmpMetadata(), None, None)
def writeToStream(self, stream, encryption_key):
stream.write("<<\n")
@ -563,7 +632,7 @@ class EncodedStreamObject(StreamObject):
return self.decodedSelf.getData()
else:
# create decoded object
decoded = StreamObject()
decoded = DecodedStreamObject()
decoded._data = filters.decodeStreamData(self)
for key, value in self.items():
if not key in ("/Length", "/Filter", "/DecodeParms"):
@ -583,8 +652,8 @@ class RectangleObject(ArrayObject):
ArrayObject.__init__(self, [self.ensureIsNumber(x) for x in arr])
def ensureIsNumber(self, value):
if not isinstance(value, NumberObject):
value = NumberObject(value)
if not isinstance(value, (NumberObject, FloatObject)):
value = FloatObject(value)
return value
def __repr__(self):

View File

@ -88,7 +88,8 @@ class PdfFileWriter(object):
return IndirectObject(len(self._objects), 0, self)
def getObject(self, ido):
assert ido.pdf == self
if ido.pdf != self:
raise ValueError("pdf must be self")
return self._objects[ido.idnum - 1]
##
@ -105,7 +106,7 @@ class PdfFileWriter(object):
page = self._addObject(page)
pages = self.getObject(self._pages)
pages["/Kids"].append(page)
pages["/Count"] = NumberObject(pages["/Count"] + 1)
pages[NameObject("/Count")] = NumberObject(pages["/Count"] + 1)
##
# Encrypt this PDF file with the PDF Standard encryption handler.
@ -272,7 +273,6 @@ class PdfFileWriter(object):
class PdfFileReader(object):
def __init__(self, stream):
self.flattenedPages = None
self.pageNumbers = {}
self.resolvedObjects = {}
self.read(stream)
self.stream = stream
@ -290,7 +290,7 @@ class PdfFileReader(object):
def getDocumentInfo(self):
if not self.trailer.has_key("/Info"):
return None
obj = self.getObject(self.trailer['/Info'])
obj = self.trailer['/Info']
retval = DocumentInformation()
retval.update(obj)
return retval
@ -302,6 +302,28 @@ class PdfFileReader(object):
# Stability: Added in v1.7, will exist for all future v1.x releases.
documentInfo = property(lambda self: self.getDocumentInfo(), None, None)
##
# Retrieves XMP (Extensible Metadata Platform) data from the PDF document
# root.
# <p>
# Stability: Added in v1.12, will exist for all future v1.x releases.
# @return Returns a {@link #generic.XmpInformation XmlInformation}
# instance that can be used to access XMP metadata from the document.
# Can also return None if no metadata was found on the document root.
def getXmpMetadata(self):
try:
self._override_encryption = True
return self.trailer["/Root"].getXmpMetadata()
finally:
self._override_encryption = False
##
# Read-only property that accesses the {@link #PdfFileReader.getXmpData
# getXmpData} function.
# <p>
# Stability: Added in v1.12, will exist for all future v1.x releases.
xmpMetadata = property(lambda self: self.getXmpMetadata(), None, None)
##
# Calculates the number of pages in this PDF file.
# <p>
@ -346,43 +368,39 @@ class PdfFileReader(object):
# Stability: Added in v1.10, will exist for all future v1.x releases.
# @return Returns a dict which maps names to {@link #Destination
# destinations}.
def getNamedDestinations(self, tree = None, map = None):
if self.flattenedPages == None:
self._flatten()
get = self.safeGetObject
if map == None:
map = {}
catalog = get(self.trailer["/Root"])
def getNamedDestinations(self, tree=None, retval=None):
if retval == None:
retval = {}
catalog = self.trailer["/Root"]
# get the name tree
if catalog.has_key("/Dests"):
tree = get(catalog["/Dests"])
tree = catalog["/Dests"]
elif catalog.has_key("/Names"):
names = get(catalog['/Names'])
names = catalog['/Names']
if names.has_key("/Dests"):
tree = get(names['/Dests'])
tree = names['/Dests']
if tree == None:
return map
return retval
if tree.has_key("/Kids"):
# recurse down the tree
for kid in get(tree["/Kids"]):
self.getNamedDestinations(get(kid), map)
for kid in tree["/Kids"]:
self.getNamedDestinations(kid.getObject(), retval)
if tree.has_key("/Names"):
names = get(tree["/Names"])
names = tree["/Names"]
for i in range(0, len(names), 2):
key = get(names[i])
val = get(names[i+1])
key = names[i].getObject()
val = names[i+1].getObject()
if isinstance(val, DictionaryObject) and val.has_key('/D'):
val = get(val['/D'])
dest = self._buildDestination(val, key)
val = val['/D']
dest = self._buildDestination(key, val)
if dest != None:
map[key] = dest
retval[key] = dest
return map
return retval
##
# Read-only property that accesses the {@link #PdfFileReader.getOutlines
@ -396,20 +414,16 @@ class PdfFileReader(object):
# <p>
# Stability: Added in v1.10, will exist for all future v1.x releases.
# @return Returns a nested list of {@link #Destination destinations}.
def getOutlines(self, node = None, outlines = None):
if self.flattenedPages == None:
self._flatten()
get = self.safeGetObject
def getOutlines(self, node=None, outlines=None):
if outlines == None:
outlines = []
catalog = get(self.trailer["/Root"])
catalog = self.trailer["/Root"]
# get the outline dictionary and named destinations
if catalog.has_key("/Outlines"):
lines = get(catalog["/Outlines"])
lines = catalog["/Outlines"]
if lines.has_key("/First"):
node = get(lines["/First"])
node = lines["/First"]
self._namedDests = self.getNamedDestinations()
if node == None:
@ -424,49 +438,44 @@ class PdfFileReader(object):
# check for sub-outlines
if node.has_key("/First"):
subOutlines = []
self.getOutlines(get(node["/First"]), subOutlines)
self.getOutlines(node["/First"], subOutlines)
if subOutlines:
outlines.append(subOutlines)
if not node.has_key("/Next"):
break
node = get(node["/Next"])
node = node["/Next"]
return outlines
def _buildDestination(self, array, title):
if not (isinstance(array, ArrayObject) and len(array) >= 2 and \
isinstance(array[0], IndirectObject)):
return None
pageKey = (array[0].generation, array[0].idnum)
if not self.pageNumbers.has_key(pageKey):
return None
pageNum = self.pageNumbers[pageKey]
return Destination(*([title, pageNum]+array[1:]))
def _buildDestination(self, title, array):
page, typ = array[0:2]
array = array[2:]
return Destination(title, page, typ, *array)
def _buildOutline(self, node):
dest, title, outline = None, None, None
if node.has_key("/A") and node.has_key("/Title"):
# Action, section 8.5 (only type GoTo supported)
title = self.safeGetObject(node["/Title"])
action = self.safeGetObject(node["/A"])
title = node["/Title"]
action = node["/A"]
if action["/S"] == "/GoTo":
dest = self.safeGetObject(action["/D"])
dest = action["/D"]
elif node.has_key("/Dest") and node.has_key("/Title"):
# Destination, section 8.2.1
title = self.safeGetObject(node["/Title"])
dest = self.safeGetObject(node["/Dest"])
title = node["/Title"]
dest = node["/Dest"]
# if destination found, then create outline
if dest:
if isinstance(dest, ArrayObject):
outline = self._buildDestination(dest, title)
elif isinstance(dest, str) and self._namedDests.has_key(dest):
outline = self._buildDestination(title, dest)
elif isinstance(dest, unicode) and self._namedDests.has_key(dest):
outline = self._namedDests[dest]
outline.title = title
outline[NameObject("/Title")] = title
else:
raise utils.PdfReadError("Unexpected destination %r" % dest)
return outline
##
@ -478,7 +487,7 @@ class PdfFileReader(object):
pages = property(lambda self: ConvertFunctionsToVirtualList(self.getNumPages, self.getPage),
None, None)
def _flatten(self, pages = None, inherit = None):
def _flatten(self, pages=None, inherit=None):
inheritablePageAttributes = (
NameObject("/Resources"), NameObject("/MediaBox"),
NameObject("/CropBox"), NameObject("/Rotate")
@ -487,37 +496,25 @@ class PdfFileReader(object):
inherit = dict()
if pages == None:
self.flattenedPages = []
catalog = self.getObject(self.trailer["/Root"])
pages = self.getObject(catalog["/Pages"])
indirectReference = None
if isinstance(pages, IndirectObject):
indirectReference = pages
pages = self.getObject(pages)
catalog = self.trailer["/Root"].getObject()
pages = catalog["/Pages"].getObject()
t = pages["/Type"]
if t == "/Pages":
for attr in inheritablePageAttributes:
if pages.has_key(attr):
inherit[attr] = pages[attr]
for page in self.safeGetObject(pages["/Kids"]):
self._flatten(page, inherit)
for page in pages["/Kids"]:
self._flatten(page.getObject(), inherit)
elif t == "/Page":
for attr,value in inherit.items():
# if the page has it's own value, it does not inherit the
# parent's value:
if not pages.has_key(attr):
pages[attr] = value
pageObj = PageObject(self, indirectReference)
pageObj = PageObject(self)
pageObj.update(pages)
if indirectReference:
key = (indirectReference.generation, indirectReference.idnum)
self.pageNumbers[key] = len(self.flattenedPages)
self.flattenedPages.append(pageObj)
def safeGetObject(self, obj):
if isinstance(obj, IndirectObject):
return self.safeGetObject(self.getObject(obj))
return obj
def getObject(self, indirectReference):
retval = self.resolvedObjects.get(indirectReference.generation, {}).get(indirectReference.idnum, None)
if retval != None:
@ -527,7 +524,7 @@ class PdfFileReader(object):
# indirect reference to object in object stream
# read the entire object stream into memory
stmnum,idx = self.xref_objStm[indirectReference.idnum]
objStm = self.getObject(IndirectObject(stmnum, 0, self))
objStm = IndirectObject(stmnum, 0, self).getObject()
assert objStm['/Type'] == '/ObjStm'
assert idx < objStm['/N']
streamData = StringIO(objStm.getData())
@ -619,7 +616,7 @@ class PdfFileReader(object):
# read all cross reference tables and their trailers
self.xref = {}
self.xref_objStm = {}
self.trailer = {}
self.trailer = DictionaryObject()
while 1:
# load the xref table
stream.seek(startxref, 0)
@ -641,6 +638,16 @@ class PdfFileReader(object):
cnt = 0
while cnt < size:
line = stream.read(20)
# It's very clear in section 3.4.3 of the PDF spec
# that all cross-reference table lines are a fixed
# 20 bytes. However... some malformed PDF files
# use a single character EOL without a preceeding
# space. Detect that case, and seek the stream
# back one character. (0-9 means we've bled into
# the next xref entry, t means we've bled into the
# text "trailer"):
if line[-1] in "0123456789t":
stream.seek(-1, 1)
offset, generation = line[:16].split(" ")
offset, generation = int(offset), int(generation)
if not self.xref.has_key(generation):
@ -669,8 +676,8 @@ class PdfFileReader(object):
for key, value in newTrailer.items():
if not self.trailer.has_key(key):
self.trailer[key] = value
if newTrailer.has_key(NameObject("/Prev")):
startxref = newTrailer[NameObject("/Prev")]
if newTrailer.has_key("/Prev"):
startxref = newTrailer["/Prev"]
else:
break
elif x.isdigit():
@ -681,8 +688,9 @@ class PdfFileReader(object):
assert xrefstream["/Type"] == "/XRef"
self.cacheIndirectObject(generation, idnum, xrefstream)
streamData = StringIO(xrefstream.getData())
num, size = xrefstream.get("/Index", [0, xrefstream.get("/Size")])
idx_pairs = xrefstream.get("/Index", [0, xrefstream.get("/Size")])
entrySizes = xrefstream.get("/W")
for num, size in self._pairs(idx_pairs):
cnt = 0
while cnt < size:
for i in range(len(entrySizes)):
@ -709,15 +717,17 @@ class PdfFileReader(object):
elif xref_type == 1:
if not self.xref.has_key(generation):
self.xref[generation] = {}
if not num in self.xref[generation]:
self.xref[generation][num] = byte_offset
elif xref_type == 2:
if not num in self.xref_objStm:
self.xref_objStm[num] = [objstr_num, obstr_idx]
cnt += 1
num += 1
trailerKeys = "/Root", "/Encrypt", "/Info", "/ID"
for key in trailerKeys:
if xrefstream.has_key(key) and not self.trailer.has_key(key):
self.trailer[NameObject(key)] = xrefstream[key]
self.trailer[NameObject(key)] = xrefstream.raw_get(key)
if xrefstream.has_key("/Prev"):
startxref = xrefstream["/Prev"]
else:
@ -737,6 +747,14 @@ class PdfFileReader(object):
assert False
break
def _pairs(self, array):
i = 0
while True:
yield array[i], array[i+1]
i += 2
if (i+1) >= len(array):
break
def readNextEndLine(self, stream):
line = ""
while True:
@ -778,7 +796,7 @@ class PdfFileReader(object):
self._override_encryption = False
def _decrypt(self, password):
encrypt = self.safeGetObject(self.trailer['/Encrypt'])
encrypt = self.trailer['/Encrypt'].getObject()
if encrypt['/Filter'] != '/Standard':
raise NotImplementedError, "only Standard PDF encryption handler is available"
if not (encrypt['/V'] in (1, 2)):
@ -788,13 +806,13 @@ class PdfFileReader(object):
self._decryption_key = key
return 1
else:
rev = self.safeGetObject(encrypt['/R'])
rev = encrypt['/R'].getObject()
if rev == 2:
keylen = 5
else:
keylen = self.safeGetObject(encrypt['/Length']) / 8
keylen = encrypt['/Length'].getObject() / 8
key = _alg33_1(password, rev, keylen)
real_O = self.safeGetObject(encrypt["/O"])
real_O = encrypt["/O"].getObject()
if rev == 2:
userpass = utils.RC4_encrypt(key, real_O)
else:
@ -812,20 +830,20 @@ class PdfFileReader(object):
return 0
def _authenticateUserPassword(self, password):
encrypt = self.safeGetObject(self.trailer['/Encrypt'])
rev = self.safeGetObject(encrypt['/R'])
owner_entry = self.safeGetObject(encrypt['/O']).original_bytes
p_entry = self.safeGetObject(encrypt['/P'])
id_entry = self.safeGetObject(self.trailer['/ID'])
id1_entry = self.safeGetObject(id_entry[0])
encrypt = self.trailer['/Encrypt'].getObject()
rev = encrypt['/R'].getObject()
owner_entry = encrypt['/O'].getObject().original_bytes
p_entry = encrypt['/P'].getObject()
id_entry = self.trailer['/ID'].getObject()
id1_entry = id_entry[0].getObject()
if rev == 2:
U, key = _alg34(password, owner_entry, p_entry, id1_entry)
elif rev >= 3:
U, key = _alg35(password, rev,
self.safeGetObject(encrypt["/Length"]) / 8, owner_entry,
encrypt["/Length"].getObject() / 8, owner_entry,
p_entry, id1_entry,
self.safeGetObject(encrypt.get("/EncryptMetadata", False)))
real_U = self.safeGetObject(encrypt['/U']).original_bytes
encrypt.get("/EncryptMetadata", BooleanObject(False)).getObject())
real_U = encrypt['/U'].getObject().original_bytes
return U == real_U, key
def getIsEncrypted(self):
@ -874,10 +892,9 @@ def createRectangleAccessor(name, fallback):
# will be created by accessing the {@link #PdfFileReader.getPage getPage}
# function of the {@link #PdfFileReader PdfFileReader} class.
class PageObject(DictionaryObject):
def __init__(self, pdf, indirectReference = None):
def __init__(self, pdf):
DictionaryObject.__init__(self)
self.pdf = pdf
self.indirectReference = indirectReference
##
# Rotates a page clockwise by increments of 90 degrees.
@ -1058,7 +1075,7 @@ class PageObject(DictionaryObject):
# implementation-defined manner. Default value: same as MediaBox.
# <p>
# Stability: Added in v1.4, will exist for all future v1.x releases.
cropBox = createRectangleAccessor("/CropBox", ("/CropBox",))
cropBox = createRectangleAccessor("/CropBox", ("/MediaBox",))
##
# A rectangle (RectangleObject), expressed in default user space units,
@ -1110,7 +1127,15 @@ class ContentStream(DecodedStreamObject):
break
stream.seek(-1, 1)
if peek.isalpha() or peek == "'" or peek == '"':
operator = readUntilWhitespace(stream, maxchars=2)
operator = ""
while True:
tok = stream.read(1)
if tok.isspace() or tok in NameObject.delimiterCharacters:
stream.seek(-1, 1)
break
elif tok == '':
break
operator += tok
if operator == "BI":
# begin inline image - a completely different parsing
# mechanism is required, of course... thanks buddy...
@ -1120,6 +1145,14 @@ class ContentStream(DecodedStreamObject):
else:
self.operations.append((operands, operator))
operands = []
elif peek == '%':
# If we encounter a comment in the content stream, we have to
# handle it here. Typically, readObject will handle
# encountering a comment -- but readObject assumes that
# following the comment must be the object we're trying to
# read. In this case, it could be an operator instead.
while peek not in ('\r', '\n'):
peek = stream.read(1)
else:
operands.append(readObject(stream, None))
@ -1251,86 +1284,74 @@ class DocumentInformation(DictionaryObject):
# See section 8.2.1 of the PDF 1.6 reference.
# Stability: Added in v1.10, will exist for all v1.x releases.
class Destination(DictionaryObject):
def __init__(self, *args):
def __init__(self, title, page, typ, *args):
DictionaryObject.__init__(self)
self.title = args[0]
self["/Page"], self["/Type"] = args[1], args[2]
self[NameObject("/Title")] = title
self[NameObject("/Page")] = page
self[NameObject("/Type")] = typ
# from table 8.2 of the PDF 1.6 reference.
mapNull = lambda x: {True: None, False: x}[isinstance(x, NullObject)]
params = map(mapNull, args[3:])
type = self["/Type"]
if type == "/XYZ":
self["/Left"], self["/Top"], self["/Zoom"] = params
elif type == "/FitR":
self["/Left"], self["/Bottom"], \
self["/Right"], self["/Top"] = params
elif type in ["/FitH", "FitBH"]:
self["/Top"], = params
elif type in ["/FitV", "FitBV"]:
self["/Left"], = params
elif type in ["/Fit", "FitB"]:
if typ == "/XYZ":
(self[NameObject("/Left")], self[NameObject("/Top")],
self[NameObject("/Zoom")]) = args
elif typ == "/FitR":
(self[NameObject("/Left")], self[NameObject("/Bottom")],
self[NameObject("/Right")], self[NameObject("/Top")]) = args
elif typ in ["/FitH", "FitBH"]:
self[NameObject("/Top")], = args
elif typ in ["/FitV", "FitBV"]:
self[NameObject("/Left")], = args
elif typ in ["/Fit", "FitB"]:
pass
else:
raise utils.PdfReadError, "Unknown Destination Type: " + type
def setTitle(self, title):
self["/Title"] = title.strip()
raise utils.PdfReadError("Unknown Destination Type: %r" % typ)
##
# Read-write property accessing the destination title.
# Read-only property accessing the destination title.
# @return A string.
title = property(lambda self: self.get("/Title"), setTitle, None)
title = property(lambda self: self.get("/Title"))
##
# Read-only property accessing the destination page.
# @return An integer.
page = property(lambda self: self.get("/Page"), None, None)
page = property(lambda self: self.get("/Page"))
##
# Read-only property accessing the destination type.
# @return A string.
type = property(lambda self: self.get("/Type"), None, None)
typ = property(lambda self: self.get("/Type"))
##
# Read-only property accessing the zoom factor.
# @return A number, or None if not available.
zoom = property(lambda self: self.get("/Zoom", None), None, None)
zoom = property(lambda self: self.get("/Zoom", None))
##
# Read-only property accessing the left horizontal coordinate.
# @return A number, or None if not available.
left = property(lambda self: self.get("/Left", None), None, None)
left = property(lambda self: self.get("/Left", None))
##
# Read-only property accessing the right horizontal coordinate.
# @return A number, or None if not available.
right = property(lambda self: self.get("/Right", None), None, None)
right = property(lambda self: self.get("/Right", None))
##
# Read-only property accessing the top vertical coordinate.
# @return A number, or None if not available.
top = property(lambda self: self.get("/Top", None), None, None)
top = property(lambda self: self.get("/Top", None))
##
# Read-only property accessing the bottom vertical coordinate.
# @return A number, or None if not available.
bottom = property(lambda self: self.get("/Bottom", None), None, None)
bottom = property(lambda self: self.get("/Bottom", None))
def convertToInt(d, size):
if size <= 4:
d = "\x00\x00\x00\x00" + d
d = d[-4:]
return struct.unpack(">l", d)[0]
elif size <= 8:
if size > 8:
raise utils.PdfReadError("invalid size in convertToInt")
d = "\x00\x00\x00\x00\x00\x00\x00\x00" + d
d = d[-8:]
return struct.unpack(">q", d)[0]
else:
# size too big
assert False
# ref: pdf1.8 spec section 3.5.2 algorithm 3.2
_encryption_padding = '\x28\xbf\x4e\x5e\x4e\x75\x8a\x41\x64\x00\x4e\x56' + \

View File

@ -34,6 +34,19 @@ Utility functions for PDF library.
__author__ = "Mathieu Fenniak"
__author_email__ = "biziqe@mathieu.fenniak.net"
#ENABLE_PSYCO = False
#if ENABLE_PSYCO:
# try:
# import psyco
# except ImportError:
# ENABLE_PSYCO = False
#
#if not ENABLE_PSYCO:
# class psyco:
# def proxy(func):
# return func
# proxy = staticmethod(proxy)
def readUntilWhitespace(stream, maxchars=None):
txt = ""
while True:

355
src/pyPdf/xmp.py Normal file
View File

@ -0,0 +1,355 @@
import re
import datetime
import decimal
from generic import PdfObject
from xml.dom import getDOMImplementation
from xml.dom.minidom import parseString
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)
##
# An object that represents Adobe XMP metadata.
class XmpInformation(PdfObject):
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
##
# Contributors to the resource (other than the authors). An unsorted
# array of names.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_contributor = property(_getter_bag(DC_NAMESPACE, "contributor", _converter_string))
##
# Text describing the extent or scope of the resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_coverage = property(_getter_single(DC_NAMESPACE, "coverage", _converter_string))
##
# A sorted array of names of the authors of the resource, listed in order
# of precedence.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_creator = property(_getter_seq(DC_NAMESPACE, "creator", _converter_string))
##
# A sorted array of dates (datetime.datetime instances) of signifigance to
# the resource. The dates and times are in UTC.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_date = property(_getter_seq(DC_NAMESPACE, "date", _converter_date))
##
# A language-keyed dictionary of textual descriptions of the content of the
# resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_description = property(_getter_langalt(DC_NAMESPACE, "description", _converter_string))
##
# The mime-type of the resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_format = property(_getter_single(DC_NAMESPACE, "format", _converter_string))
##
# Unique identifier of the resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_identifier = property(_getter_single(DC_NAMESPACE, "identifier", _converter_string))
##
# An unordered array specifying the languages used in the resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_language = property(_getter_bag(DC_NAMESPACE, "language", _converter_string))
##
# An unordered array of publisher names.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_publisher = property(_getter_bag(DC_NAMESPACE, "publisher", _converter_string))
##
# An unordered array of text descriptions of relationships to other
# documents.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_relation = property(_getter_bag(DC_NAMESPACE, "relation", _converter_string))
##
# A language-keyed dictionary of textual descriptions of the rights the
# user has to this resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_rights = property(_getter_langalt(DC_NAMESPACE, "rights", _converter_string))
##
# Unique identifier of the work from which this resource was derived.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_source = property(_getter_single(DC_NAMESPACE, "source", _converter_string))
##
# An unordered array of descriptive phrases or keywrods that specify the
# topic of the content of the resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_subject = property(_getter_bag(DC_NAMESPACE, "subject", _converter_string))
##
# A language-keyed dictionary of the title of the resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_title = property(_getter_langalt(DC_NAMESPACE, "title", _converter_string))
##
# An unordered array of textual descriptions of the document type.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
dc_type = property(_getter_bag(DC_NAMESPACE, "type", _converter_string))
##
# An unformatted text string representing document keywords.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
pdf_keywords = property(_getter_single(PDF_NAMESPACE, "Keywords", _converter_string))
##
# The PDF file version, for example 1.0, 1.3.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
pdf_pdfversion = property(_getter_single(PDF_NAMESPACE, "PDFVersion", _converter_string))
##
# The name of the tool that created the PDF document.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
pdf_producer = property(_getter_single(PDF_NAMESPACE, "Producer", _converter_string))
##
# The date and time the resource was originally created. The date and
# time are returned as a UTC datetime.datetime object.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
xmp_createDate = property(_getter_single(XMP_NAMESPACE, "CreateDate", _converter_date))
##
# The date and time the resource was last modified. The date and time
# are returned as a UTC datetime.datetime object.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
xmp_modifyDate = property(_getter_single(XMP_NAMESPACE, "ModifyDate", _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.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
xmp_metadataDate = property(_getter_single(XMP_NAMESPACE, "MetadataDate", _converter_date))
##
# The name of the first known tool used to create the resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
xmp_creatorTool = property(_getter_single(XMP_NAMESPACE, "CreatorTool", _converter_string))
##
# The common identifier for all versions and renditions of this resource.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
xmpmm_documentId = property(_getter_single(XMPMM_NAMESPACE, "DocumentID", _converter_string))
##
# An identifier for a specific incarnation of a document, updated each
# time a file is saved.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
xmpmm_instanceId = property(_getter_single(XMPMM_NAMESPACE, "InstanceID", _converter_string))
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
##
# Retrieves custom metadata properties defined in the undocumented pdfx
# metadata schema.
# <p>Stability: Added in v1.12, will exist for all future v1.x releases.
# @return Returns a dictionary of key/value items for custom metadata
# properties.
custom_properties = property(custom_properties)