94bb9e98be8fb89553daa14fe65fda46de878511
[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)
340 {
341     TRACE;
342     load();
343     mLastBookmark.part = part;
344     mLastBookmark.pos = position;
345     save();
346 }
347
348 Book::Bookmark Book::lastBookmark()
349 {
350     load();
351     return Book::Bookmark(mLastBookmark);
352 }
353
354 void Book::addBookmark(int part, qreal position, const QString &note)
355 {
356     load();
357     mBookmarks.append(Bookmark(part, position, note));
358     qSort(mBookmarks.begin(), mBookmarks.end());
359     save();
360 }
361
362 void Book::setBookmarkNote(int index, const QString &note)
363 {
364     load();
365     if (index >= 0 && index < mBookmarks.length()) {
366         mBookmarks[index].note = note;
367     }
368     save();
369
370 }
371
372 void Book::deleteBookmark(int index)
373 {
374     load();
375     mBookmarks.removeAt(index);
376     save();
377 }
378
379 QList<Book::Bookmark> Book::bookmarks()
380 {
381     load();
382     return mBookmarks;
383 }
384
385 QString Book::opsPath()
386 {
387     TRACE;
388     load();
389     QString ret;
390
391     QFile container(tmpDir() + "/META-INF/container.xml");
392     qDebug() << container.fileName();
393     QXmlSimpleReader reader;
394     QXmlInputSource *source = new QXmlInputSource(&container);
395     ContainerHandler *containerHandler = new ContainerHandler();
396     XmlErrorHandler *errorHandler = new XmlErrorHandler();
397     reader.setContentHandler(containerHandler);
398     reader.setErrorHandler(errorHandler);
399     if (reader.parse(source)) {
400         ret = tmpDir() + "/" + containerHandler->rootFile;
401         mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
402         qDebug() << "OSP path" << ret << "\nRoot dir" << mRootPath;
403     }
404     delete errorHandler;
405     delete containerHandler;
406     delete source;
407     return ret;
408 }
409
410 QString Book::rootPath()
411 {
412     load();
413     return mRootPath;
414 }
415
416 QString Book::name()
417 {
418     load();
419     if (title.size()) {
420         QString ret = title;
421         if (creators.length()) {
422             ret += "\nBy " + creators.join(", ");
423         }
424         return ret;
425     }
426     return path();
427 }
428
429 QString Book::shortName()
430 {
431     load();
432     return (title.isEmpty())? QFileInfo(path()).baseName(): title;
433 }
434
435 QImage Book::coverImage()
436 {
437     load();
438     return cover;
439 }
440
441 int Book::chapterFromPart(int index)
442 {
443     TRACE;
444     load();
445     int ret = -1;
446
447     QString partId = parts[index];
448     QString partHref = content[partId].href;
449
450     for (int i = 0; i < chapters.size(); i++) {
451         QString id = chapters[i];
452         QString href = content[id].href;
453         int hashPos = href.indexOf("#");
454         if (hashPos != -1) {
455             href = href.left(hashPos);
456         }
457         if (href == partHref) {
458             ret = i;
459             // Don't break, keep looking
460         }
461     }
462
463     qDebug() << "Part" << index << partId << partHref << ":" << ret;
464     return ret;
465 }
466
467 int Book::partFromChapter(int index, QString &fragment)
468 {
469     TRACE;
470     load();
471     fragment.clear();
472     QString id = chapters[index];
473     QString href = content[id].href;
474     int hashPos = href.indexOf("#");
475     if (hashPos != -1) {
476         fragment = href.mid(hashPos);
477         href = href.left(hashPos);
478     }
479
480     qDebug() << "Chapter" << index;
481     qDebug() << " id" << id;
482     qDebug() << " href" << href;
483     qDebug() << " fragment" << fragment;
484
485     for (int i = 0; i < parts.size(); i++) {
486         QString partId = parts[i];
487         if (content[partId].href == href) {
488             qDebug() << "Part index for" << href << "is" << i;
489             return i;
490         }
491     }
492
493     qWarning() << "Book::partFromChapter: Could not find part index for"
494             << href;
495     return -1;
496 }
497
498 qreal Book::getProgress(int part, qreal position)
499 {
500     load();
501     Q_ASSERT(part < parts.size());
502     QString key;
503     qreal partSize = 0;
504     for (int i = 0; i < part; i++) {
505         key = parts[i];
506         partSize += content[key].size;
507     }
508     key = parts[part];
509     partSize += content[key].size * position;
510     return partSize / (qreal)size;
511 }
512
513 bool Book::extractMetaData()
514 {
515     QStringList excludedExtensions;
516     excludedExtensions << ".html" << ".xhtml" << ".xht" << ".htm" << ".gif"
517             << ".css" << "*.ttf" << "mimetype";
518     return extract(excludedExtensions);
519 }
520
521 void Book::upgrade()
522 {
523     TRACE;
524
525     // Load book from old database (QSettings)
526     QSettings settings;
527     QString key = "book/" + path() + "/";
528     title = settings.value(key + "title").toString();
529     qDebug() << title;
530     creators = settings.value(key + "creators").toStringList();
531     date = settings.value(key + "date").toString();
532     publisher = settings.value(key + "publisher").toString();
533     datePublished = settings.value(key + "datepublished").toString();
534     subject = settings.value(key + "subject").toString();
535     source = settings.value(key + "source").toString();
536     rights = settings.value(key + "rights").toString();
537     mLastBookmark.part = settings.value(key + "lastpart").toInt();
538     mLastBookmark.pos = settings.value(key + "lastpos").toReal();
539     cover = settings.value(key + "cover").value<QImage>();
540     if (cover.isNull()) {
541         cover = makeCover(":/icons/book.png");
542     } else {
543         cover = makeCover(QPixmap::fromImage(cover));
544     }
545     int size = settings.value(key + "bookmarks").toInt();
546     for (int i = 0; i < size; i++) {
547         int part = settings.value(key + "bookmark" + QString::number(i) +
548                                      "/part").toInt();
549         qreal pos = settings.value(key + "bookmark" + QString::number(i) +
550                                    "/pos").toReal();
551         qDebug() << QString("Bookmark %1 at part %2, %3").
552                 arg(i).arg(part).arg(pos);
553         mBookmarks.append(Bookmark(part, pos));
554     }
555
556     // Remove QSettings
557     settings.remove("book/" + path());
558
559     // Save book to new database
560     save();
561 }
562
563 void Book::remove()
564 {
565     TRACE;
566     close();
567     BookDb::instance()->remove(path());
568 }
569
570 QImage Book::makeCover(const QString &fileName)
571 {
572     TRACE;
573     qDebug() << fileName;
574     QFileInfo info(fileName);
575     if (info.isReadable() && (info.size() < COVER_MAX)) {
576         return makeCover(QPixmap(fileName));
577     }
578     return makeCover(QPixmap(":/icons/book.png"));
579 }
580
581 QImage Book::makeCover(const QPixmap &pixmap)
582 {
583     TRACE;
584     QPixmap src = pixmap.scaled(COVER_WIDTH, COVER_HEIGHT,
585         Qt::KeepAspectRatio, Qt::SmoothTransformation);
586     QPixmap transparent(COVER_WIDTH, COVER_HEIGHT);
587     transparent.fill(Qt::transparent);
588
589     QPainter p;
590     p.begin(&transparent);
591     p.setCompositionMode(QPainter::CompositionMode_Source);
592     p.drawPixmap((COVER_WIDTH - src.width()) / 2,
593                  (COVER_HEIGHT - src.height()) / 2, src);
594     p.end();
595
596     return transparent.toImage();
597 }
598
599