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