Fix author name in library and book info dialogs.
[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.join(", ");
408         }
409         return ret;
410     }
411     return path();
412 }
413
414 QString Book::shortName()
415 {
416     load();
417     return (title.isEmpty())? QFileInfo(path()).baseName(): title;
418 }
419
420 QImage Book::coverImage()
421 {
422     load();
423     return cover;
424 }
425
426 int Book::chapterFromPart(int index)
427 {
428     TRACE;
429     load();
430     int ret = -1;
431
432     QString partId = parts[index];
433     QString partHref = content[partId].href;
434
435     for (int i = 0; i < chapters.size(); i++) {
436         QString id = chapters[i];
437         QString href = content[id].href;
438         QString baseRef(href);
439         QUrl url(QString("file://") + href);
440         if (url.hasFragment()) {
441             QString fragment = url.fragment();
442             baseRef.chop(fragment.length() + 1);
443         }
444         if (baseRef == partHref) {
445             ret = i;
446             // Don't break, keep looking
447         }
448     }
449
450     return ret;
451 }
452
453 int Book::partFromChapter(int index, QString &fragment)
454 {
455     TRACE;
456     load();
457     fragment.clear();
458     QString id = chapters[index];
459     QString href = content[id].href;
460     int hashPos = href.indexOf("#");
461     if (hashPos != -1) {
462         fragment = href.mid(hashPos);
463         href = href.left(hashPos);
464     }
465
466     qDebug() << "Chapter" << index;
467     qDebug() << " id" << id;
468     qDebug() << " href" << href;
469     qDebug() << " fragment" << fragment;
470
471     for (int i = 0; i < parts.size(); i++) {
472         QString partId = parts[i];
473         if (content[partId].href == href) {
474             qDebug() << "Part index for" << href << "is" << i;
475             return i;
476         }
477     }
478
479     qWarning() << "Book::partFromChapter: Could not find part index for"
480             << href;
481     return -1;
482 }
483
484 qreal Book::getProgress(int part, qreal position)
485 {
486     load();
487     Q_ASSERT(part < parts.size());
488     QString key;
489     qreal partSize = 0;
490     for (int i = 0; i < part; i++) {
491         key = parts[i];
492         partSize += content[key].size;
493     }
494     key = parts[part];
495     partSize += content[key].size * position;
496     return partSize / (qreal)size;
497 }
498
499 bool Book::extractMetaData()
500 {
501     QStringList excludedExtensions;
502     excludedExtensions << ".html" << ".xhtml" << ".xht" << ".htm" << ".gif"
503             << ".css" << "*.ttf" << "mimetype";
504     return extract(excludedExtensions);
505 }
506
507 void Book::upgrade()
508 {
509     TRACE;
510
511     // Load book from old database (QSettings)
512     QSettings settings;
513     QString key = "book/" + path() + "/";
514     title = settings.value(key + "title").toString();
515     qDebug() << title;
516     creators = settings.value(key + "creators").toStringList();
517     date = settings.value(key + "date").toString();
518     publisher = settings.value(key + "publisher").toString();
519     datePublished = settings.value(key + "datepublished").toString();
520     subject = settings.value(key + "subject").toString();
521     source = settings.value(key + "source").toString();
522     rights = settings.value(key + "rights").toString();
523     mLastBookmark.part = settings.value(key + "lastpart").toInt();
524     mLastBookmark.pos = settings.value(key + "lastpos").toReal();
525     cover = settings.value(key + "cover").value<QImage>();
526     if (cover.isNull()) {
527         cover = makeCover(":/icons/book.png");
528     } else {
529         cover = makeCover(QPixmap::fromImage(cover));
530     }
531     int size = settings.value(key + "bookmarks").toInt();
532     for (int i = 0; i < size; i++) {
533         int part = settings.value(key + "bookmark" + QString::number(i) +
534                                      "/part").toInt();
535         qreal pos = settings.value(key + "bookmark" + QString::number(i) +
536                                    "/pos").toReal();
537         qDebug() << QString("Bookmark %1 at part %2, %3").
538                 arg(i).arg(part).arg(pos);
539         mBookmarks.append(Bookmark(part, pos));
540     }
541
542     // Remove QSettings
543     settings.remove("book/" + path());
544
545     // Save book to new database
546     save();
547 }
548
549 void Book::remove()
550 {
551     TRACE;
552     close();
553     BookDb::instance()->remove(path());
554 }
555
556 QImage Book::makeCover(const QString &fileName)
557 {
558     TRACE;
559     qDebug() << fileName;
560     QFileInfo info(fileName);
561     if (info.isReadable() && (info.size() < COVER_MAX)) {
562         return makeCover(QPixmap(fileName));
563     }
564     return makeCover(QPixmap(":/icons/book.png"));
565 }
566
567 QImage Book::makeCover(const QPixmap &pixmap)
568 {
569     TRACE;
570     QPixmap src = pixmap.scaled(COVER_WIDTH, COVER_HEIGHT,
571         Qt::KeepAspectRatio, Qt::SmoothTransformation);
572     QPixmap transparent(COVER_WIDTH, COVER_HEIGHT);
573     transparent.fill(Qt::transparent);
574
575     QPainter p;
576     p.begin(&transparent);
577     p.setCompositionMode(QPainter::CompositionMode_Source);
578     p.drawPixmap((COVER_WIDTH - src.width()) / 2,
579                  (COVER_HEIGHT - src.height()) / 2, src);
580     p.end();
581
582     return transparent.toImage();
583 }
584
585