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