#include "imagecompare.h" #include "border.h" #include "uimanager.h" #include "fileop.h" #include "imageutils.h" #include "ifapp.h" #include "imageheaders.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include KIFCompare::KIFCompare(const QString &path, int iconSize, UIManager *manager, QWidget *parent, const char *name) : QSemiModal(parent, name, true, WDestructiveClose) { setCaption(i18n("Finding Similiar Images")); testTime.start(); stopProcessing = false; dirPath = path; iSize = iconSize; mgr = manager; fileList.resize(6007); fileList.setAutoDelete(true); QVBoxLayout *layout = new QVBoxLayout(this, 5); stepLbl = new QLabel(i18n("Step 1: Generating image fingerprints."), this); layout->addWidget(stepLbl); layout->addSpacing(16); progress = new KProgress(Qt::Horizontal, this); progress->setValue(0); connect(this, SIGNAL(updateProgress(int)), progress, SLOT(setValue(int))); layout->addWidget(progress); layout->addSpacing(16); QWidget *btnFrame = new QWidget(this); QHBoxLayout *btnLayout = new QHBoxLayout(btnFrame); btnLayout->addStretch(1); QPushButton *cancelBtn = new QPushButton(i18n("Cancel"), btnFrame); connect(cancelBtn, SIGNAL(clicked()), this, SLOT(slotStopClicked())); btnLayout->addWidget(cancelBtn); btnLayout->addStretch(1); layout->addWidget(btnFrame); layout->addStretch(1); KStatusBar *statusBar = new KStatusBar(this); connect(this, SIGNAL(setStatusBarText(const QString &)), statusBar, SLOT(message(const QString &))); layout->addWidget(statusBar); setMinimumWidth(350); resize(sizeHint()); show(); view = NULL; stopProcessing = false; generateCompareData(); if(!stopProcessing) runCompare(); } void KIFCompare::closeEvent(QCloseEvent *) { stopProcessing = true; //QSemiModal::closeEvent(ev); } void KIFCompare::generateCompareData() { fileList.clear(); modified = false; QDir dir(dirPath); // load existing db entries dbFile.setName(dir.absPath() + "/.pixiedupes"); if(dbFile.open(IO_ReadOnly)){ loadCompareDB(); dbFile.close(); } else qWarning("No DB file found in %s", dir.absPath().ascii()); const QFileInfoList *fileList = dir.entryInfoList(); QFileInfoListIterator it(*fileList); QFileInfo *fi; int percent = 0; int current = 1; int count = it.count(); bool isImage; QImage img; // main loop while((fi = it.current()) && !stopProcessing){ if(!fi->isDir()){ KURL url("file:"+fi->absFilePath()); isImage = KMimeType::findByURL(url, true, true)->name().left(6) == "image/"; if(isImage){ loadCompareData(fi); } } if((int)(((float)current/count)*100) != percent){ percent = (int)(((float)current/count)*100); emit updateProgress(percent); kifapp()->processEvents(); } ++it; ++current; } // save db if modified if(modified && !stopProcessing){ if(!dbFile.open(IO_WriteOnly)){ KMessageBox::sorry(NULL, i18n("You do not have write access to this folder!\n\ PixiePlus will be unable to store image data."), i18n("Pixie Write Access Error")); } else{ writeCompareDB(); dbFile.close(); } } else qWarning("No images modified or added. DB not written"); } void KIFCompare::loadCompareData(QFileInfo *fi) { // check if already in DB and updated KIFCompareData *data = fileList.find(fi->fileName().ascii()); if(data){ // is it updated? if(data->time >= fi->lastModified()){ emit setStatusBarText(fi->fileName() + i18n(" already in DB")); kapp->processEvents(); return; } else{ emit setStatusBarText(fi->fileName() + i18n(" is in DB but modified...")); kapp->processEvents(); fileList.remove(fi->fileName().ascii()); } } else{ emit setStatusBarText(i18n("Generating fingerprint for ") + fi->fileName() + "..."); kapp->processEvents(); } modified = true; QImage img; if(!loadImage(img, fi->absFilePath())){ qWarning("Unable to load image: %s", fi->fileName().latin1()); return; } //img = img.smoothScale(160, 160); img = KImageEffect::sample(img, 160, 160); KImageEffect::toGray(img); img = KImageEffect::blur(img, 99); KImageEffect::normalize(img); KImageEffect::equalize(img); //img = img.smoothScale(16, 16); img = KImageEffect::sample(img, 16, 16); KImageEffect::threshold(img, 128); img = img.convertDepth(1, MonoOnly | ThresholdDither); data = new KIFCompareData; data->time = fi->lastModified(); // Can't just memcpy img.bits() because scanlines are aligned on a 32bit // boundary and there is only 2 bytes of data per scanline. Took me // awhile to figure this out ;-) int y, dy; unsigned char *scanline; for(y=0, dy=0; y < 16; ++y, dy+=2){ scanline = (unsigned char *)img.scanLine(y); data->data[dy] = scanline[0]; data->data[dy+1] = scanline[1]; } fileList.insert(fi->fileName().ascii(), data); } void KIFCompare::loadCompareDB() { emit setStatusBarText(i18n("Loading database...")); kapp->processEvents(); QDataStream stream(&dbFile); QString fn; while(!dbFile.atEnd()){ KIFCompareData *data = new KIFCompareData; stream >> fn; stream >> data->time; stream.readRawBytes((char *)data->data, 32); if(QFile::exists(dirPath + "/" + fn)) fileList.insert(fn.ascii(), data); else qWarning("Ignoring invalid entry %s", fn.ascii()); } } void KIFCompare::writeCompareDB() { emit setStatusBarText(i18n("Saving database...")); kapp->processEvents(); QDataStream stream(&dbFile); QAsciiDictIterator it(fileList); for(it.toFirst(); it.current(); ++it){ stream << QString(it.currentKey()); stream << (*it).time; stream.writeRawBytes((const char *)(*it).data, 32); } } void KIFCompare::outputFingerPrint(const unsigned char *data) { int i; QString s, str; for(i=0; i < 32; ++i){ s.sprintf("%02x", data[i]); str += s; } qWarning("Generated fingerprint %s, len: %d", str.latin1(), str.length()); } void KIFCompare::slotStopClicked() { qWarning("In slotStopClicked"); stopProcessing = true; } int KIFCompare::countBits(unsigned char val) { unsigned char i=1; int acc = 0; int count; for(count=0; count < 8; ++count){ if(val & i) ++acc; i = i << 1; } return(acc); } bool KIFCompare::checkIfMatched(const QString &first, const QString &second) { KIFCompareViewItem *i = (KIFCompareViewItem *)view->firstChild(); KIFCompareViewItem *j; bool exists = false; bool onematch = false; while(i && !exists){ j = (KIFCompareViewItem *)i->firstChild(); while(j && !exists){ // check if a match from child to parent if((i->path() == first && j->path() == second) || (j->path() == first && i->path() == second)){ qWarning("Found previous parent to child match of %s to %s", first.latin1(), second.latin1()); exists = true; } // check if a match from child to child else if(j->path() == first || j->path() == second){ if(onematch){ qWarning("Found previous child match for %s", j->path().latin1()); exists = true; } else onematch = true; } j = (KIFCompareViewItem *)j->nextSibling(); } i = (KIFCompareViewItem *)i->nextSibling(); } return(exists); } void KIFCompare::runCompare() { emit updateProgress(0); stepLbl->setText(i18n("Step 2: Comparing fingerprints")); kapp->processEvents(); QAsciiDictIterator it(fileList); QAsciiDictIterator secondIt(fileList); int percent = 0; int current = 1; int count = it.count(); int bitcount; unsigned char xor_bits; int c; view = new KIFCompareView(dirPath, iSize); connect(view, SIGNAL(imageSelected(const QString &)), mgr, SLOT(slotAddAndSetURL(const QString &))); connect(view, SIGNAL(addToFileList(const QString &)), mgr, SLOT(slotAddURL(const QString &))); KIFCompareViewItem *parentItem; // todo - make option int threshold = 92; int maxbits = (int)(256.0*(1.0-threshold/100.0)); QString str; for(it.toFirst(); it.current() && !stopProcessing; ++it){ emit setStatusBarText(i18n("Searching for matches to ") + it.currentKey()); // check this item to every other item parentItem = NULL; for(secondIt.toFirst(); secondIt.current() && !stopProcessing; ++secondIt){ // xor and accumulate bits if((*secondIt).data != (*it).data){ bitcount = 0; for(c=0; c < 32; ++c){ xor_bits = (*it).data[c] ^ (*secondIt).data[c]; if(xor_bits) bitcount += countBits(xor_bits); } if(bitcount <= maxbits){ // match found if(!checkIfMatched( dirPath+"/"+it.currentKey(), dirPath+"/"+secondIt.currentKey())){ if(!parentItem){ parentItem = new KIFCompareViewItem(view, dirPath+"/"+it.currentKey(), iSize); } (void)new KIFCompareViewItem(parentItem, dirPath+"/"+secondIt.currentKey(), bitcount, iSize); } } } } if((int)(((float)current/count)*100) != percent){ percent = (int)(((float)current/count)*100); emit updateProgress(percent); kifapp()->processEvents(); } ++it; ++current; } if(stopProcessing){ qWarning("runCompare canceled."); delete view; view = NULL; return; } QListViewItem *vi = view->firstChild(); while(vi){ vi->setOpen(true); vi = vi->nextSibling(); } qWarning("Time elapsed: %d", testTime.elapsed()); view->show(); } KIFCompareView::KIFCompareView(const QString &path, int iconSize, const char *name) : QListView(NULL, name, WDestructiveClose) { dirWatch = new KDirWatch; connect(dirWatch, SIGNAL(dirty(const QString &)), this, SLOT(slotDirChanged(const QString &))); dirWatch->addDir(path); dirWatch->startScan(); setAllColumnsShowFocus(true); setTreeStepSize(64); setShowToolTips(false); addColumn(i18n("Image")); addColumn(i18n("Details")); setCaption(i18n("Comparison Results")); connect(this, SIGNAL(doubleClicked(QListViewItem *)), this, SLOT(slotDoubleClicked(QListViewItem *))); connect(this, SIGNAL(rightButtonClicked(QListViewItem *, const QPoint &, int)), this, SLOT(slotRightButton(QListViewItem *, const QPoint &, int))); QImage tmpImg(iconSize-4, iconSize-4, 32); tmpImg.fill(Qt::lightGray.rgb()); QImage dest; KIFBorderEffect::solid(tmpImg, dest, Qt::black, 2); tmpPix.convertFromImage(dest); tmpImg.reset(); tmpImg.create(iconSize-4, iconSize-4, 32); tmpImg.fill(Qt::darkGray.rgb()); KIFBorderEffect::solid(tmpImg, dest, Qt::black, 2); emptyPix.convertFromImage(dest); parentCg = colorGroup(); parentCg.setColor(QColorGroup::Base, parentCg.base().dark(110)); stopProcessing = false; tips = new CompareTip(this, NULL); } KIFCompareView::~KIFCompareView() { qWarning("In KIFCompareView destructor"); delete dirWatch; delete tips; } void KIFCompareView::makeThumbnails() { QListViewItem *i, *j; i = firstChild(); while(i && !stopProcessing){ ((KIFCompareViewItem *)i)->calcPixmap(); kapp->processEvents(); j = i->firstChild(); while(j && !stopProcessing){ ((KIFCompareViewItem*)j)->calcPixmap(); kapp->processEvents(); j = j->nextSibling(); } i = i->nextSibling(); } } void KIFCompareView::closeEvent(QCloseEvent *ev) { stopProcessing = true; QListView::closeEvent(ev); } void KIFCompareView::slotDirChanged(const QString &) { qWarning("Compare view folder changed"); // iterate through all items and see if anything was deleted KIFCompareViewItem *i, *j, *tmp; bool changed = false; QFileInfo fi; i = (KIFCompareViewItem *)firstChild(); while(i){ if(!QFile::exists(i->path())){ // Parent item deleted. TODO: Rescan list and remove item, moving // child item to top if more than 2. changed = true; i->setPath(QString::null); i->setPixmap(0, emptyPix); i->setText(1, i18n("Original Image Deleted")); } j = (KIFCompareViewItem *)i->firstChild(); while(j){ tmp = (KIFCompareViewItem *)j->nextSibling(); if(!QFile::exists(j->path())){ // Child item deleted. changed = true; delete j; } j = tmp; } i = (KIFCompareViewItem *)i->nextSibling(); } if(changed){ qWarning("File was deleted"); // see if any parent items are empty, or if deleted parent items only // had one match i = (KIFCompareViewItem *)firstChild(); while(i){ tmp = (KIFCompareViewItem *)i->nextSibling(); if(i->childCount() == 0 || (i->path() == QString::null && i->childCount() == 1)) delete i; i = tmp; } } } void KIFCompareView::slotRightButton(QListViewItem *i, const QPoint &p, int col) { if(!i || col == -1 || ((KIFCompareViewItem *)i)->path() == QString::null) return; KPopupMenu *mnu = new KPopupMenu; mnu->insertTitle(BarIcon("filenew", KIcon::SizeSmall), i18n("Duplicate Image")); mnu->insertItem(BarIcon("filenew", KIcon::SizeSmall), i18n("Copy to FileList"), 1); mnu->insertItem(BarIcon("editcopy", KIcon::SizeSmall), i18n("Copy file location"), 2); mnu->insertItem(BarIcon("editcopy", KIcon::SizeSmall), i18n("Copy filename only"), 3); mnu->insertSeparator(); mnu->insertItem(BarIcon("edittrash", KIcon::SizeSmall), i18n("Delete"), 4); int id = mnu->exec(p); delete mnu; if(id == -1) return; if(id == 1){ emit addToFileList(((KIFCompareViewItem *)i)->path()); } else if(id == 2){ QFileInfo fi(((KIFCompareViewItem *)i)->path()); #if QT_VERSION & 0x00FF00 QApplication::clipboard()->setText(fi.absFilePath(), QClipboard::Selection); #else QApplication::clipboard()->setText(fi.absFilePath()); #endif } else if(id == 3){ QFileInfo fi(((KIFCompareViewItem *)i)->path()); #if QT_VERSION & 0x00FF00 QApplication::clipboard()->setText(fi.fileName(), QClipboard::Selection); #else QApplication::clipboard()->setText(fi.fileName()); #endif } else if(id == 4){ QString fn = ((KIFCompareViewItem *)i)->path(); if(unlink(fn.ascii()) == -1){ KMessageBox::sorry(this, i18n("Could not remove file ") + fn.ascii()); return; } } } void KIFCompareView::slotDoubleClicked(QListViewItem *item) { if(((KIFCompareViewItem *)item)->path() != QString::null) emit imageSelected(((KIFCompareViewItem *)item)->path()); } KIFCompareViewItem::KIFCompareViewItem(KIFCompareView *parent, const QString &path, int iconSize) : QListViewItem(parent) { pathStr = path; iSize = iconSize; QFileInfo fi(path); QString imageStr, cameraStr, commentStr; QString str = i18n("Original image:\n") + fi.fileName() + "\n" + calcSizeString(fi.size()); appendTooltipData(QFile::encodeName(fi.absFilePath()), imageStr, cameraStr, commentStr, false); if(!imageStr.isEmpty()) str += "\n" + imageStr; //calcPixmap(fi, iconSize); setPixmap(0, parent->tempFill()); setText(1, str); bits = 0; // shouldn't be used } KIFCompareViewItem::KIFCompareViewItem(KIFCompareViewItem *parent, const QString &path, int bitcount, int iconSize) : QListViewItem(parent) { QString imageStr, cameraStr, commentStr; pathStr = path; iSize = iconSize; QFileInfo fi(path); QString str; str.sprintf("%0.2f%% match\n", (1.0-bitcount/256.0)*100.0); str += fi.fileName() + "\n" + calcSizeString(fi.size()); appendTooltipData(QFile::encodeName(fi.absFilePath()), imageStr, cameraStr, commentStr, false); if(!imageStr.isEmpty()) str += "\n" + imageStr; //calcPixmap(fi, iconSize); setPixmap(0, ((KIFCompareView*)listView())->tempFill()); setText(1, str); bits = bitcount; } void KIFCompareViewItem::setup() { QFont fnt(listView()->font()); QFontMetrics fm(fnt); if(fm.lineSpacing()*7 > iSize+2) setHeight(fm.lineSpacing()*7); else setHeight(iSize+2); } void KIFCompareViewItem::setOpen(bool open) { if(open) QListViewItem::setOpen(open); } void KIFCompareViewItem::calcPixmap() { QFileInfo fi(pathStr); qWarning("Calculating pixmap for %s", fi.fileName().latin1()); QImage img; switch(iSize){ case 112: if(QFile::exists(fi.dirPath(true) + "/.pics/huge/" + fi.fileName())) img.load(fi.dirPath(true) + "/.pics/huge/" + fi.fileName(), "PNG"); break; case 90: if(QFile::exists(fi.dirPath(true) + "/.pics/large/" + fi.fileName())) img.load(fi.dirPath(true) + "/.pics/large/" + fi.fileName(), "PNG"); break; case 64: if(QFile::exists(fi.dirPath(true) + "/.pics/med/" + fi.fileName())) img.load(fi.dirPath(true) + "/.pics/med/" + fi.fileName(), "PNG"); break; case 48: if(QFile::exists(fi.dirPath(true) + "/.pics/small/" + fi.fileName())) img.load(fi.dirPath(true) + "/.pics/small/" + fi.fileName(), "PNG"); break; default: break; } if(img.isNull()){ if(!loadImage(img, fi.absFilePath())){ setText(1, i18n("Invalid Image!")); return; } else if(img.width() > iSize || img.height() > iSize){ if(img.width() > img.height()){ float percent = (((float)iSize)/img.width()); int h = (int)(img.height()*percent); img = img.smoothScale(iSize, h); } else{ float percent = (((float)iSize)/img.height()); int w = (int)(img.width()*percent); img = img.smoothScale(w, iSize); } } } QPixmap pix; pix.convertFromImage(img); setPixmap(0, pix); } QString KIFCompareViewItem::key(int, bool) const { return(QString::number(bits)); } QString KIFCompareViewItem::calcSizeString(int size) { QString str; if(size >= 1024){ size /= 1024; if(size >= 1024){ size /= 1024; str += i18n("Size: ")+QString::number(size)+"M"; } else{ str += i18n("Size: ")+QString::number(size)+"K"; } } else{ str += i18n("Size: ")+QString::number(size)+"B"; } return(str); } void KIFCompareViewItem::paintCell(QPainter *p, const QColorGroup &cg, int col, int w, int align) { if(!childCount()) QListViewItem::paintCell(p, cg, col, w, align); else QListViewItem::paintCell(p, ((KIFCompareView *)listView())->parentColorGroup(), col, w, align); } void KIFCompareViewItem::paintBranches(QPainter *p, const QColorGroup &cg, int w, int /*y*/, int h) { p->fillRect(QRect(0, 0, w, h ), cg.base()); } void CompareTip::maybeTip(const QPoint &p) { KIFCompareView *view = (KIFCompareView *)parentWidget(); KIFCompareViewItem *item = (KIFCompareViewItem *)view->itemAt(p); if(item){ QString tipStr; QFileInfo fi(item->path()); tipStr += fi.fileName() + "\n" + i18n("Dbl click to view, right click for options"); tip(view->itemRect(item), tipStr); } }