1 #include <qtextdocument.h> // Qt::escape is currently defined here...
5 #include "opshandler.h"
6 #include "xmlerrorhandler.h"
7 #include "extractzip.h"
9 #include "containerhandler.h"
10 #include "ncxhandler.h"
14 const int COVER_WIDTH = 53;
15 const int COVER_HEIGHT = 59;
16 const int COVER_MAX = 512 * 1024;
18 Book::Book(const QString &p, QObject *parent): QObject(parent), loaded(false)
23 mPath = info.absoluteFilePath();
24 title = info.baseName();
46 if (path().isEmpty()) {
50 if (!extract(QStringList())) {
56 dateOpened = QDateTime::currentDateTime().toUTC();
69 if (path().isEmpty()) {
73 if (!extractMetaData()) {
85 Trace t("Book::close");
88 QDir::setCurrent(QDir::rootPath());
92 QString Book::tmpDir() const
94 QString tmpName = QFileInfo(mTempFile.fileName()).fileName();
95 return QDir(QDir::temp().absoluteFilePath("dorian")).
96 absoluteFilePath(tmpName);
99 bool Book::extract(const QStringList &excludedExtensions)
103 QString tmp = tmpDir();
104 qDebug() << "Extracting" << mPath << "to" << tmp;
107 QDir::setCurrent(QDir::rootPath());
108 if (!clearDir(tmp)) {
109 qCritical() << "Book::extract: Failed to remove" << tmp;
114 if (!d.mkpath(tmp)) {
115 qCritical() << "Book::extract: Could not create" << tmp;
120 // If book comes from resource, copy it to the temporary directory first
121 QString bookPath = path();
122 if (bookPath.startsWith(":/books/")) {
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;
133 QString oldDir = QDir::currentPath();
134 if (!QDir::setCurrent(tmp)) {
135 qCritical() << "Book::extract: Could not change to" << tmp;
138 ret = extractZip(bookPath, excludedExtensions);
140 qCritical() << "Book::extract: Extracting ZIP failed";
142 QDir::setCurrent(oldDir);
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);
168 // Initially, put all content items in the chapter list.
169 // This will be refined by parsing the NCX file later
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);
182 if (coverPath.isEmpty()) {
184 QString coverJpeg = QDir(rootPath()).absoluteFilePath("cover.jpg");
185 if (QFileInfo(coverJpeg).exists()) {
186 coverPath = coverJpeg;
189 if (!coverPath.isEmpty()) {
190 qDebug() << "Loading cover image from" << coverPath;
191 cover = makeCover(coverPath);
194 // If there is an "ncx" item in content, parse it: That's the real table
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;
204 qDebug() << "No NCX table of contents";
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);
220 // Calculate book part sizes
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;
231 bool Book::clearDir(const QString &dir)
237 QDirIterator i(dir, QDirIterator::Subdirectories);
238 while (i.hasNext()) {
239 QString entry = i.next();
240 if (entry.endsWith("/.") || entry.endsWith("/..")) {
243 QFileInfo info(entry);
245 if (!clearDir(entry)) {
250 if (!QFile::remove(entry)) {
251 qCritical() << "Book::clearDir: Could not remove" << entry;
252 // FIXME: To be investigated: This is happening too often
282 qDebug() << "path" << path();
284 QVariantHash data = BookDb::instance()->load(path());
285 title = data["title"].toString();
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");
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));
307 dateAdded = data["dateadded"].toDateTime();
308 dateOpened = data["dateopened"].toDateTime();
317 data["title"] = title;
318 data["creators"] = creators;
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;
334 data["dateadded"] = dateAdded;
335 data["dateopened"] = dateOpened;
336 BookDb::instance()->save(path(), data);
339 void Book::setLastBookmark(int part, qreal position, bool fast)
342 qDebug() << "Part" << part << "position" << position << "fast?" << fast;
346 mLastBookmark.part = part;
347 mLastBookmark.pos = position;
353 Book::Bookmark Book::lastBookmark()
356 return Book::Bookmark(mLastBookmark);
359 void Book::addBookmark(int part, qreal position, const QString ¬e)
362 mBookmarks.append(Bookmark(part, position, note));
363 qSort(mBookmarks.begin(), mBookmarks.end());
367 void Book::setBookmarkNote(int index, const QString ¬e)
370 if (index >= 0 && index < mBookmarks.length()) {
371 mBookmarks[index].note = note;
377 void Book::deleteBookmark(int index)
380 mBookmarks.removeAt(index);
384 QList<Book::Bookmark> Book::bookmarks()
390 QString Book::opsPath()
396 QFile container(tmpDir() + "/META-INF/container.xml");
397 qDebug() << container.fileName();
398 QXmlSimpleReader reader;
399 QXmlInputSource *source = new QXmlInputSource(&container);
400 ContainerHandler *containerHandler = new ContainerHandler();
401 XmlErrorHandler *errorHandler = new XmlErrorHandler();
402 reader.setContentHandler(containerHandler);
403 reader.setErrorHandler(errorHandler);
404 if (reader.parse(source)) {
405 ret = tmpDir() + "/" + containerHandler->rootFile;
406 mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
407 qDebug() << "OSP path" << ret << "\nRoot dir" << mRootPath;
410 delete containerHandler;
415 QString Book::rootPath()
426 if (creators.length()) {
427 ret += "\nBy " + creators.join(", ");
434 QString Book::shortName()
437 return (title.isEmpty())? QFileInfo(path()).baseName(): title;
440 QImage Book::coverImage()
446 int Book::chapterFromPart(int index)
452 QString partId = parts[index];
453 QString partHref = content[partId].href;
455 for (int i = 0; i < chapters.size(); i++) {
456 QString id = chapters[i];
457 QString href = content[id].href;
458 int hashPos = href.indexOf("#");
460 href = href.left(hashPos);
462 if (href == partHref) {
464 // Don't break, keep looking
468 qDebug() << "Part" << index << partId << partHref << ":" << ret;
472 int Book::partFromChapter(int index, QString &fragment)
477 QString id = chapters[index];
478 QString href = content[id].href;
479 int hashPos = href.indexOf("#");
481 fragment = href.mid(hashPos);
482 href = href.left(hashPos);
485 qDebug() << "Chapter" << index;
486 qDebug() << " id" << id;
487 qDebug() << " href" << href;
488 qDebug() << " fragment" << fragment;
490 for (int i = 0; i < parts.size(); i++) {
491 QString partId = parts[i];
492 if (content[partId].href == href) {
493 qDebug() << "Part index for" << href << "is" << i;
498 qWarning() << "Book::partFromChapter: Could not find part index for"
503 qreal Book::getProgress(int part, qreal position)
506 Q_ASSERT(part < parts.size());
509 for (int i = 0; i < part; i++) {
511 partSize += content[key].size;
514 partSize += content[key].size * position;
515 return partSize / (qreal)size;
518 bool Book::extractMetaData()
520 QStringList excludedExtensions;
521 excludedExtensions << ".html" << ".xhtml" << ".xht" << ".htm" << ".gif"
522 << ".css" << "*.ttf" << "mimetype";
523 return extract(excludedExtensions);
530 // Load book from old database (QSettings)
532 QString key = "book/" + path() + "/";
533 title = settings.value(key + "title").toString();
535 creators = settings.value(key + "creators").toStringList();
536 date = settings.value(key + "date").toString();
537 publisher = settings.value(key + "publisher").toString();
538 datePublished = settings.value(key + "datepublished").toString();
539 subject = settings.value(key + "subject").toString();
540 source = settings.value(key + "source").toString();
541 rights = settings.value(key + "rights").toString();
542 mLastBookmark.part = settings.value(key + "lastpart").toInt();
543 mLastBookmark.pos = settings.value(key + "lastpos").toReal();
544 cover = settings.value(key + "cover").value<QImage>();
545 if (cover.isNull()) {
546 cover = makeCover(":/icons/book.png");
548 cover = makeCover(QPixmap::fromImage(cover));
550 int size = settings.value(key + "bookmarks").toInt();
551 for (int i = 0; i < size; i++) {
552 int part = settings.value(key + "bookmark" + QString::number(i) +
554 qreal pos = settings.value(key + "bookmark" + QString::number(i) +
556 qDebug() << QString("Bookmark %1 at part %2, %3").
557 arg(i).arg(part).arg(pos);
558 mBookmarks.append(Bookmark(part, pos));
562 settings.remove("book/" + path());
564 // Save book to new database
572 BookDb::instance()->remove(path());
575 QImage Book::makeCover(const QString &fileName)
578 qDebug() << fileName;
579 QFileInfo info(fileName);
580 if (info.isReadable() && (info.size() < COVER_MAX)) {
581 return makeCover(QPixmap(fileName));
583 return makeCover(QPixmap(":/icons/book.png"));
586 QImage Book::makeCover(const QPixmap &pixmap)
589 QPixmap src = pixmap.scaled(COVER_WIDTH, COVER_HEIGHT,
590 Qt::KeepAspectRatio, Qt::SmoothTransformation);
591 QPixmap transparent(COVER_WIDTH, COVER_HEIGHT);
592 transparent.fill(Qt::transparent);
595 p.begin(&transparent);
596 p.setCompositionMode(QPainter::CompositionMode_Source);
597 p.drawPixmap((COVER_WIDTH - src.width()) / 2,
598 (COVER_HEIGHT - src.height()) / 2, src);
601 return transparent.toImage();