#include "comic.h" #include #include #include #include #include #include #include #include "bookmarks.h" //TODO desacoplar la dependencia con bookmarks #include "qnaturalsorting.h" #include "compressed_archive.h" #include "comic_db.h" #include "QsLog.h" enum YACReaderPageSortingMode { YACReaderNumericalSorting, YACReaderHeuristicSorting, YACReaderAlphabeticalSorting }; void comic_pages_sort(QList & pageNames, YACReaderPageSortingMode sortingMode); const QStringList Comic::imageExtensions = QStringList() << "*.jpg" << "*.jpeg" << "*.png" << "*.gif" << "*.tiff" << "*.tif" << "*.bmp" << "*.webp"; const QStringList Comic::literalImageExtensions = QStringList() << "jpg" << "jpeg" << "png" << "gif" << "tiff" << "tif" << "bmp" << "webp"; #ifndef use_unarr const QStringList ComicArchiveExtensions = QStringList() << "*.cbr" << "*.cbz" << "*.rar" << "*.zip" << "*.tar" << "*.7z" << "*.cb7" << "*.arj" << "*.cbt"; const QStringList LiteralComicArchiveExtensions = QStringList() << "cbr" << "cbz" << "rar" << "zip" << "tar" << "7z" << "cb7" << "arj" << "cbt"; #else const QStringList ComicArchiveExtensions = QStringList() << "*.cbr" << "*.cbz" << "*.rar" << "*.zip" << "*.tar" << "*.cbt"; const QStringList LiteralComicArchiveExtensions = QStringList() << "cbr" << "cbz" << "rar" << "zip" << "tar" << "cbt"; #endif //use_unarr #ifndef NO_PDF const QStringList Comic::comicExtensions = QStringList() << ComicArchiveExtensions << "*.pdf"; const QStringList Comic::literalComicExtensions = QStringList() << LiteralComicArchiveExtensions << "pdf"; #else const QStringList Comic::comicExtensions = ComicArchiveExtensions; const QStringList Comic::literalComicExtensions = LiteralComicArchiveExtensions; #endif //NO_PDF //----------------------------------------------------------------------------- Comic::Comic() :_pages(),_index(0),_path(),_loaded(false),bm(new Bookmarks()),_loadedPages(),_isPDF(false),_invalidated(false),_errorOpening(false) { setup(); } //----------------------------------------------------------------------------- Comic::Comic(const QString & pathFile, int atPage ) :_pages(),_index(0),_path(pathFile),_loaded(false),bm(new Bookmarks()),_loadedPages(),_isPDF(false),_firstPage(atPage),_errorOpening(false) { setup(); } //----------------------------------------------------------------------------- Comic::~Comic() { emit destroyed(); delete bm; } //----------------------------------------------------------------------------- void Comic::setup() { connect(this,SIGNAL(pageChanged(int)),this,SLOT(checkIsBookmark(int))); connect(this,SIGNAL(imageLoaded(int)),this,SLOT(updateBookmarkImage(int))); connect(this,SIGNAL(imageLoaded(int)),this,SLOT(setPageLoaded(int))); auto l = [&](){ _errorOpening = true; }; void (Comic::* errorOpeningPtr)() = &Comic::errorOpening; void (Comic::* errorOpeningWithStringPtr)(QString) = &Comic::errorOpening; connect(this, errorOpeningPtr, l); connect(this, errorOpeningWithStringPtr, l); connect(this, &Comic::crcErrorFound, l); } //----------------------------------------------------------------------------- int Comic::nextPage() { if(_index<_pages.size()-1) { _index++; emit pageChanged(_index); } else { emit isLast(); } return _index; } //--------------------------------------------------------------------------- int Comic::previousPage() { if(_index>0) { _index--; emit pageChanged(_index); } else { emit isCover(); } return _index; } //----------------------------------------------------------------------------- void Comic::setIndex(unsigned int index) { int previousIndex = _index; if(static_cast(index)<_pages.size()-1) { _index = index; } else { _index = _pages.size()-1; } if(previousIndex != _index) { emit pageChanged(_index); } } //----------------------------------------------------------------------------- /*QPixmap * Comic::currentPage() { QPixmap * p = new QPixmap(); p->loadFromData(_pages[_index]); return p; } //----------------------------------------------------------------------------- QPixmap * Comic::operator[](unsigned int index) { QPixmap * p = new QPixmap(); p->loadFromData(_pages[index]); return p; }*/ bool Comic::load(const QString & path, const ComicDB & comic) { Q_UNUSED(path); Q_UNUSED(comic); return false; }; //----------------------------------------------------------------------------- bool Comic::loaded() { return _loaded; } //----------------------------------------------------------------------------- void Comic::loadFinished() { emit imagesLoaded(); } //----------------------------------------------------------------------------- void Comic::setBookmark() { QImage p; p.loadFromData(_pages[_index]); bm->setBookmark(_index,p); //emit bookmarksLoaded(*bm); emit bookmarksUpdated(); } //----------------------------------------------------------------------------- void Comic::removeBookmark() { bm->removeBookmark(_index); //emit bookmarksLoaded(*bm); emit bookmarksUpdated(); } //----------------------------------------------------------------------------- void Comic::saveBookmarks() { QImage p; p.loadFromData(_pages[_index]); bm->setLastPage(_index,p); bm->save(); } //----------------------------------------------------------------------------- void Comic::checkIsBookmark(int index) { emit isBookmark(bm->isBookmark(index)); } //----------------------------------------------------------------------------- void Comic::updateBookmarkImage(int index) { if (bm == nullptr) { return; } if(bm->isBookmark(index)) { QImage p; p.loadFromData(_pages[index]); bm->setBookmark(index,p); emit bookmarksUpdated(); //emit bookmarksLoaded(*bm); } if(bm->getLastPage() == index) { QImage p; p.loadFromData(_pages[index]); bm->setLastPage(index,p); emit bookmarksUpdated(); //emit bookmarksLoaded(*bm); } } //----------------------------------------------------------------------------- void Comic::setPageLoaded(int page) { _loadedPages[page] = true; } void Comic::invalidate() { _invalidated = true; emit invalidated(); } //----------------------------------------------------------------------------- QByteArray Comic::getRawPage(int page) { if(page < 0 || page >= _pages.size()) { return QByteArray(); } return _pages[page]; } //----------------------------------------------------------------------------- bool Comic::pageIsLoaded(int page) { if(page < 0 || page >= _pages.size()) { return false; } return _loadedPages[page]; } bool Comic::hasBeenAnErrorOpening() { return _errorOpening; } bool Comic::fileIsComic(const QString &path) { QFileInfo info(path); return literalComicExtensions.contains(info.suffix()); } QList Comic::findValidComicFiles(const QList &list) { QLOG_DEBUG() << "-findValidComicFiles-"; QList validComicFiles; QString currentPath; foreach (QUrl url, list) { currentPath = url.toLocalFile(); if(Comic::fileIsComic(currentPath)) { validComicFiles << currentPath; } else if(QFileInfo(currentPath).isDir()) { validComicFiles << findValidComicFilesInFolder(currentPath); } } QLOG_DEBUG() << "-" << validComicFiles << "-"; return validComicFiles; } QList Comic::findValidComicFilesInFolder(const QString &path) { QLOG_DEBUG() << "-findValidComicFilesInFolder-" << path; if(!QFileInfo(path).isDir()) return QList(); QList validComicFiles; QDir folder(path); folder.setNameFilters(Comic::comicExtensions); folder.setFilter(QDir::AllDirs|QDir::Files|QDir::NoDotAndDotDot); QFileInfoList folderContent = folder.entryInfoList(); QString currentPath; foreach (QFileInfo info, folderContent) { currentPath = info.absoluteFilePath(); if(info.isDir()) { validComicFiles << findValidComicFilesInFolder(currentPath); //find comics recursively } else if(Comic::fileIsComic(currentPath)) { validComicFiles << currentPath; } } return validComicFiles; } //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// FileComic::FileComic() :Comic() { } FileComic::FileComic(const QString & path, int atPage ) :Comic(path,atPage) { load(path,atPage); } FileComic::~FileComic() { _pages.clear(); _loadedPages.clear(); _fileNames.clear(); _newOrder.clear(); _order.clear(); } bool FileComic::load(const QString & path, int atPage) { QFileInfo fi(path); if(fi.exists()) { if(atPage == -1) { bm->newComic(path); emit bookmarksUpdated(); } _firstPage = atPage; //emit bookmarksLoaded(*bm); _path = QDir::cleanPath(path); //load files size return true; } else { //QMessageBox::critical(NULL,tr("Not found"),tr("Comic not found")+" : " + path); emit errorOpening(); return false; } } bool FileComic::load(const QString & path, const ComicDB & comic) { QFileInfo fi(path); if(fi.exists()) { QList bookmarkIndexes; bookmarkIndexes << comic.info.bookmark1 << comic.info.bookmark2 << comic.info.bookmark3; if(bm->load(bookmarkIndexes,comic.info.currentPage-1)) { emit bookmarksUpdated(); } _firstPage = comic.info.currentPage-1; _path = QDir::cleanPath(path); return true; } else { //QMessageBox::critical(NULL,tr("Not found"),tr("Comic not found")+" : " + path); moveToThread(QCoreApplication::instance()->thread()); emit errorOpening(); return false; } } QList FileComic::filter(const QList & src) { QList extensions = getSupportedImageLiteralFormats(); QList filtered; bool fileAccepted = false; foreach(QString fileName,src) { fileAccepted = false; if(!fileName.contains("__MACOSX")) { foreach(QString extension,extensions) { if(fileName.endsWith(extension,Qt::CaseInsensitive)) { fileAccepted = true; break; } } } if(fileAccepted) { filtered.append(fileName); } } return filtered; } //DELEGATE methods void FileComic::fileExtracted(int index, const QByteArray & rawData) { /*QFile f("c:/temp/out2.txt"); f.open(QIODevice::Append); QTextStream out(&f);*/ int sortedIndex = _fileNames.indexOf(_order.at(index)); //out << sortedIndex << " , "; //f.close(); if(sortedIndex == -1) { return; } _pages[sortedIndex] = rawData; emit imageLoaded(sortedIndex); emit imageLoaded(sortedIndex,_pages[sortedIndex]); } void FileComic::crcError(int index) { emit crcErrorFound(tr("CRC error on page (%1): some of the pages will not be displayed correctly").arg(index+1)); } //TODO: comprobar que si se produce uno de estos errores, la carga del c�mic es irrecuperable void FileComic::unknownError(int index) { Q_UNUSED(index) emit errorOpening(tr("Unknown error opening the file")); //emit errorOpening(); } bool FileComic::isCancelled() { return _invalidated; } //-------------------------------------- QList > FileComic::getSections(int & sectionIndex) { QVector sortedIndexes; foreach(QString name, _fileNames) { sortedIndexes.append(_order.indexOf(name)); } QList > sections; quint32 previous = 0; sectionIndex = -1; int sectionCount = 0; QVector section; int idx = 0; unsigned int realIdx; foreach(quint32 i, sortedIndexes) { if(_firstPage == idx) { sectionIndex = sectionCount; realIdx = i; } if(previous <= i) { //out << "idx : " << i << endl; section.append(i); previous = i; } else { if(sectionIndex == sectionCount) //found { if(section.indexOf(realIdx)!=0) { QVector section1; QVector section2; foreach(quint32 si,section) { if(si (); //out << "---------------" << endl; section.append(i); //out << "idx : " << i << endl; previous = i; sectionCount++; } idx++; } if(sectionIndex == sectionCount) //found { if(section.indexOf(realIdx)!=0) { QVector section1; QVector section2; foreach(quint32 si,section) { if(sithread()); emit errorOpening(tr("7z not found")); return; } if(!archive.isValid()) { moveToThread(QCoreApplication::instance()->thread()); emit errorOpening(tr("Format not supported")); return; } //se filtran para obtener s�lo los formatos soportados _order = archive.getFileNames(); _fileNames = filter(_order); if(_fileNames.size()==0) { //QMessageBox::critical(NULL,tr("File error"),tr("File not found or not images in file")); moveToThread(QCoreApplication::instance()->thread()); emit errorOpening(); return; } //TODO, cambiar por listas //_order = _fileNames; _pages.resize(_fileNames.size()); _loadedPages = QVector(_fileNames.size(),false); emit pageChanged(0); // this indicates new comic, index=0 emit numPages(_pages.size()); _loaded = true; _cfi=0; //TODO, add a setting for choosing the type of page sorting used. comic_pages_sort(_fileNames, YACReaderHeuristicSorting); if(_firstPage == -1) { _firstPage = bm->getLastPage(); } if(_firstPage >= _pages.length()) { _firstPage = 0; } _index = _firstPage; emit(openAt(_index)); int sectionIndex; QList > sections = getSections(sectionIndex); for(int i = sectionIndex; ithread()); return; } archive.getAllData(sections.at(i),this); } for(int i = 0; ithread()); return; } archive.getAllData(sections.at(i),this); } //archive.getAllData(QVector(),this); /* foreach(QString name,_fileNames) { index = _order.indexOf(name); sortedIndex = _fileNames.indexOf(name); _pages[sortedIndex] = allData.at(index); emit imageLoaded(sortedIndex); emit imageLoaded(sortedIndex,_pages[sortedIndex]); }*/ moveToThread(QCoreApplication::instance()->thread()); emit imagesLoaded(); } //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// FolderComic::FolderComic() :Comic() { } FolderComic::FolderComic(const QString & path, int atPage ) :Comic(path, atPage ) { load(path, atPage ); } FolderComic::~FolderComic() { } bool FolderComic::load(const QString & path, int atPage ) { _path = path; if(atPage == -1) { bm->newComic(_path); emit bookmarksUpdated(); } _firstPage = atPage; //emit bookmarksLoaded(*bm); return true; } void FolderComic::process() { QDir d(_path); d.setNameFilters(getSupportedImageFormats()); d.setFilter(QDir::Files|QDir::NoDotAndDotDot); //d.setSorting(QDir::Name|QDir::IgnoreCase|QDir::LocaleAware); QFileInfoList list = d.entryInfoList(); //don't fix double page files sorting, because the user can see how the SO sorts the files in the folder. std::sort(list.begin(),list.end(),naturalSortLessThanCIFileInfo); int nPages = list.size(); _pages.clear(); _pages.resize(nPages); _loadedPages = QVector(nPages,false); if(nPages==0) { //TODO emitir este mensaje en otro sitio //QMessageBox::critical(NULL,QObject::tr("No images found"),QObject::tr("There are not images on the selected folder")); moveToThread(QCoreApplication::instance()->thread()); emit errorOpening(); } else { if(_firstPage == -1) { _firstPage = bm->getLastPage(); } if(_firstPage >= _pages.length()) { _firstPage = 0; } _index = _firstPage; emit(openAt(_index)); emit pageChanged(0); // this indicates new comic, index=0 emit numPages(_pages.size()); _loaded = true; int count=0; int i=_firstPage; while(countthread()); return; } QFile f(list.at(i).absoluteFilePath()); f.open(QIODevice::ReadOnly); _pages[i]=f.readAll(); emit imageLoaded(i); emit imageLoaded(i,_pages[i]); i++; if(i==nPages) { i=0; } count++; } } moveToThread(QCoreApplication::instance()->thread()); emit imagesLoaded(); } //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// #ifndef NO_PDF PDFComic::PDFComic() :Comic() { } PDFComic::PDFComic(const QString & path, int atPage) :Comic(path,atPage) { load(path,atPage); } PDFComic::~PDFComic() { } bool PDFComic::load(const QString & path, int atPage) { QFileInfo fi(path); if(fi.exists()) { _path = path; if(atPage == -1) { bm->newComic(_path); emit bookmarksUpdated(); } _firstPage = atPage; //emit bookmarksLoaded(*bm); return true; } else { moveToThread(QCoreApplication::instance()->thread()); emit errorOpening(); return false; } } bool PDFComic::load(const QString & path, const ComicDB & comic) { QFileInfo fi(path); if(fi.exists()) { QList bookmarkIndexes; bookmarkIndexes << comic.info.bookmark1 << comic.info.bookmark2 << comic.info.bookmark3; if(bm->load(bookmarkIndexes,comic.info.currentPage-1)) { emit bookmarksUpdated(); } _firstPage = comic.info.currentPage-1; _path = QDir::cleanPath(path); return true; } else { //QMessageBox::critical(NULL,tr("Not found"),tr("Comic not found")+" : " + path); moveToThread(QCoreApplication::instance()->thread()); emit errorOpening(); return false; } } void PDFComic::process() { #if defined Q_OS_MAC && defined USE_PDFKIT pdfComic = new MacOSXPDFComic(); if(!pdfComic->openComic(_path)) { delete pdfComic; emit errorOpening(); return; } #elif defined USE_PDFIUM pdfComic = new PdfiumComic(); if(!pdfComic->openComic(_path)) { delete pdfComic; emit errorOpening(); return; } #else pdfComic = Poppler::Document::load(_path); if (!pdfComic) { //delete pdfComic; //pdfComic = 0; moveToThread(QCoreApplication::instance()->thread()); emit errorOpening(); return; } if (pdfComic->isLocked()) { moveToThread(QCoreApplication::instance()->thread()); emit errorOpening(); return; } //pdfComic->setRenderHint(Poppler::Document::Antialiasing, true); pdfComic->setRenderHint(Poppler::Document::TextAntialiasing, true); #endif int nPages = pdfComic->numPages(); emit pageChanged(0); // this indicates new comic, index=0 emit numPages(nPages); _loaded = true; //QMessageBox::critical(NULL,QString("%1").arg(nPages),tr("Invalid PDF file")); _pages.clear(); _pages.resize(nPages); _loadedPages = QVector(nPages,false); if(_firstPage == -1) { _firstPage = bm->getLastPage(); } if(_firstPage >= _pages.length()) { _firstPage = 0; } _index = _firstPage; emit(openAt(_index)); //buffer index to avoid race conditions int buffered_index = _index; for(int i=buffered_index;ithread()); return; } renderPage(i); } for(int i=0;ithread()); return; } renderPage(i); } delete pdfComic; moveToThread(QCoreApplication::instance()->thread()); emit imagesLoaded(); } void PDFComic::renderPage(int page) { #if defined Q_OS_MAC && defined USE_PDFKIT QImage img = pdfComic->getPage(page); if(!img.isNull()) { #elif defined USE_PDFIUM QImage img = pdfComic->getPage(page); if(!img.isNull()) { #else Poppler::Page* pdfpage = pdfComic->page(page); if (pdfpage) { QImage img = pdfpage->renderToImage(150,150); delete pdfpage; #endif QByteArray ba; QBuffer buf(&ba); img.save(&buf, "jpg", 96); _pages[page] = ba; emit imageLoaded(page); emit imageLoaded(page,_pages[page]); } } #endif //NO_PDF Comic * FactoryComic::newComic(const QString & path) { QFileInfo fi(path); if(fi.exists()) { if(fi.isFile()) { #ifndef NO_PDF if(fi.suffix().compare("pdf",Qt::CaseInsensitive) == 0) { return new PDFComic(); } else { return new FileComic(); } #else return new FileComic(); #endif } else { if(fi.isDir()) { return new FolderComic(); } else { return NULL; } } } else return NULL; } bool is_double_page(const QString & pageName, const QString & commonPrefix, const int maxExpectedDoublePagesNumberLenght) { if(pageName.startsWith(commonPrefix)) { QString substringContainingPageNumbers = pageName.mid(commonPrefix.length()); QString pageNumbersSubString; for(int i = 0 ; i < substringContainingPageNumbers.length() && substringContainingPageNumbers.at(i).isDigit(); i++) { pageNumbersSubString.append(substringContainingPageNumbers.at(i)); } if(pageNumbersSubString.length() < 3 || pageNumbersSubString.length() > maxExpectedDoublePagesNumberLenght || pageNumbersSubString.length() % 2 == 1) { return false; } int leftPageNumber = pageNumbersSubString.left(pageNumbersSubString.length() / 2).toInt(); int rightPageNumber = pageNumbersSubString.mid(pageNumbersSubString.length() / 2).toInt(); if(leftPageNumber == 0 || rightPageNumber == 0) { return false; } if((rightPageNumber - leftPageNumber) == 1) { return true; } } return false; } QString get_most_common_prefix(const QList & pageNames) { if(pageNames.isEmpty()) { return ""; } QMap frequency; int currentPrefixLenght = pageNames.at(0).split('/').last().length(); int currentPrefixCount = 1; int i; QString previous; QString current; for(i = 1; i < pageNames.length(); i++) { int pos = 0; previous = pageNames.at(i-1).split('/').last(); current = pageNames.at(i).split('/').last(); for(; pos < current.length() && previous[pos] == current[pos]; pos++); if(pos < currentPrefixLenght && pos > 0) { frequency.insert(previous.left(currentPrefixLenght), currentPrefixCount); currentPrefixLenght = pos; currentPrefixCount++; } /* else if(pos > currentPrefixLenght) { frequency.insert(pageNames.at(i-1).left(currentPrefixLenght), currentPrefixCount - 1); currentPrefixLenght = pos; currentPrefixCount = 2; }*/ else if(pos == 0) { frequency.insert(previous.left(currentPrefixLenght), currentPrefixCount); currentPrefixLenght = current.length(); currentPrefixCount = 1; } else { currentPrefixCount++; } } frequency.insert(previous.left(currentPrefixLenght), currentPrefixCount); uint maxFrequency = 0; QString common_prefix = ""; foreach(QString key, frequency.keys()) { if(maxFrequency < frequency.value(key)) { maxFrequency = frequency.value(key); common_prefix = key; } } QRegExp allNumberRegExp("\\d+"); if (allNumberRegExp.exactMatch(common_prefix)) { return ""; } if(maxFrequency < pageNames.length() * 0.60) //the most common tipe of image file should a proper page, so we can asume that the common_prefix should be in, at least, the 60% of the pages { return ""; } return common_prefix; } void get_double_pages(const QList & pageNames, QList & singlePageNames/*out*/, QList & doublePageNames/*out*/) { uint maxExpectedDoublePagesNumberLenght = (int)(log10(pageNames.length())+1) * 2; QString mostCommonPrefix = get_most_common_prefix(pageNames); foreach(const QString & pageName, pageNames) { if(is_double_page(pageName.split('/').last(), mostCommonPrefix, maxExpectedDoublePagesNumberLenght)) { doublePageNames.append(pageName); } else { singlePageNames.append(pageName); } } } QList merge_pages(QList & singlePageNames, QList & doublePageNames) { //NOTE: this implementation doesn't differ from std::merge using a custom comparator, but it can be easily tweaked if merging requeries an additional heuristic behaviour QList pageNames; int i = 0; int j = 0; while (i < singlePageNames.length() && j < doublePageNames.length()) { if (singlePageNames.at(i).compare(doublePageNames.at(j), Qt::CaseInsensitive) < 0) { pageNames.append(singlePageNames.at(i++)); } else { pageNames.append(doublePageNames.at(j++)); } } while (i < singlePageNames.length()) { pageNames.append(singlePageNames.at(i++)); } while (j < doublePageNames.length()) { pageNames.append(doublePageNames.at(j++)); } return pageNames; } void comic_pages_sort(QList & pageNames, YACReaderPageSortingMode sortingMode) { switch(sortingMode) { case YACReaderNumericalSorting: std::sort(pageNames.begin(), pageNames.end(), naturalSortLessThanCI); break; case YACReaderHeuristicSorting: { std::sort(pageNames.begin(), pageNames.end(), naturalSortLessThanCI); QList singlePageNames; QList doublePageNames; get_double_pages(pageNames, singlePageNames, doublePageNames); if(doublePageNames.length() > 0) { pageNames = merge_pages(singlePageNames, doublePageNames); } } break; case YACReaderAlphabeticalSorting: std::sort(pageNames.begin(), pageNames.end()); break; } }