Introduction
Many websites including slions.net do decorate links with favicons from their target. To achieve this on XenForo I've been using that Favicon For Links AddOn. It is simply using a Google service to fetch said favicon. However I grew frustrated at the amount of prominent sites whose favicons are missing from this service including GitHub and decide to do something about.Requirements
We not only wanted to reliably obtain favicons from websites we also wanted to make sure they displayed nicely on our dark themed pages by reverting colours of darkest icons.Ideal solution
We hoped we could come up with a solution based on Ajax, unfortunately CORS security policies on modern browsers won't let you do that, unless both your website and the ones from which you are fetching favicons are setup properly. Therefore, we could implement that solution but failed to deploy it.To disable security in Chrome launch it using the following command on Windows:
chrome.exe --user-data-dir="C://Chrome-dev-session" --disable-web-security
. Only then will you be able to load the following code sample:
HTML:
<!DOCTYPE html>
<html>
<body style="background-color:grey;">
<script type="text/javascript">
// This is the sha1 of the favicon returned by GitHub services when specified domain favicon is not available
const KDefaultFaviconHashGitHub = 'e5c76d46c49e27fef1f614a3459cc390f0a37f1c';
// This is the sha1 of the favicon returned by Google services when specified domain favicon is not available
const KDefaultFaviconHashGoogle = 'b82d0979d555bd137b33c15021129e06cbeea59a';
//
const KRequestFaviconGitHub = 'https://favicons.githubusercontent.com/';
const KRequestFaviconGoogle = 'https://www.google.com/s2/favicons?domain=';
document.addEventListener('DOMContentLoaded', function(event) {
addFavicon("stackoverflow.com");
addFavicon("bbc.co.uk");
addFavicon("github.com");
addFavicon("theregister.co.uk");
addFavicon("slions.net");
addFavicon("alternate.de");
addFavicon("amazon.de");
addFavicon("microsoft.com");
addFavicon("apple.com");
addFavicon("googlesource.com");
addFavicon("android.googlesource.com");
addFavicon("firebase.google.com");
addFavicon("play.google.com");
addFavicon("google.com");
addFavicon("team-mediaportal.com");
addFavicon("caseking.de");
addFavicon("developer.mozilla.org");
addFavicon("theguardian.com");
addFavicon("niche-beauty.com");
addFavicon("octobre-editions.com");
addFavicon("dw.com");
addFavicon("douglas.com");
addFavicon("douglas.de");
addFavicon("www.sncf.fr");
addFavicon("paris.fr");
addFavicon("bahn.de");
addFavicon("hopfully.that.domain.does.not.exists.nowaythisisavaliddomain.fart");
});
/**
*
*/
function addFavicon(aDomain)
{
var a = document.createElement("a");
a.href = "http://" + aDomain;
//a.style.display = "block";
var img = document.createElement("img");
var div = document.createElement("div");
div.innerText = aDomain;
div.style.verticalAlign = "middle";
div.style.display = "inline-block";
img.style.width = "16px";
img.style.height = "16px";
img.style.verticalAlign = "middle";
img.style.display = "inline-block";
img.style.marginRight = "4px";
a.appendChild(img);
a.appendChild(div);
document.body.appendChild(a);
document.body.appendChild(document.createElement("p"));
downloadFavicon(aDomain,img,a);
}
/**
* Set downloaded favicon as source to the given image.
*/
function downloadFavicon(aDomain,aImage)
{
var img = document.createElement("img");
img.onload = function () {
// Invert image if it is too dark, usefull for dark web sites
isImageDark(img.src,function (isDark)
{
if (isDark)
{
aImage.style.filter = "invert(1)";
}
aImage.src = img.src;
});
}
// First try GitHub API
downloadBlob(KRequestFaviconGitHub+aDomain, async function response(e)
{
var urlCreator = window.URL || window.webkitURL;
const gitHubResponse = this.response;
computeHash(gitHubResponse, function (aHash) {
//console.log(hash);
//console.log(KDefaultFaviconHashGitHub);
if (aHash!==KDefaultFaviconHashGitHub)
{
// We got a valid favicon, use it then
img.src = urlCreator.createObjectURL(gitHubResponse);
return;
}
// No valid favicon found using GitHub API, fallback to Goolge API
downloadBlob(KRequestFaviconGoogle+aDomain, async function response(e)
{
const googleResponse = this.response;
computeHash(googleResponse, function (aHash) {
if (aHash!==KDefaultFaviconHashGoogle)
{
// We got a valid favicon, use it then
img.src = urlCreator.createObjectURL(googleResponse);
return;
}
// Neither GitHub nor Google have a favicon
// Try it all over again without subdomain maybe, that notably tackles the case of android.googlesource.com
var matches = aDomain.match(/^(.+?)\.(.+\..+)/i);
//console.log(matches);
var domain = matches && matches[2]; // domain will be null if no match is found
if (domain==null)
{
// Still no valid favicon, use it anyway
img.src = urlCreator.createObjectURL(googleResponse);
return;
}
// Have another go at it after poping latest subdomain
downloadFavicon(domain,aImage);
});
});
});
});
}
/**
* Download given URL as blob.
*/
function downloadBlob(aUrl,aOnload)
{
var xhr = new XMLHttpRequest();
xhr.open("GET", aUrl);
xhr.responseType = "blob";
xhr.onload = aOnload;
xhr.send();
}
/**
* Compute hash for given string and return it as hex string.
*/
function computeHash(blob,aCallBack) {
var a = new FileReader();
a.readAsArrayBuffer(blob);
a.onloadend = async function () {
const hashBuffer = await crypto.subtle.digest("SHA-1", a.result);
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
//document.getElementById("h1id").innerHTML = hashHex;
aCallBack(hashHex);
};
}
/**
* See: https://stackoverflow.com/a/13766539/3969362
*/
function isImageDark(imageSrc,callback) {
// Adjust this between 0 and 1 where 0 is always dark and 1 is never dark
// 0.3 was flagging stackoverflow.com and team-mediaportal as dark 0.4 was is not
// 0.8 is flagging bbc.co.uk as dark and 0.9 is not
// 0.4 is flagging developer.mozilla.org as dark 0.5 was is not
var fuzzy = 0.9;
var img = document.createElement("img");
img.src = imageSrc;
img.style.display = "none";
document.body.appendChild(img);
img.onload = function() {
// create canvas
var canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(this,0,0);
var imageData = ctx.getImageData(0,0,canvas.width,canvas.height);
var data = imageData.data;
var r,g,b, max_rgb;
var light = 0, dark = 0;
for(var x = 0, len = data.length; x < len; x+=4) {
r = data[x];
g = data[x+1];
b = data[x+2];
max_rgb = Math.max(Math.max(r, g), b);
if (max_rgb < 128)
dark++;
else
light++;
}
var dl_diff = ((light - dark) / (this.width*this.height));
if (dl_diff + fuzzy < 0)
callback(true); /* Dark. */
else
callback(false); /* Not dark. */
}
}
</script>
</body>
</html>
That solution would have been great as it does not require any manual configuration here is how it works:
- Check if favicon is available on GitHub.
- Check if favicon is available on Google.
- If favicon is still not available pop subdomain if any and try again, otherwise just give up and use default favicon from Google.
- Check if selected favicon is too dark and revert its colours if needed.
img
elements to download icons instead of XMLHttpRequest
but that would not bypass CORS security policies as canvas
element created from cross origin img
are marked as tainted and do not let you access image data.Fall-back solution
Seeing as we could not deploy our favourite solution he had to come up with something that worked and still fitted our requirements. We now rely on a predefined list of hostnames to point us to the favicon download link and tell us if it needs to be inverted. Unknown hostnames are just using Google services.
HTML:
<!DOCTYPE html>
<html>
<body style="background-color:grey;">
<script type="text/javascript">
const KRequestFaviconGitHub = 'https://favicons.githubusercontent.com/';
const KRequestFaviconGoogle = 'https://www.google.com/s2/favicons?domain=';
const KDefaultUrl = KRequestFaviconGoogle;
// We rely on pre-defined hostname configurations
const hostnames = {
"stackoverflow.com": { url:KRequestFaviconGoogle+"stackoverflow.com", invert:0 },
"theregister.co.uk": { url:KRequestFaviconGoogle+"theregister.co.uk", invert:1 },
"github.com": { url:KRequestFaviconGitHub+"github.com", invert:1 },
"android.googlesource.com": { url:KRequestFaviconGoogle+"googlesource.com", invert:0 },
"developer.android.com": { url:KRequestFaviconGitHub+"developer.android.com", invert:0 }
};
document.addEventListener('DOMContentLoaded', function(event) {
addFavicon("stackoverflow.com");
addFavicon("bbc.co.uk");
addFavicon("github.com");
addFavicon("theregister.co.uk");
addFavicon("developer.android.com");
addFavicon("android-doc.github.io");
addFavicon("slions.net");
addFavicon("alternate.de");
addFavicon("amazon.de");
addFavicon("microsoft.com");
addFavicon("apple.com");
addFavicon("googlesource.com");
addFavicon("android.googlesource.com");
addFavicon("firebase.google.com");
addFavicon("play.google.com");
addFavicon("google.com");
addFavicon("team-mediaportal.com");
addFavicon("caseking.de");
addFavicon("developer.mozilla.org");
addFavicon("theguardian.com");
addFavicon("niche-beauty.com");
addFavicon("octobre-editions.com");
addFavicon("dw.com");
addFavicon("douglas.com");
addFavicon("douglas.de");
addFavicon("www.sncf.fr");
addFavicon("paris.fr");
addFavicon("bahn.de");
addFavicon("hopfully.that.domain.does.not.exists.nowaythisisavaliddomain.fart");
});
/**
*
*/
function addFavicon(aDomain)
{
var a = document.createElement("a");
a.href = "http://" + aDomain;
//a.style.display = "block";
var div = document.createElement("div");
div.innerText = aDomain;
div.style.verticalAlign = "middle";
div.style.display = "inline-block";
var img = document.createElement("img");
img.className = "link-favicon";
img.style.width = "16px";
img.style.height = "16px";
img.style.verticalAlign = "middle";
img.style.display = "inline-block";
img.style.marginRight = "4px";
a.prepend(img);
a.appendChild(div);
document.body.appendChild(a);
document.body.appendChild(document.createElement("p"));
const conf = hostnames[aDomain]
if (conf==null)
{
img.src = KDefaultUrl+aDomain;
}
else
{
img.src = conf.url;
img.style.filter = "invert(" + conf.invert + ")";
}
}
</script>
</body>
</html>
Conclusion
We implemented our fall-back solution in a modified version of that XenForo AddOn.Resources
Last edited: