/* * Copyright (C) 2015-2022 Jolla Ltd. * Copyright (C) 2015-2022 Slava Monich * * You may use this file under the terms of the BSD license as follows: * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * 3. Neither the names of the copyright holders nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "BooksBook.h" #include "BooksDefs.h" #include "BooksTextView.h" #include "BooksTextStyle.h" #include "BooksPaintContext.h" #include "BooksSettings.h" #include "BooksUtil.h" #include "HarbourJson.h" #include "HarbourDebug.h" #include "HarbourTask.h" #include "ZLImage.h" #include "image/ZLQtImageManager.h" #include "bookmodel/BookModel.h" #include "library/Author.h" #include #include #include #include #include #include #include #include #define BOOK_STATE_PAGE_STACK_INDEX "pageStackIndex" #define BOOK_STATE_FONT_SIZE_ADJUST "fontSizeAdjust" #define BOOK_STATE_POSITION "position" #define BOOK_COVER_SUFFIX ".cover." // ========================================================================== // BooksBook::CoverContext // ========================================================================== class BooksBook::CoverPaintContext : public BooksPaintContext { public: CoverPaintContext(); void drawImage(int, int, const ZLImageData&) Q_DECL_OVERRIDE; void drawImage(int, int, const ZLImageData&, int, int, ScalingType) Q_DECL_OVERRIDE; void handleImage(const ZLImageData&); bool gotIt() const; public: static QSize gMaxScreenSize; static bool gMaxScreenSizeKnown; QImage iImage; }; QSize BooksBook::CoverPaintContext::gMaxScreenSize(480,640); bool BooksBook::CoverPaintContext::gMaxScreenSizeKnown = false; BooksBook::CoverPaintContext::CoverPaintContext() { if (!gMaxScreenSizeKnown) { QList screens = qGuiApp->screens(); const int n = screens.count(); for (int i=0; isize(); gMaxScreenSize.setWidth(qMax(s.width(), gMaxScreenSize.width())); gMaxScreenSize.setHeight(qMax(s.width(), gMaxScreenSize.height())); } } iWidth = gMaxScreenSize.width(); iHeight = gMaxScreenSize.height(); } void BooksBook::CoverPaintContext::drawImage(int x, int y, const ZLImageData& image) { handleImage(image); } void BooksBook::CoverPaintContext::drawImage(int x, int y, const ZLImageData& image, int width, int height, ScalingType type) { handleImage(image); } void BooksBook::CoverPaintContext::handleImage(const ZLImageData& image) { const QImage* qImage = ((ZLQtImageData&)image).image(); HDEBUG(image.width() << 'x' << image.height()); if (qImage->height() > qImage->width() && qImage->width() > iImage.width() && qImage->height() > iImage.height()) { iImage = *qImage; } } bool BooksBook::CoverPaintContext::gotIt() const { return iImage.width() >= 50 && iImage.height() >= 80 && iImage.height() > iImage.width(); } // ========================================================================== // BooksBook::CoverTask // ========================================================================== class BooksBook::CoverTask : public HarbourTask { public: CoverTask(QThreadPool* aPool, QString aStateDir, shared_ptr aBook, QString aImagePath) : HarbourTask(aPool), iStateDir(aStateDir), iBook(aBook), iImagePath(aImagePath), iCoverMissing(false) {} bool hasImage() const; public: QString iStateDir; shared_ptr iBook; QString iImagePath; QImage iCoverImage; bool iCoverMissing; }; inline bool BooksBook::CoverTask::hasImage() const { return iCoverImage.width() > 0 && iCoverImage.height() > 0; } // ========================================================================== // BooksBook::LoadCoverTask // ========================================================================== class BooksBook::LoadCoverTask : public BooksBook::CoverTask { public: LoadCoverTask(QThreadPool* aPool, QString aStateDir, shared_ptr aBook, shared_ptr aFormatPlugin, QString aImagePath) : BooksBook::CoverTask(aPool, aStateDir, aBook, aImagePath), iFormatPlugin(aFormatPlugin) {} virtual void performTask(); public: shared_ptr iFormatPlugin; }; void BooksBook::LoadCoverTask::performTask() { if (!isCanceled()) { // Try to load cached (or custom) cover if (!iStateDir.isEmpty()) { QString coverPrefix(QString::fromStdString( iBook->file().name(false)) + BOOK_COVER_SUFFIX); QDirIterator it(iStateDir); while (it.hasNext()) { QString path(it.next()); if (it.fileName().startsWith(coverPrefix)) { if (QFile(path).size() == 0) { HDEBUG("no cover for" << iBook->title().c_str()); iCoverMissing = true; return; } else if (iCoverImage.load(path)) { HDEBUG("loaded cover" << iCoverImage.width() << 'x' << iCoverImage.height() << "for" << iBook->title().c_str() << "from" << qPrintable(path)); return; } } } } // OK, fetch one from the book file shared_ptr imageData; shared_ptr image = iFormatPlugin->coverImage(iBook->file()); if (!image.isNull()) { imageData = ZLImageManager::Instance().imageData(*image); } if (!imageData.isNull()) { QImage* qImage = (QImage*)((ZLQtImageData&)*imageData).image(); if (qImage) iCoverImage = *qImage; } } #if HARBOUR_DEBUG if (hasImage()) { HDEBUG("loaded cover" << iCoverImage.width() << 'x' << iCoverImage.height() << "for" << iBook->title().c_str()); } else if (isCanceled()) { HDEBUG("cancelled" << iBook->title().c_str()); } else { HDEBUG("no cover found in" << iBook->title().c_str()); } #endif } // ========================================================================== // BooksBook::GuessCoverTask // ========================================================================== class BooksBook::GuessCoverTask : public BooksBook::CoverTask { public: GuessCoverTask(QThreadPool* aPool, QString aStateDir, shared_ptr aBook, QString aImagePath) : BooksBook::CoverTask(aPool, aStateDir, aBook, aImagePath) {} virtual void performTask(); }; void BooksBook::GuessCoverTask::performTask() { if (!isCanceled()) { BooksMargins margins; CoverPaintContext context; BookModel bookModel(iBook); shared_ptr model(bookModel.bookTextModel()); BooksTextView view(context, BooksTextStyle::defaults(), margins); view.setModel(model); view.rewind(); if (!isCanceled()) { view.rewind(); if (!isCanceled()) { view.paint(); iCoverImage = context.iImage; } } } if (hasImage()) { HDEBUG("using image" << iCoverImage.width() << 'x' << iCoverImage.width() << "as cover for" << iBook->title().c_str()); // Save the extracted image if (!iImagePath.isEmpty()) { QFileInfo file(iImagePath); QDir dir(file.dir()); if (!dir.mkpath(dir.absolutePath())) { HWARN("failed to create" << qPrintable(dir.absolutePath())); } if (iCoverImage.save(iImagePath)) { HDEBUG("saved cover to" << qPrintable(iImagePath)); } else { HWARN("failed to save" << qPrintable(iImagePath)); } } } else if (isCanceled()) { HDEBUG("cancelled" << iBook->title().c_str()); } else { HDEBUG("no cover for" << iBook->title().c_str()); iCoverMissing = true; // Create empty file. Guessing the cover image is an expensive task, // we don't want to do it every time the application is started. if (!iImagePath.isEmpty() && // Check if the book file still exists - the failure could've // been caused by the SD-card removal. QFile::exists(QString::fromStdString(iBook->file().path()))) { QFile(iImagePath).open(QIODevice::WriteOnly); } } } // ========================================================================== // BooksBook::HashTask // ========================================================================== class BooksBook::HashTask : public HarbourTask { public: HashTask(QThreadPool* aPool, QThread* aTargetThread, QString aPath); virtual void performTask(); public: QString iPath; QByteArray iHash; }; BooksBook::HashTask::HashTask(QThreadPool* aPool, QThread* aTargetThread, QString aPath) : HarbourTask(aPool, aTargetThread), iPath(aPath) { } void BooksBook::HashTask::performTask() { iHash = BooksUtil::computeFileHashAndSetAttr(iPath, this); } // ========================================================================== // BooksBook // ========================================================================== // This constructor isn't really used, but is required by qmlRegisterType BooksBook::BooksBook(QObject* aParent) : QObject(aParent), iRef(-1) { init(); } BooksBook::BooksBook(const BooksStorage& aStorage, QString aRelativePath, shared_ptr aBook) : QObject(Q_NULLPTR), iRef(1), iStorage(aStorage), iBook(aBook), iFormatPlugin(PluginCollection::Instance().plugin(*iBook)), iTaskQueue(BooksTaskQueue::defaultQueue()), iTitle(QString::fromStdString(iBook->title())), iPath(QString::fromStdString(iBook->file().physicalFilePath())), iFileName(QFileInfo(iPath).fileName()), iHash(BooksUtil::fileHashAttr(iPath)) { init(); AuthorList authors(iBook->authors()); const int n = authors.size(); for (int i=0; i 0) iAuthors += ", "; iAuthors += QString::fromStdString(authors[i]->name()); } if (iStorage.isValid()) { iStateDir = QDir::cleanPath(iStorage.configDir().path() + QDir::separator() + aRelativePath); iStateFileBase = QDir::cleanPath(iStateDir + QDir::separator() + iFileName); iStateFilePath = storageFile(BOOKS_STATE_FILE_SUFFIX); // Load the state QVariantMap state; if (HarbourJson::load(iStateFilePath, state)) { iFontSizeAdjust = state.value(BOOK_STATE_FONT_SIZE_ADJUST).toInt(); #ifdef BOOK_STATE_PAGE_STACK_INDEX iPageStackPos = state.value(BOOK_STATE_PAGE_STACK_INDEX).toInt(); #endif // Current position can be stored in two formats - a single // position (older format) or a list of position (newer one). // We have to detect which one we are dealing with QVariant position(state.value(BOOK_STATE_POSITION)); BooksPos bookPos(BooksPos::fromVariant(position)); if (bookPos.valid()) { // Old format (single position) iPageStack.append(bookPos); } else { // New format (list of positions) iPageStack = BooksPos::List::fromVariant(position); } } } // Validate the state if (iPageStack.isEmpty()) { iPageStack.append(BooksPos(0,0,0)); } if (iPageStackPos < 0) { iPageStackPos = 0; } else if (iPageStackPos >= iPageStack.count()) { iPageStackPos = iPageStack.count() - 1; } if (iHash.isEmpty()) { HDEBUG("need to calculate hash for" << qPrintable(iPath)); iHashTaskQueue = BooksTaskQueue::hashQueue(); iHashTask = new HashTask(iHashTaskQueue->pool(), thread(), iPath); iHashTask->submit(this, SLOT(onHashTaskDone())); } // Refcounted BooksBook objects are managed by C++ code QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } void BooksBook::init() { iFontSizeAdjust = 0; iPageStackPos = 0; iCoverTask = NULL; iHashTask = NULL; iCoverTasksDone = false; iCopyingOut = false; iSaveTimer = NULL; moveToThread(qApp->thread()); } BooksBook::~BooksBook() { HDEBUG(qPrintable(iPath)); HASSERT(!iRef.load()); if (iCoverTask) iCoverTask->release(this); if (iHashTask) iHashTask->release(this); } BooksItem* BooksBook::retain() { if (iRef.load() >= 0) { iRef.ref(); } return this; } void BooksBook::release() { if (iRef.load() >= 0 && !iRef.deref()) { delete this; } } QObject* BooksBook::object() { return this; } BooksShelf* BooksBook::shelf() { return NULL; } BooksBook* BooksBook::book() { return this; } QString BooksBook::name() const { return iTitle; } QString BooksBook::fileName() const { return iFileName; } QString BooksBook::path() const { return iPath; } bool BooksBook::accessible() const { return !iCopyingOut; } bool BooksBook::setFontSizeAdjust(int aFontSizeAdjust) { if (aFontSizeAdjust > BooksSettings::FontSizeSteps) { aFontSizeAdjust = BooksSettings::FontSizeSteps; } else if (aFontSizeAdjust < -BooksSettings::FontSizeSteps) { aFontSizeAdjust = -BooksSettings::FontSizeSteps; } if (iFontSizeAdjust != aFontSizeAdjust) { iFontSizeAdjust = aFontSizeAdjust; requestSave(); Q_EMIT fontSizeAdjustChanged(); return true; } else { return false; } } void BooksBook::setPageStack(BooksPos::List aStack, int aStackPos) { if (aStackPos < 0) { aStackPos = 0; } else if (aStackPos >= aStack.count()) { aStackPos = aStack.count() - 1; } bool changed = false; if (iPageStack != aStack) { iPageStack = aStack; changed = true; } if (iPageStackPos != aStackPos) { iPageStackPos = aStackPos; changed = true; } if (changed) { requestSave(); } } void BooksBook::requestSave() { // We only need save timer if we have the state file if (!iSaveTimer && iStorage.isValid()) { iSaveTimer = new BooksSaveTimer(this); connect(iSaveTimer, SIGNAL(save()), SLOT(saveState())); } if (iSaveTimer) iSaveTimer->requestSave(); } void BooksBook::setCopyingOut(bool aValue) { if (iCopyingOut != aValue) { const bool wasAccessible = accessible(); iCopyingOut = aValue; Q_EMIT copyingOutChanged(); if (wasAccessible != accessible()) { Q_EMIT accessibleChanged(); } } } bool BooksBook::hasCoverImage() const { return iCoverImage.width() > 0 && iCoverImage.height() > 0; } void BooksBook::setCoverImage(const QImage aImage) { if (iCoverImage != aImage) { const bool hadCover = hasCoverImage(); iCoverImage = aImage; Q_EMIT coverImageChanged(); if (hadCover != hasCoverImage()) { Q_EMIT hasCoverChanged(); } } } bool BooksBook::requestCoverImage() { if (!iBook.isNull() && !iFormatPlugin.isNull() && !iCoverTasksDone && !iCoverTask) { HDEBUG(iTitle); (iCoverTask = new LoadCoverTask(iTaskQueue->pool(), iStateDir, iBook, iFormatPlugin, cachedImagePath()))->submit(this, SLOT(onLoadCoverTaskDone())); Q_EMIT loadingCoverChanged(); } return iCoverTask != NULL; } void BooksBook::cancelCoverRequest() { if (iCoverTask) { iCoverTask->release(this); iCoverTask = NULL; } } bool BooksBook::coverTaskDone() { HASSERT(sender() == iCoverTask); HASSERT(!iCoverTasksDone); HDEBUG(iTitle << iCoverTask->hasImage()); const bool gotCover = iCoverTask->hasImage(); if (gotCover) { setCoverImage(iCoverTask->iCoverImage); } iCoverTask->release(this); iCoverTask = NULL; return gotCover; } void BooksBook::onLoadCoverTaskDone() { HDEBUG(iTitle); const bool coverMissing = iCoverTask->iCoverMissing; if (coverTaskDone() || coverMissing) { iCoverTasksDone = true; Q_EMIT loadingCoverChanged(); } else { (iCoverTask = new GuessCoverTask(iTaskQueue->pool(), iStateDir, iBook, cachedImagePath()))->submit(this, SLOT(onGuessCoverTaskDone())); } } void BooksBook::onGuessCoverTaskDone() { HDEBUG(iTitle); coverTaskDone(); iCoverTasksDone = true; Q_EMIT loadingCoverChanged(); } void BooksBook::onHashTaskDone() { iHash = iHashTask->iHash; HDEBUG(QString(iHash.toHex())); iHashTask->release(this); iHashTask = NULL; iHashTaskQueue.reset(); Q_EMIT hashChanged(); } QString BooksBook::cachedImagePath() const { if (!iStateDir.isEmpty() && !iBook.isNull()) { return QDir::cleanPath(iStateDir + QDir::separator() + QString::fromStdString(iBook->file().name(false)) + BOOK_COVER_SUFFIX "jpg"); } return QString(); } void BooksBook::saveState() { if (!iStateFilePath.isEmpty()) { QVariantMap state; HarbourJson::load(iStateFilePath, state); state.insert(BOOK_STATE_POSITION, iPageStack.toVariantList()); state.insert(BOOK_STATE_FONT_SIZE_ADJUST, iFontSizeAdjust); #ifdef BOOK_STATE_PAGE_STACK_INDEX state.insert(BOOK_STATE_PAGE_STACK_INDEX, iPageStackPos); #endif if (HarbourJson::save(iStateFilePath, state)) { HDEBUG("wrote" << iStateFilePath); } } } void BooksBook::deleteFiles() { if (iStorage.isValid()) { if (QFile::remove(iPath)) { HDEBUG("deleted" << qPrintable(iPath)); } else { HWARN("failed to delete" << qPrintable(iPath)); } QDirIterator it(iStateDir); while (it.hasNext()) { QString path(it.next()); if (it.fileName().startsWith(iFileName)) { if (QFile::remove(path)) { HDEBUG(qPrintable(path)); } else { HWARN("failed to delete" << qPrintable(path)); } } } } } BooksBook* BooksBook::newBook(QString aFullPath) { shared_ptr ref = BooksUtil::bookFromFile(aFullPath); if (!ref.isNull()) { return new BooksBook(BooksStorage::tmpStorage(), QFileInfo(aFullPath).dir().absolutePath(), ref); } else { return NULL; } } BooksBook* BooksBook::newBook(const BooksStorage& aStorage, QString aRelPath, QString aFileName) { shared_ptr ref = BooksUtil::bookFromFile( QFileInfo(QDir(aStorage.fullPath(aRelPath)), aFileName). absoluteFilePath()); if (!ref.isNull()) { return new BooksBook(aStorage, aRelPath, ref); } else { return NULL; } } // NOTE: below methods are invoked on the worker thread bool BooksBook::makeLink(QString aDestPath) { QByteArray oldp(iPath.toLocal8Bit()); QByteArray newp(aDestPath.toLocal8Bit()); if (!oldp.isEmpty()) { if (!newp.isEmpty()) { int err = link(oldp.data(), newp.data()); if (!err) { HDEBUG("linked" << newp << "->" << oldp); return true; } else { HDEBUG(newp << "->" << oldp << "error:" << strerror(errno)); } } else { HDEBUG("failed to convert" << newp << "to locale encoding"); } } else { HDEBUG("failed to convert" << oldp << "to locale encoding"); } return false; } BooksItem* BooksBook::copyTo(const BooksStorage& aStorage, QString aRelPath, CopyOperation* aOperation) { QDir destDir(aStorage.fullPath(aRelPath)); destDir.mkpath(destDir.path()); const QString absDestPath(QFileInfo(QDir(aStorage.fullPath(aRelPath)), iFileName).absoluteFilePath()); if (!isCanceled(aOperation) && makeLink(absDestPath)) { return newBook(aStorage, aRelPath, iFileName); } else if (isCanceled(aOperation)) { return NULL; } else { BooksBook* copy = NULL; QFile src(iPath); const qint64 total = src.size(); qint64 copied = 0; if (src.open(QIODevice::ReadOnly)) { QFile dest(absDestPath); if (dest.open(QIODevice::WriteOnly)) { QDateTime lastTime; const qint64 bufsiz = 0x1000; char* buf = new char[bufsiz]; int progress = 0; qint64 len; while (!isCanceled(aOperation) && (len = src.read(buf, bufsiz)) > 0 && !isCanceled(aOperation) && dest.write(buf, len) == len) { copied += len; if (aOperation) { int newProg = (int)(copied*PROGRESS_PRECISION/total); if (progress != newProg) { // Don't fire signals too often QDateTime now(QDateTime::currentDateTimeUtc()); if (!lastTime.isValid() || lastTime.msecsTo(now) >= MIN_PROGRESS_DELAY) { lastTime = now; progress = newProg; aOperation->copyProgressChanged(progress); } } } } delete [] buf; dest.close(); aOperation->copyProgressChanged(PROGRESS_PRECISION); if (copied == total) { dest.setPermissions(BOOKS_FILE_PERMISSIONS); HDEBUG(total << "bytes copied from"<< qPrintable(iPath) << "to" << qPrintable(absDestPath)); copy = newBook(aStorage, aRelPath, iFileName); // Copy cover image too if (copy && !iCoverImage.isNull()) { QString cover(copy->cachedImagePath()); if (!cover.isEmpty() && iCoverImage.save(cover)) { HDEBUG("copied cover to" << qPrintable(cover)); } } } else { if (isCanceled(aOperation)) { HDEBUG("copy" << qPrintable(iPath) << "to" << qPrintable(absDestPath) << "cancelled"); } else { HWARN(copied << "out of" << total << "bytes copied from" << qPrintable(iPath) << "to" << qPrintable(absDestPath)); } dest.remove(); } } else { HWARN("failed to open" << qPrintable(absDestPath)); } src.close(); } else { HWARN("failed to open" << qPrintable(iPath)); } return copy; } }