~ubuntu-branches/ubuntu/jaunty/calibre/jaunty-backports

« back to all changes in this revision

Viewing changes to src/calibre/ebooks/metadata/epub.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-01-20 17:14:02 UTC
  • Revision ID: james.westby@ubuntu.com-20090120171402-8y3znf6nokwqe80k
Tags: upstream-0.4.125+dfsg
ImportĀ upstreamĀ versionĀ 0.4.125+dfsg

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
from __future__ import with_statement
 
3
__license__   = 'GPL v3'
 
4
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
 
5
 
 
6
'''Read meta information from epub files'''
 
7
 
 
8
import sys, os, time
 
9
from cStringIO import StringIO
 
10
from contextlib import closing
 
11
 
 
12
from PyQt4.Qt import QUrl, QEventLoop, QSize, QByteArray, QBuffer, \
 
13
                     SIGNAL, QPainter, QImage, QObject, QApplication, Qt, QPalette
 
14
from PyQt4.QtWebKit import QWebPage
 
15
 
 
16
from calibre.utils.zipfile import ZipFile, BadZipfile, safe_replace
 
17
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
 
18
from calibre.ebooks.metadata import get_parser, MetaInformation
 
19
from calibre.ebooks.metadata.opf2 import OPF
 
20
from calibre.ptempfile import TemporaryDirectory
 
21
from calibre import CurrentDir, fit_image
 
22
 
 
23
class EPubException(Exception):
 
24
    pass
 
25
 
 
26
class OCFException(EPubException):
 
27
    pass
 
28
 
 
29
class ContainerException(OCFException):
 
30
    pass
 
31
 
 
32
class Container(dict):
 
33
    def __init__(self, stream=None):
 
34
        if not stream: return
 
35
        soup = BeautifulStoneSoup(stream.read())
 
36
        container = soup.find('container')
 
37
        if not container:
 
38
            raise OCFException("<container/> element missing")
 
39
        if container.get('version', None) != '1.0':
 
40
            raise EPubException("unsupported version of OCF")
 
41
        rootfiles = container.find('rootfiles')
 
42
        if not rootfiles:
 
43
            raise EPubException("<rootfiles/> element missing")
 
44
        for rootfile in rootfiles.findAll('rootfile'):
 
45
            try:
 
46
                self[rootfile['media-type']] = rootfile['full-path']
 
47
            except KeyError:
 
48
                raise EPubException("<rootfile/> element malformed")
 
49
 
 
50
class OCF(object):
 
51
    MIMETYPE        = 'application/epub+zip'
 
52
    CONTAINER_PATH  = 'META-INF/container.xml'
 
53
    ENCRYPTION_PATH = 'META-INF/encryption.xml'
 
54
    
 
55
    def __init__(self):
 
56
        raise NotImplementedError('Abstract base class')
 
57
 
 
58
 
 
59
class OCFReader(OCF):
 
60
    def __init__(self):
 
61
        try:
 
62
            mimetype = self.open('mimetype').read().rstrip()
 
63
            if mimetype != OCF.MIMETYPE:
 
64
                print 'WARNING: Invalid mimetype declaration', mimetype
 
65
        except:
 
66
            print 'WARNING: Epub doesn\'t contain a mimetype declaration'
 
67
 
 
68
        try:
 
69
            with closing(self.open(OCF.CONTAINER_PATH)) as f:
 
70
                self.container = Container(f)
 
71
        except KeyError:
 
72
            raise EPubException("missing OCF container.xml file")
 
73
        self.opf_path = self.container[OPF.MIMETYPE] 
 
74
        try:
 
75
            with closing(self.open(self.opf_path)) as f:
 
76
                self.opf = OPF(f, self.root)
 
77
        except KeyError:
 
78
            raise EPubException("missing OPF package file")
 
79
                
 
80
 
 
81
class OCFZipReader(OCFReader):
 
82
    def __init__(self, stream, mode='r', root=None):
 
83
        try:
 
84
            self.archive = ZipFile(stream, mode=mode)
 
85
        except BadZipfile:
 
86
            raise EPubException("not a ZIP .epub OCF container")
 
87
        self.root = root
 
88
        if self.root is None:
 
89
            self.root = os.getcwdu()
 
90
            if hasattr(stream, 'name'):
 
91
                self.root = os.path.abspath(os.path.dirname(stream.name))
 
92
        super(OCFZipReader, self).__init__()
 
93
 
 
94
    def open(self, name, mode='r'):
 
95
        return StringIO(self.archive.read(name))
 
96
    
 
97
class OCFDirReader(OCFReader):
 
98
    def __init__(self, path):
 
99
        self.root = path
 
100
        super(OCFDirReader, self).__init__()
 
101
        
 
102
    def open(self, path, *args, **kwargs):
 
103
        return open(os.path.join(self.root, path), *args, **kwargs)
 
104
 
 
105
class CoverRenderer(QObject):
 
106
    WIDTH  = 1280
 
107
    HEIGHT = 1024
 
108
    
 
109
    def __init__(self, url, size, loop):
 
110
        QObject.__init__(self)
 
111
        self.loop = loop
 
112
        self.page = QWebPage()
 
113
        pal = self.page.palette()
 
114
        pal.setBrush(QPalette.Background, Qt.white)
 
115
        self.page.setPalette(pal)
 
116
        self.page.setViewportSize(QSize(600, 800))
 
117
        self.page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
 
118
        self.page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
 
119
        QObject.connect(self.page, SIGNAL('loadFinished(bool)'), self.render_html)
 
120
        self.image_data = None
 
121
        self.rendered = False
 
122
        self.page.mainFrame().load(url)
 
123
        
 
124
    def render_html(self, ok):
 
125
        self.rendered = True
 
126
        try:
 
127
            if not ok:
 
128
                return
 
129
            size = self.page.mainFrame().contentsSize()
 
130
            width, height = fit_image(size.width(), size.height(), self.WIDTH, self.HEIGHT)[1:]
 
131
            self.page.setViewportSize(QSize(width, height))
 
132
            image = QImage(self.page.viewportSize(), QImage.Format_ARGB32)
 
133
            image.setDotsPerMeterX(96*(100/2.54))
 
134
            image.setDotsPerMeterY(96*(100/2.54))
 
135
            painter = QPainter(image)
 
136
            self.page.mainFrame().render(painter)
 
137
            painter.end()
 
138
            
 
139
            ba = QByteArray()
 
140
            buf = QBuffer(ba)
 
141
            buf.open(QBuffer.WriteOnly)
 
142
            image.save(buf, 'JPEG')
 
143
            self.image_data = str(ba.data())
 
144
        finally:
 
145
            self.loop.exit(0)
 
146
        
 
147
 
 
148
def get_cover(opf, opf_path, stream):
 
149
    spine = list(opf.spine_items())
 
150
    if not spine:
 
151
        return
 
152
    cpage = spine[0]
 
153
    with TemporaryDirectory('_epub_meta') as tdir:
 
154
        with CurrentDir(tdir):
 
155
            stream.seek(0)
 
156
            ZipFile(stream).extractall()
 
157
            opf_path = opf_path.replace('/', os.sep)
 
158
            cpage = os.path.join(tdir, os.path.dirname(opf_path), *cpage.split('/'))
 
159
            if not os.path.exists(cpage):
 
160
                return
 
161
            if QApplication.instance() is None:
 
162
                QApplication([])
 
163
            url = QUrl.fromLocalFile(cpage)
 
164
            loop = QEventLoop()
 
165
            cr = CoverRenderer(url, os.stat(cpage).st_size, loop)
 
166
            loop.exec_()
 
167
            count = 0
 
168
            while count < 50 and not cr.rendered:
 
169
                time.sleep(0.1)
 
170
                count += 1
 
171
    return cr.image_data 
 
172
    
 
173
def get_metadata(stream, extract_cover=True):
 
174
    """ Return metadata as a :class:`MetaInformation` object """
 
175
    stream.seek(0)
 
176
    reader = OCFZipReader(stream)
 
177
    mi = MetaInformation(reader.opf)
 
178
    if extract_cover:
 
179
        try:
 
180
            cdata = get_cover(reader.opf, reader.opf_path, stream)
 
181
            if cdata is not None:
 
182
                mi.cover_data = ('jpg', cdata)
 
183
        except:
 
184
            import traceback
 
185
            traceback.print_exc()
 
186
    return mi
 
187
 
 
188
def set_metadata(stream, mi):
 
189
    stream.seek(0)
 
190
    reader = OCFZipReader(stream, root=os.getcwdu())
 
191
    reader.opf.smart_update(mi)
 
192
    newopf = StringIO(reader.opf.render())
 
193
    safe_replace(stream, reader.container[OPF.MIMETYPE], newopf)
 
194
    
 
195
def option_parser():
 
196
    parser = get_parser('epub')
 
197
    parser.remove_option('--category')
 
198
    parser.add_option('--tags', default=None, 
 
199
                      help=_('A comma separated list of tags to set'))
 
200
    parser.add_option('--series', default=None,
 
201
                      help=_('The series to which this book belongs'))
 
202
    parser.add_option('--series-index', default=None,
 
203
                      help=_('The series index'))
 
204
    parser.add_option('--language', default=None,
 
205
                      help=_('The book language'))
 
206
    parser.add_option('--get-cover', default=False, action='store_true',
 
207
                      help=_('Extract the cover'))
 
208
    return parser
 
209
 
 
210
def main(args=sys.argv):
 
211
    parser = option_parser()
 
212
    opts, args = parser.parse_args(args)
 
213
    if len(args) != 2:
 
214
        parser.print_help()
 
215
        return 1
 
216
    with open(args[1], 'r+b') as stream:
 
217
        mi = get_metadata(stream, extract_cover=opts.get_cover)
 
218
        changed = False
 
219
        if opts.title:
 
220
            mi.title = opts.title
 
221
            changed = True
 
222
        if opts.authors:
 
223
            mi.authors = opts.authors.split(',')
 
224
            changed = True
 
225
        if opts.tags:
 
226
            mi.tags = opts.tags.split(',')
 
227
            changed = True
 
228
        if opts.comment:
 
229
            mi.comments = opts.comment
 
230
            changed = True
 
231
        if opts.series:
 
232
            mi.series = opts.series
 
233
            changed = True
 
234
        if opts.series_index:
 
235
            mi.series_index = opts.series_index
 
236
            changed = True
 
237
        if opts.language is not None:
 
238
            mi.language = opts.language
 
239
            changed = True
 
240
        
 
241
        if changed:
 
242
            set_metadata(stream, mi)
 
243
        print unicode(get_metadata(stream, extract_cover=False)).encode('utf-8')
 
244
        
 
245
    if mi.cover_data[1] is not None:
 
246
        cpath = os.path.splitext(os.path.basename(args[1]))[0] + '_cover.jpg'
 
247
        with open(cpath, 'wb') as f:
 
248
            f.write(mi.cover_data[1])
 
249
            print 'Cover saved to', f.name
 
250
    
 
251
    return 0
 
252
 
 
253
if __name__ == '__main__':
 
254
    sys.exit(main())