LCOV - code coverage report
Current view: top level - src/utilities - FaviconGrabber.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 86.4 % 110 95
Test Date: 2026-03-23 10:19:47 Functions: 85.7 % 14 12

            Line data    Source code
       1              : #include "FaviconGrabber.h"
       2              : #include "../network/BatchDownloadCore.h"
       3              : 
       4              : #include <QString>
       5              : #include <QStringList>
       6              : #include <QImage>
       7              : #include <QXmlStreamReader>
       8              : 
       9              : #include "NetworkUtilities.h"
      10              : #include "FangLogging.h"
      11              : 
      12            4 : FaviconGrabber::FaviconGrabber(QObject *parent, QNetworkAccessManager* networkManager) :
      13              :     FangObject(parent),
      14            4 :     batchDownloader(new BatchDownloadCore(15000, 10, this, networkManager)),
      15            8 :     webGrabber(true, 5000, this, networkManager)
      16              : {
      17              :     // Set up our state machine.
      18            8 :     machine.addStateChange(START, WEB_GRABBER, [this]() { onWebGrabber(); });
      19            8 :     machine.addStateChange(WEB_GRABBER, CHECK_ICONS, [this]() { onCheckIcons(); });
      20            8 :     machine.addStateChange(CHECK_ICONS, PICK_BEST, [this]() { onPickBest(); });
      21              : 
      22            4 :     machine.addStateChange(-1, GRAB_ERROR, [this]() { onError(); }); // Many errors, one slot.
      23              : 
      24              :     // Signals!
      25            4 :     connect(batchDownloader, &BatchDownloadCore::finished, this, &FaviconGrabber::onBatchFinished);
      26            4 :     connect(&webGrabber, &WebPageGrabber::ready, this, &FaviconGrabber::onWebGrabberReady);
      27            4 : }
      28              : 
      29            4 : void FaviconGrabber::find(const QUrl &url)
      30              : {
      31            4 :     urlsToCheck.clear();
      32            4 :     imagesToCheck.clear();
      33            4 :     location = url;
      34            4 :     machine.start(START);
      35              : 
      36              :     // Make a list of "root" favicons.
      37            4 :     QUrl host = NetworkUtilities::getHost(location);
      38           24 :     const QStringList extensions{"ico", "jpg", "jpeg", "png", "gif"};
      39              : 
      40              :     // Add each extension to our list.
      41           24 :     for (const QString& ext : extensions) {
      42           20 :         QUrl toCheck(host);
      43           20 :         toCheck.setPath("/favicon." + ext);
      44           20 :         urlsToCheck << toCheck;
      45           20 :     }
      46              : 
      47            4 :     machine.setState(WEB_GRABBER);
      48            8 : }
      49              : 
      50            4 : void FaviconGrabber::onWebGrabber()
      51              : {
      52              :     // Check for favicons embedded in the HTML.
      53              :     // We look at the main page rather than the feed.
      54            4 :     webGrabber.load(NetworkUtilities::getHost(location));
      55            4 : }
      56              : 
      57            4 : void FaviconGrabber::onCheckIcons()
      58              : {
      59            4 :     if (urlsToCheck.isEmpty()) {
      60            0 :         machine.setState(GRAB_ERROR);
      61            0 :         return;
      62              :     }
      63              : 
      64              :     // Start batch download of all favicon candidates.
      65            4 :     batchDownloader->download(urlsToCheck);
      66              : }
      67              : 
      68            4 : void FaviconGrabber::onBatchFinished()
      69              : {
      70              :     // Ignore if we've moved past CHECK_ICONS state.
      71            4 :     if (machine.getState() != CHECK_ICONS) {
      72            0 :         return;
      73              :     }
      74              : 
      75              :     // Process batch results.
      76            4 :     QMap<QUrl, BatchDownloadResult> results = batchDownloader->results();
      77              : 
      78           28 :     for (auto it = results.constBegin(); it != results.constEnd(); ++it) {
      79           24 :         const BatchDownloadResult& result = it.value();
      80              : 
      81           24 :         if (result.success && !result.data.isEmpty()) {
      82            4 :             QImage img;
      83            4 :             if (img.loadFromData(result.data)) {
      84            8 :                 qCDebug(logFavicon) << "Successfully loaded image:" << img.width() << "x" << img.height();
      85            4 :                 imagesToCheck << QPair<QUrl, QImage>(it.key(), img);
      86              :             } else {
      87            0 :                 qCDebug(logFavicon) << "Failed to load image from data";
      88              :             }
      89            4 :         }
      90              :     }
      91              : 
      92            4 :     machine.setState(PICK_BEST);
      93            4 : }
      94              : 
      95            4 : void FaviconGrabber::onPickBest()
      96              : {
      97            4 :     if (imagesToCheck.isEmpty()) {
      98            0 :         machine.setState(GRAB_ERROR);
      99            4 :         return;
     100              :     }
     101              : 
     102            4 :     int topTotalPixels = 0;
     103            4 :     QImage topImage;
     104              : 
     105              :     // Go over all the images. Find the one with the max total pixels.
     106            8 :     for (const auto& pair : std::as_const(imagesToCheck)) {
     107            4 :         const QImage& img = pair.second;
     108            4 :         int totalPixels = img.width() * img.height();
     109            4 :         if (totalPixels > topTotalPixels) {
     110            4 :             topTotalPixels = totalPixels;
     111            4 :             topImage = img;
     112              :         }
     113              :     }
     114              : 
     115            4 :     if (topTotalPixels > 0) {
     116            4 :         QString dataUri = imageToDataUri(topImage);
     117            4 :         if (!dataUri.isEmpty()) {
     118            4 :             emit finished(dataUri);
     119            4 :             return;
     120              :         }
     121            4 :     }
     122              : 
     123            0 :     machine.setState(GRAB_ERROR);
     124            4 : }
     125              : 
     126            0 : void FaviconGrabber::onError()
     127              : {
     128            0 :     emit finished(QString()); // empty string indicates failure
     129            0 : }
     130              : 
     131            4 : QString FaviconGrabber::imageToDataUri(const QImage& image)
     132              : {
     133            4 :     if (image.isNull()) {
     134              :         // Not much we can do here.
     135            0 :         return "";
     136              :     }
     137              : 
     138            4 :     QImage finalImage = image;
     139              : 
     140              :     // Scale down large images. Shouldn't happen, but we want to be defensive.
     141            4 :     if (image.width() > MAX_FAVICON_DIMENSION || image.height() > MAX_FAVICON_DIMENSION) {
     142            0 :         finalImage = image.scaled(MAX_FAVICON_DIMENSION, MAX_FAVICON_DIMENSION,
     143            0 :                                   Qt::KeepAspectRatio, Qt::SmoothTransformation);
     144              :     }
     145              : 
     146              :     // Convert to PNG and base64 encode.
     147            4 :     QByteArray imageData;
     148            4 :     QBuffer buffer(&imageData);
     149            4 :     buffer.open(QIODevice::WriteOnly);
     150            4 :     finalImage.save(&buffer, "PNG");
     151              : 
     152            4 :     return "data:image/png;base64," + QString::fromLatin1(imageData.toBase64());
     153            4 : }
     154              : 
     155            4 : void FaviconGrabber::onWebGrabberReady(WebPageGrabber* grabber, QString *document)
     156              : {
     157              :     Q_UNUSED(grabber);
     158              : 
     159              :     // Ignore responses that arrive after we've already moved past WEB_GRABBER state
     160              :     // (e.g., multiple async responses from favicon URLs being parsed as HTML)
     161            4 :     if (machine.getState() != WEB_GRABBER) {
     162            0 :         return;
     163              :     }
     164              : 
     165              :     // Could indicate no internet.
     166            4 :     if (document == nullptr || document->isEmpty()) {
     167            0 :         machine.setState(CHECK_ICONS);
     168              : 
     169            0 :         return;
     170              :     }
     171              : 
     172            4 :     findIcons(*document);
     173              : 
     174            4 :     machine.setState(CHECK_ICONS);
     175              : }
     176              : 
     177            4 : void FaviconGrabber::findIcons(const QString& document)
     178              : {
     179              :     // Examples of what we're looking for:
     180              :     //     <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
     181              :     //     <link rel="icon" href="/favicon.ico" type="image/x-icon" />
     182              :     //     <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
     183              : 
     184            4 :     QXmlStreamReader xml;
     185            4 :     xml.addData(document);
     186              : 
     187           80 :     while (!xml.atEnd()) {
     188           76 :         xml.readNext();
     189              : 
     190           76 :         if (xml.isStartElement()) {
     191           24 :             QString tagName = xml.name().toString().toLower();
     192           24 :             if (tagName == "body") {
     193              :                 // We're done with the header, so bail.
     194            4 :                 return;
     195              :             }
     196              : 
     197           20 :             if (tagName == "link") {
     198            4 :                 QXmlStreamAttributes attributes = xml.attributes();
     199            8 :                 if (attributes.hasAttribute("rel") && attributes.hasAttribute("href")) {
     200            4 :                     QString rel = attributes.value("", "rel").toString().toLower();
     201            4 :                     if (rel == "apple-touch-icon" || rel == "icon" || rel == "shortcut icon") {
     202            8 :                         urlsToCheck << QUrl(attributes.value("", "href").toString());
     203              :                     }
     204            4 :                 }
     205            4 :             }
     206           24 :         }
     207              :     }
     208            4 : }
        

Generated by: LCOV version 2.0-1