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