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