Fix cover images.
[dorian] / model / book.cpp
1 #include <qtextdocument.h>  // Qt::escape is currently defined here...
2 #include <QtGui>
3
4 #include "book.h"
5 #include "opshandler.h"
6 #include "xmlerrorhandler.h"
7 #include "extractzip.h"
8 #include "library.h"
9 #include "containerhandler.h"
10 #include "ncxhandler.h"
11 #include "trace.h"
12 #include "bookdb.h"
13
14 const int COVER_WIDTH = 53;
15 const int COVER_HEIGHT = 59;
16 const int COVER_MAX = 512 * 1024;
17
18 Book::Book(const QString &p, QObject *parent): QObject(parent), loaded(false)
19 {
20     mPath = "";
21     if (p.size()) {
22         QFileInfo info(p);
23         mPath = info.absoluteFilePath();
24         title = info.baseName();
25         mTempFile.open();
26     }
27 }
28
29 Book::~Book()
30 {
31     close();
32 }
33
34 QString Book::path()
35 {
36     return mPath;
37 }
38
39 bool Book::open()
40 {
41     TRACE;
42     qDebug() << path();
43     close();
44     clear();
45     load();
46     if (path().isEmpty()) {
47         title = "No book";
48         return false;
49     }
50     if (!extract(QStringList())) {
51         return false;
52     }
53     if (!parse()) {
54         return false;
55     }
56     save();
57     emit opened(path());
58     return true;
59 }
60
61 void Book::peek()
62 {
63     TRACE;
64     qDebug() << path();
65     close();
66     clear();
67     load();
68     if (path().isEmpty()) {
69         title = "No book";
70         return;
71     }
72     if (!extractMetaData()) {
73         return;
74     }
75     if (!parse()) {
76         return;
77     }
78     save();
79     close();
80 }
81
82 void Book::close()
83 {
84     TRACE;
85     content.clear();
86     parts.clear();
87     QDir::setCurrent(QDir::rootPath());
88     clearDir(tmpDir());
89 }
90
91 QString Book::tmpDir() const
92 {
93     QString tmpName = QFileInfo(mTempFile.fileName()).fileName();
94     return QDir(QDir::temp().absoluteFilePath("dorian")).
95             absoluteFilePath(tmpName);
96 }
97
98 bool Book::extract(const QStringList &excludedExtensions)
99 {
100     TRACE;
101     bool ret = false;
102     QString tmp = tmpDir();
103     qDebug() << "Extracting" << mPath << "to" << tmp;
104
105     load();
106     QDir::setCurrent(QDir::rootPath());
107     if (!clearDir(tmp)) {
108         qCritical() << "Book::extract: Failed to remove" << tmp;
109         return false;
110     }
111     QDir d(tmp);
112     if (!d.exists()) {
113         if (!d.mkpath(tmp)) {
114             qCritical() << "Book::extract: Could not create" << tmp;
115             return false;
116         }
117     }
118
119     // If book comes from resource, copy it to the temporary directory first
120     QString bookPath = path();
121     if (bookPath.startsWith(":/books/")) {
122         QFile src(bookPath);
123         QString dst(QDir(tmp).absoluteFilePath("book.epub"));
124         if (!src.copy(dst)) {
125             qCritical() << "Book::extract: Failed to copy built-in book"
126                     << bookPath << "to" << dst;
127             return false;
128         }
129         bookPath = dst;
130     }
131
132     QString oldDir = QDir::currentPath();
133     if (!QDir::setCurrent(tmp)) {
134         qCritical() << "Book::extract: Could not change to" << tmp;
135         return false;
136     }
137     ret = extractZip(bookPath, excludedExtensions);
138     if (!ret) {
139         qCritical() << "Book::extract: Extracting ZIP failed";
140     }
141     QDir::setCurrent(oldDir);
142     return ret;
143 }
144
145 bool Book::parse()
146 {
147     TRACE;
148
149     load();
150
151     // Parse OPS file
152     bool ret = false;
153     QString opsFileName = opsPath();
154     qDebug() << "Parsing OPS file" << opsFileName;
155     QFile opsFile(opsFileName);
156     QXmlSimpleReader reader;
157     QXmlInputSource *source = new QXmlInputSource(&opsFile);
158     OpsHandler *opsHandler = new OpsHandler(*this);
159     XmlErrorHandler *errorHandler = new XmlErrorHandler();
160     reader.setContentHandler(opsHandler);
161     reader.setErrorHandler(errorHandler);
162     ret = reader.parse(source);
163     delete errorHandler;
164     delete opsHandler;
165     delete source;
166
167     // Initially, put all content items in the chapter list.
168     // This will be refined by parsing the NCX file later
169     chapters = parts;
170
171     // Load cover image
172     QString coverPath;
173     QStringList coverKeys;
174     coverKeys << "cover-image" << "img-cover-jpeg" << "cover";
175     foreach (QString key, coverKeys) {
176         if (content.contains(key)) {
177             coverPath = QDir(rootPath()).absoluteFilePath(content[key].href);
178             break;
179         }
180     }
181     if (coverPath.isEmpty()) {
182         // Last resort
183         QString coverJpeg = QDir(rootPath()).absoluteFilePath("cover.jpg");
184         if (QFileInfo(coverJpeg).exists()) {
185             coverPath = coverJpeg;
186         }
187     }
188     if (!coverPath.isEmpty()) {
189         qDebug() << "Loading cover image from" << coverPath;
190         cover = makeCover(coverPath);
191     }
192
193     // If there is an "ncx" item in content, parse it: That's the real table of
194     // contents
195     QString ncxFileName;
196     if (content.contains("ncx")) {
197         ncxFileName = content["ncx"].href;
198     } else if (content.contains("ncxtoc")) {
199         ncxFileName = content["ncxtoc"].href;
200     } else if (content.contains("toc")) {
201         ncxFileName = content["toc"].href;
202     } else {
203         qDebug() << "No NCX table of contents";
204     }
205     if (!ncxFileName.isEmpty()) {
206         qDebug() << "Parsing NCX file" << ncxFileName;
207         QFile ncxFile(QDir(rootPath()).absoluteFilePath(ncxFileName));
208         source = new QXmlInputSource(&ncxFile);
209         NcxHandler *ncxHandler = new NcxHandler(*this);
210         errorHandler = new XmlErrorHandler();
211         reader.setContentHandler(ncxHandler);
212         reader.setErrorHandler(errorHandler);
213         ret = reader.parse(source);
214         delete errorHandler;
215         delete ncxHandler;
216         delete source;
217     }
218
219     // Calculate book part sizes
220     size = 0;
221     foreach (QString part, parts) {
222         QFileInfo info(QDir(rootPath()).absoluteFilePath(content[part].href));
223         content[part].size = info.size();
224         size += content[part].size;
225     }
226
227     return ret;
228 }
229
230 bool Book::clearDir(const QString &dir)
231 {
232     QDir d(dir);
233     if (!d.exists()) {
234         return true;
235     }
236     QDirIterator i(dir, QDirIterator::Subdirectories);
237     while (i.hasNext()) {
238         QString entry = i.next();
239         if (entry.endsWith("/.") || entry.endsWith("/..")) {
240             continue;
241         }
242         QFileInfo info(entry);
243         if (info.isDir()) {
244             if (!clearDir(entry)) {
245                 return false;
246             }
247         }
248         else {
249             if (!QFile::remove(entry)) {
250                 qCritical() << "Book::clearDir: Could not remove" << entry;
251                 // FIXME: To be investigated: This is happening too often
252                 // return false;
253             }
254         }
255     }
256     (void)d.rmpath(dir);
257     return true;
258 }
259
260 void Book::clear()
261 {
262     close();
263     title = "";
264     creators.clear();
265     date = "";
266     publisher = "";
267     datePublished = "";
268     subject = "";
269     source = "";
270     rights = "";
271 }
272
273 void Book::load()
274 {
275     if (loaded) {
276         return;
277     }
278
279     TRACE;
280     loaded = true;
281     qDebug() << "path" << path();
282
283     QVariantHash data = BookDb::instance()->load(path());
284     title = data["title"].toString();
285     qDebug() << title;
286     creators = data["creators"].toStringList();
287     date = data["date"].toString();
288     publisher = data["publisher"].toString();
289     datePublished = data["datepublished"].toString();
290     subject = data["subject"].toString();
291     source = data["source"].toString();
292     rights = data["rights"].toString();
293     mLastBookmark.part = data["lastpart"].toInt();
294     mLastBookmark.pos = data["lastpos"].toReal();
295     cover = data["cover"].value<QImage>();
296     if (cover.isNull()) {
297         cover = makeCover(":/icons/book.png");
298     }
299     int size = data["bookmarks"].toInt();
300     for (int i = 0; i < size; i++) {
301         int part = data[QString("bookmark%1part").arg(i)].toInt();
302         qreal pos = data[QString("bookmark%1pos").arg(i)].toReal();
303         QString note = data[QString("bookmark%1note").arg(i)].toString();
304         mBookmarks.append(Bookmark(part, pos, note));
305     }
306 }
307
308 void Book::save()
309 {
310     TRACE;
311
312     load();
313     QVariantHash data;
314     data["title"] = title;
315     data["creators"] = creators;
316     data["date"] = date;
317     data["publisher"] = publisher;
318     data["datepublished"] = datePublished;
319     data["subject"] = subject;
320     data["source"] = source;
321     data["rights"] = rights;
322     data["lastpart"] = mLastBookmark.part;
323     data["lastpos"] = mLastBookmark.pos;
324     data["cover"] = cover;
325     data["bookmarks"] = mBookmarks.size();
326     for (int i = 0; i < mBookmarks.size(); i++) {
327         data[QString("bookmark%1part").arg(i)] = mBookmarks[i].part;
328         data[QString("bookmark%1pos").arg(i)] = mBookmarks[i].pos;
329         data[QString("bookmark%1note").arg(i)] = mBookmarks[i].note;
330     }
331     BookDb::instance()->save(path(), data);
332 }
333
334 void Book::setLastBookmark(int part, qreal position)
335 {
336     TRACE;
337     load();
338     mLastBookmark.part = part;
339     mLastBookmark.pos = position;
340     save();
341 }
342
343 Book::Bookmark Book::lastBookmark()
344 {
345     load();
346     return Book::Bookmark(mLastBookmark);
347 }
348
349 void Book::addBookmark(int part, qreal position, const QString &note)
350 {
351     load();
352     mBookmarks.append(Bookmark(part, position, note));
353     qSort(mBookmarks.begin(), mBookmarks.end());
354     save();
355 }
356
357 void Book::deleteBookmark(int index)
358 {
359     load();
360     mBookmarks.removeAt(index);
361     save();
362 }
363
364 QList<Book::Bookmark> Book::bookmarks()
365 {
366     load();
367     return mBookmarks;
368 }
369
370 QString Book::opsPath()
371 {
372     TRACE;
373     load();
374     QString ret;
375
376     QFile container(tmpDir() + "/META-INF/container.xml");
377     qDebug() << container.fileName();
378     QXmlSimpleReader reader;
379     QXmlInputSource *source = new QXmlInputSource(&container);
380     ContainerHandler *containerHandler = new ContainerHandler();
381     XmlErrorHandler *errorHandler = new XmlErrorHandler();
382     reader.setContentHandler(containerHandler);
383     reader.setErrorHandler(errorHandler);
384     if (reader.parse(source)) {
385         ret = tmpDir() + "/" + containerHandler->rootFile;
386         mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
387         qDebug() << "OSP path" << ret << "\nRoot dir" << mRootPath;
388     }
389     delete errorHandler;
390     delete containerHandler;
391     delete source;
392     return ret;
393 }
394
395 QString Book::rootPath()
396 {
397     load();
398     return mRootPath;
399 }
400
401 QString Book::name()
402 {
403     load();
404     if (title.size()) {
405         QString ret = title;
406         if (creators.length()) {
407             ret += "\nBy " + creators[0];
408             for (int i = 1; i < creators.length(); i++) {
409                 ret += ", " + creators[i];
410             }
411         }
412         return ret;
413     }
414     return path();
415 }
416
417 QString Book::shortName()
418 {
419     load();
420     return (title.isEmpty())? QFileInfo(path()).baseName(): title;
421 }
422
423 QImage Book::coverImage()
424 {
425     load();
426     return cover;
427 }
428
429 int Book::chapterFromPart(int index)
430 {
431     TRACE;
432     load();
433     int ret = -1;
434
435     QString partId = parts[index];
436     QString partHref = content[partId].href;
437
438     for (int i = 0; i < chapters.size(); i++) {
439         QString id = chapters[i];
440         QString href = content[id].href;
441         QString baseRef(href);
442         QUrl url(QString("file://") + href);
443         if (url.hasFragment()) {
444             QString fragment = url.fragment();
445             baseRef.chop(fragment.length() + 1);
446         }
447         if (baseRef == partHref) {
448             ret = i;
449             // Don't break, keep looking
450         }
451     }
452
453     return ret;
454 }
455
456 int Book::partFromChapter(int index, QString &fragment)
457 {
458     TRACE;
459     load();
460     fragment.clear();
461     QString id = chapters[index];
462     QString href = content[id].href;
463     int hashPos = href.indexOf("#");
464     if (hashPos != -1) {
465         fragment = href.mid(hashPos);
466         href = href.left(hashPos);
467     }
468
469     qDebug() << "Chapter" << index;
470     qDebug() << " id" << id;
471     qDebug() << " href" << href;
472     qDebug() << " fragment" << fragment;
473
474     for (int i = 0; i < parts.size(); i++) {
475         QString partId = parts[i];
476         if (content[partId].href == href) {
477             qDebug() << "Part index for" << href << "is" << i;
478             return i;
479         }
480     }
481
482     qWarning() << "Book::partFromChapter: Could not find part index for"
483             << href;
484     return -1;
485 }
486
487 qreal Book::getProgress(int part, qreal position)
488 {
489     load();
490     Q_ASSERT(part < parts.size());
491     QString key;
492     qreal partSize = 0;
493     for (int i = 0; i < part; i++) {
494         key = parts[i];
495         partSize += content[key].size;
496     }
497     key = parts[part];
498     partSize += content[key].size * position;
499     return partSize / (qreal)size;
500 }
501
502 bool Book::extractMetaData()
503 {
504     QStringList excludedExtensions;
505     excludedExtensions << ".html" << ".xhtml" << ".xht" << ".htm" << ".gif"
506             << ".css" << "*.ttf" << "mimetype";
507     return extract(excludedExtensions);
508 }
509
510 void Book::upgrade()
511 {
512     TRACE;
513
514     // Load book from old database (QSettings)
515     QSettings settings;
516     QString key = "book/" + path() + "/";
517     title = settings.value(key + "title").toString();
518     qDebug() << title;
519     creators = settings.value(key + "creators").toStringList();
520     date = settings.value(key + "date").toString();
521     publisher = settings.value(key + "publisher").toString();
522     datePublished = settings.value(key + "datepublished").toString();
523     subject = settings.value(key + "subject").toString();
524     source = settings.value(key + "source").toString();
525     rights = settings.value(key + "rights").toString();
526     mLastBookmark.part = settings.value(key + "lastpart").toInt();
527     mLastBookmark.pos = settings.value(key + "lastpos").toReal();
528     cover = settings.value(key + "cover").value<QImage>();
529     if (cover.isNull()) {
530         cover = makeCover(":/icons/book.png");
531     } else {
532         cover = makeCover(QPixmap::fromImage(cover));
533     }
534     int size = settings.value(key + "bookmarks").toInt();
535     for (int i = 0; i < size; i++) {
536         int part = settings.value(key + "bookmark" + QString::number(i) +
537                                      "/part").toInt();
538         qreal pos = settings.value(key + "bookmark" + QString::number(i) +
539                                    "/pos").toReal();
540         qDebug() << QString("Bookmark %1 at part %2, %3").
541                 arg(i).arg(part).arg(pos);
542         mBookmarks.append(Bookmark(part, pos));
543     }
544
545     // Remove QSettings
546     settings.remove("book/" + path());
547
548     // Save book to new database
549     save();
550 }
551
552 void Book::remove()
553 {
554     TRACE;
555     close();
556     BookDb::instance()->remove(path());
557 }
558
559 QImage Book::makeCover(const QString &fileName)
560 {
561     TRACE;
562     qDebug() << fileName;
563     QFileInfo info(fileName);
564     if (info.isReadable() && (info.size() < COVER_MAX)) {
565         return makeCover(QPixmap(fileName));
566     }
567     return makeCover(QPixmap(":/icons/book.png"));
568 }
569
570 QImage Book::makeCover(const QPixmap &pixmap)
571 {
572     TRACE;
573     QPixmap src = pixmap.scaled(COVER_WIDTH, COVER_HEIGHT,
574         Qt::KeepAspectRatio, Qt::SmoothTransformation);
575     QPixmap transparent(COVER_WIDTH, COVER_HEIGHT);
576     transparent.fill(Qt::transparent);
577
578     QPainter p;
579     p.begin(&transparent);
580     p.setCompositionMode(QPainter::CompositionMode_Source);
581     p.drawPixmap((COVER_WIDTH - src.width()) / 2,
582                  (COVER_HEIGHT - src.height()) / 2, src);
583     p.end();
584
585     return transparent.toImage();
586 }
587
588