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 : }
|