Quellcode durchsuchen

Add: Accept image URL drop

Satoshi Yoneda vor 2 Wochen
Ursprung
Commit
57ef7672da

+ 2 - 1
CMakeLists.txt

@@ -17,7 +17,7 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
 endif()
 
 # Qt 6
-find_package(Qt6 COMPONENTS Widgets Concurrent Linguist REQUIRED)
+find_package(Qt6 COMPONENTS Widgets Concurrent Linguist Network REQUIRED)
 set(CMAKE_AUTOMOC ON)
 set(CMAKE_AUTOUIC ON)
 set(CMAKE_AUTORCC ON)
@@ -41,6 +41,7 @@ target_link_libraries(DupFindCore
     PUBLIC 
         Qt6::Widgets
         Qt6::Concurrent
+        Qt6::Network
         ${OpenCV_LIBS}
         SQLite::SQLite3
 )

+ 6 - 1
include/MainWindow.hpp

@@ -13,6 +13,8 @@
 #include <QListView>
 #include <QListWidget>
 #include <QMainWindow>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
 #include <QObject>
 #include <QProgressBar>
 #include <QPushButton>
@@ -47,6 +49,7 @@ private slots:
                               const QPoint &globalPos);
   void onFileDoubleClicked(const std::string &path);
   void onSearchTextChanged(const QString &text);
+  void onUrlDownloadFinished(QNetworkReply *reply);
 
 private:
   // 内部処理・初期化メソッド
@@ -63,7 +66,7 @@ private:
               QEvent *event) override; // 右クリックメニュー等のイベントフィルタ
   void
   closeEvent(QCloseEvent *event) override; // ウィンドウ終了時の保存処理など
-  void dropEvent(QDropEvent *e) override; // ドロップを受け付ける
+  void dropEvent(QDropEvent *e) override;  // ドロップを受け付ける
   void dragEnterEvent(QDragEnterEvent *event) override; // ドロップを受け付ける
 
   // データベースマネージャーのインスタンス
@@ -107,4 +110,6 @@ private:
   std::vector<DuplicateGroup> m_currentGroups; // 現在表示中のグループ一覧
   std::unordered_set<std::string>
       m_ignoredPaths; // セッション中に除外した画像のパス
+
+  QNetworkAccessManager *m_networkManager;
 };

+ 1 - 1
include/ResultListModel.hpp

@@ -7,7 +7,7 @@
 #include "SimilaritySearch.hpp"
 
 struct ResultListItem {
-    enum Type { Header, ImageRow };
+    enum Type { Header, ImageRow, Message };
     Type type;
     int groupId;
     QString headerText;

+ 84 - 2
src/MainWindow.cpp

@@ -1,5 +1,6 @@
 #include "MainWindow.hpp"
 #include "ImageHasher.hpp"
+#include <QDateTime>
 #include <QDir>
 #include <QDirIterator>
 #include <QFile>
@@ -7,6 +8,7 @@
 #include <QFileInfo>
 #include <QHBoxLayout>
 #include <QMessageBox>
+#include <QNetworkRequest>
 #include <QPixmap>
 #include <QUrl>
 #include <QVBoxLayout>
@@ -39,6 +41,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
   m_dbManager->open();
   m_scanWatcher = new QFutureWatcher<void>(this);
   m_searchWatcher = new QFutureWatcher<std::vector<DuplicateGroup>>(this);
+  m_networkManager = new QNetworkAccessManager(this);
+  connect(m_networkManager, &QNetworkAccessManager::finished, this,
+          &MainWindow::onUrlDownloadFinished);
 
   m_searchTimer = new QTimer(this);
   m_searchTimer->setSingleShot(true);
@@ -271,8 +276,17 @@ void MainWindow::dropEvent(QDropEvent *e) {
 
   for (const QUrl &url : mimeData->urls()) {
     QString localPath = url.toLocalFile();
-    if (localPath.isEmpty())
+    if (localPath.isEmpty()) {
+      // 外部URLの可能性がある場合
+      if (url.isValid() &&
+          (url.scheme() == "http" || url.scheme() == "https")) {
+        m_networkManager->get(QNetworkRequest(url));
+        m_progressBar->setVisible(true);
+        m_progressBar->setRange(0, 0);
+        m_progressBar->setFormat(tr("Downloading image..."));
+      }
       continue;
+    }
 
     QFileInfo fileInfo(localPath);
     if (fileInfo.isDir()) {
@@ -293,7 +307,7 @@ void MainWindow::dropEvent(QDropEvent *e) {
       cv::Mat img = ImageHasher::loadImage(localPath.toStdString());
       if (img.empty())
         continue;
-      
+
       // ここでクリア
       m_model->clear();
 
@@ -718,3 +732,71 @@ void MainWindow::onDeleteSelected() {
     updateResultGrid(m_currentGroups);
   }
 }
+
+void MainWindow::onUrlDownloadFinished(QNetworkReply *reply) {
+  m_progressBar->setVisible(false);
+  if (reply->error() != QNetworkReply::NoError) {
+    QMessageBox::warning(
+        this, tr("Download Error"),
+        tr("Failed to download image: %1").arg(reply->errorString()));
+    reply->deleteLater();
+    return;
+  }
+
+  QByteArray data = reply->readAll();
+  reply->deleteLater();
+
+  std::vector<uchar> buffer(data.begin(), data.end());
+  cv::Mat img = cv::imdecode(buffer, cv::IMREAD_COLOR);
+
+  if (img.empty()) {
+    QMessageBox::warning(this, tr("Invalid Image"),
+                         tr("The dropped URL does not contain a valid image."));
+    return;
+  }
+
+  m_model->clear();
+  uint64_t dhash = ImageHasher::calculateDHash(img);
+  uint64_t phash = ImageHasher::calculatePHash(img);
+
+  auto candidates = getFilteredImages();
+  DuplicateGroup foundGroup;
+
+  // ドロップされた画像(URL)を「Dropped Image」として追加
+  ImageData dropped;
+  dropped.path = reply->url().toString().toStdString();
+  dropped.dhash = dhash;
+  dropped.phash = phash;
+  dropped.file_size = data.size();
+  dropped.timestamp = QDateTime::currentSecsSinceEpoch();
+  foundGroup.images.push_back(dropped);
+
+  for (const auto &cand : candidates) {
+    bool match = false;
+    if (m_strictMode) {
+      if (ImageHasher::hammingDistance(dhash, cand.dhash) <=
+              m_currentThreshold &&
+          ImageHasher::hammingDistance(phash, cand.phash) <=
+              m_currentThreshold) {
+        match = true;
+      }
+    } else {
+      if (ImageHasher::hammingDistance(dhash, cand.dhash) <=
+              m_currentThreshold ||
+          ImageHasher::hammingDistance(phash, cand.phash) <=
+              m_currentThreshold) {
+        match = true;
+      }
+    }
+    if (match) {
+      foundGroup.images.push_back(cand);
+    }
+  }
+
+  std::vector<DuplicateGroup> results;
+  if (foundGroup.images.size() > 1) {
+    results.push_back(foundGroup);
+  }
+
+  updateResultGrid(results);
+}

+ 5 - 0
src/ResultItemDelegate.cpp

@@ -55,6 +55,9 @@ void ResultItemDelegate::paint(QPainter *painter,
     painter->setPen(Qt::black);
     painter->drawText(r.adjusted(5, 0, -5, 0), Qt::AlignVCenter | Qt::AlignLeft,
                       item.headerText);
+  } else if (item.type == ResultListItem::Message) {
+    painter->setPen(QColor("#999999"));
+    painter->drawText(option.rect, Qt::AlignCenter, item.headerText);
   } else {
     int cardWidth = option.rect.width() / 4;
     for (int i = 0; i < static_cast<int>(item.images.size()); ++i) {
@@ -121,6 +124,8 @@ QSize ResultItemDelegate::sizeHint(const QStyleOptionViewItem &option,
   const auto &item = model->getItem(sourceIndex.row());
   if (item.type == ResultListItem::Header) {
     return QSize(option.rect.width(), 40);
+  } else if (item.type == ResultListItem::Message) {
+    return QSize(option.rect.width(), 100);
   } else {
     return QSize(option.rect.width(), 220);
   }

+ 38 - 31
src/ResultListModel.cpp

@@ -37,44 +37,51 @@ void ResultListModel::setGroups(const std::vector<DuplicateGroup>& groups, bool
     m_items.clear();
     
     int groupId = 0;
-    for (const auto& group : groups) {
-        if (group.images.empty()) continue;
-
-        const ImageData* bestImage = &group.images[0];
-        for (size_t i = 1; i < group.images.size(); ++i) {
-            if (group.images[i].file_size > bestImage->file_size) {
-                bestImage = &group.images[i];
+    if (groups.empty()) {
+        ResultListItem msg;
+        msg.type = ResultListItem::Message;
+        msg.headerText = tr("No similar images found.");
+        m_items.push_back(msg);
+    } else {
+        for (const auto& group : groups) {
+            if (group.images.empty()) continue;
+
+            const ImageData* bestImage = &group.images[0];
+            for (size_t i = 1; i < group.images.size(); ++i) {
+                if (group.images[i].file_size > bestImage->file_size) {
+                    bestImage = &group.images[i];
+                }
             }
-        }
 
-        for (const auto& img : group.images) {
-            if (preserveState && oldState.find(img.path) != oldState.end()) {
-                m_checkStates[img.path] = oldState[img.path];
-            } else {
-                m_checkStates[img.path] = (&img != bestImage);
+            for (const auto& img : group.images) {
+                if (preserveState && oldState.find(img.path) != oldState.end()) {
+                    m_checkStates[img.path] = oldState[img.path];
+                } else {
+                    m_checkStates[img.path] = (&img != bestImage);
+                }
             }
-        }
 
-        ResultListItem header;
-        header.type = ResultListItem::Header;
-        header.groupId = groupId;
-        header.headerText = tr("Duplicate Group - %1 images").arg(group.images.size());
-        m_items.push_back(header);
-        
-        ResultListItem rowItem;
-        rowItem.type = ResultListItem::ImageRow;
-        rowItem.groupId = groupId;
-        for (const auto& img : group.images) {
-            rowItem.images.push_back(img);
-            if (rowItem.images.size() == 4) {
+            ResultListItem header;
+            header.type = ResultListItem::Header;
+            header.groupId = groupId;
+            header.headerText = tr("Duplicate Group - %1 images").arg(group.images.size());
+            m_items.push_back(header);
+            
+            ResultListItem rowItem;
+            rowItem.type = ResultListItem::ImageRow;
+            rowItem.groupId = groupId;
+            for (const auto& img : group.images) {
+                rowItem.images.push_back(img);
+                if (rowItem.images.size() == 4) {
+                    m_items.push_back(rowItem);
+                    rowItem.images.clear();
+                }
+            }
+            if (!rowItem.images.empty()) {
                 m_items.push_back(rowItem);
-                rowItem.images.clear();
             }
+            groupId++;
         }
-        if (!rowItem.images.empty()) {
-            m_items.push_back(rowItem);
-        }
-        groupId++;
     }
     endResetModel();
 }

+ 4 - 0
translations/dupfind_de.ts

@@ -115,5 +115,9 @@ Sind Sie sicher, dass Sie den Löschvorgang ausführen möchten?</translation>
         <source>Duplicate Group - %1 images</source>
         <translation>Duplikatgruppe - %1 Bilder</translation>
     </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>Keine ähnlichen Bilder gefunden.</translation>
+    </message>
 </context>
 </TS>

+ 4 - 0
translations/dupfind_en.ts

@@ -115,5 +115,9 @@ Are you sure you want to execute the deletion?</translation>
         <source>Duplicate Group - %1 images</source>
         <translation>Duplicate Group - %1 images</translation>
     </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>No similar images found.</translation>
+    </message>
 </context>
 </TS>

+ 4 - 0
translations/dupfind_es.ts

@@ -115,5 +115,9 @@ Si continúa, todos esos archivos de imagen se perderán.
         <source>Duplicate Group - %1 images</source>
         <translation>Grupo de duplicados - %1 imágenes</translation>
     </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>No se encontraron imágenes similares.</translation>
+    </message>
 </context>
 </TS>

+ 4 - 0
translations/dupfind_ja.ts

@@ -115,5 +115,9 @@
         <source>Duplicate Group - %1 images</source>
         <translation>重複グループ - %1 枚の画像</translation>
     </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>類似画像が見つかりませんでした。</translation>
+    </message>
 </context>
 </TS>

+ 4 - 0
translations/dupfind_ko.ts

@@ -115,5 +115,9 @@
         <source>Duplicate Group - %1 images</source>
         <translation>중복 그룹 - %1 개의 이미지</translation>
     </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>유사한 이미지를 찾을 수 없습니다.</translation>
+    </message>
 </context>
 </TS>

+ 4 - 0
translations/dupfind_pl.ts

@@ -115,5 +115,9 @@ Czy na pewno chcesz wykonać usuwanie?</translation>
         <source>Duplicate Group - %1 images</source>
         <translation>Grupa duplikatów - %1 obrazów</translation>
     </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>Nie znaleziono podobnych obrazów.</translation>
+    </message>
 </context>
 </TS>

+ 5 - 1
translations/dupfind_pt.ts

@@ -113,7 +113,11 @@ Tem a certeza de que deseja executar a exclusão?</translation>
     <name>ResultListModel</name>
     <message>
         <source>Duplicate Group - %1 images</source>
-        <translation>Grupo de duplicatas - %1 imagens</translation>
+        <translation>Grupo de duplicadas - %1 imagens</translation>
+    </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>Nenhuma imagem semelhante encontrada.</translation>
     </message>
 </context>
 </TS>

+ 4 - 0
translations/dupfind_ru.ts

@@ -115,5 +115,9 @@
         <source>Duplicate Group - %1 images</source>
         <translation>Группа дубликатов - %1 изображений</translation>
     </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>Похожие изображения не найдены.</translation>
+    </message>
 </context>
 </TS>

+ 4 - 0
translations/dupfind_zh_CN.ts

@@ -115,5 +115,9 @@
         <source>Duplicate Group - %1 images</source>
         <translation>重复组 - %1 个图像</translation>
     </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>未找到相似图像。</translation>
+    </message>
 </context>
 </TS>

+ 4 - 0
translations/dupfind_zh_TW.ts

@@ -115,5 +115,9 @@
         <source>Duplicate Group - %1 images</source>
         <translation>重複組 - %1 張圖片</translation>
     </message>
+    <message>
+        <source>No similar images found.</source>
+        <translation>未找到相似圖像。</translation>
+    </message>
 </context>
 </TS>