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