LCOV - code coverage report
Current view: top level - src/utilities - FaviconGrabber.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 86.5 % 111 96
Test Date: 2026-04-19 00:35:54 Functions: 85.7 % 14 12

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

Generated by: LCOV version 2.0-1