On est en train de tweaker un peu multiboards.net Max et moi, et il a voulu ajouter un feature rigolote : les flux sont affichés avec une palette de couleurs proche de celle du site.
Comme récupérer les couleurs d’un site est compliqué, on s’est rabattus sur les couleurs du favicon, qui en théorie doit résumer l’identité d’un site. Ce n’est pas parfait, mais c’est plus simple à implémenter.
Au début, on avait écrit une solution avec Pillow et numpy. Ça marchait très bien, mais des extensions compilées juste pour faire de la déco c’était un peu lourd, surtout si un jour on libère le code source (mais vu comme les sources sont dégueues et qu’on a pas de doc, pour le moment c’est pas gagné).
On a donc changé notre fusil d’épaule, et on a choisit une nouvelle technique : faire la moitié du taff côté client.
Récupérer le favicon côté serveur
On ne peut pas faire des requêtes cross domain en JS, donc on doit toujours récupérer l’image côté serveur. get_favicon_url()
parse le site à coup de regex (c’est mal mais ça évite de rajouter BeautifulSoup en dépendance pour une balise) pour récupérer son URL sur la page donnée, ou à défaut, sur la page à la racine du site. On se donne plusieurs essais en cas d’erreurs de réseau :
import urllib import urlparse def get_favicon_url(url, retry=3, try_home_page=True): """ Try to find a favicon url on the given page """ parsed_url = urlparse.urlparse(url) url_root = "%s://%s" % (parsed_url.scheme, parsed_url.netloc) try: # try to get it using a regex on the current URL html = fetch_url(url, retry=retry) html = html.decode('ascii', errors='ignore') pattern = r""" href=(?:"|')\s* (?P<favicon>[^\s'"]*favicon.[a-zA-Z]{3,4}) \s*(?:"|') """ match = re.search(pattern, html, re.U|re.VERBOSE) favicon_url = match.groups()[0] except IOError: # this is a network error so eventually, let it crash if not try_home_page: raise # try with the home page, maybe this one is accessible favicon_url = get_favicon_url(url_root, retry=retry, try_home_page=False) except (IndexError, AttributeError): # url is not on this page, try the home page if try_home_page: return get_favicon_url(url_root, retry=retry, try_home_page=False) # can't find the favicon url, default to standard url favicon_url = '/favicon.ico' # make sure to have the domain of the original website in the favicon url if url_root not in favicon_url: favicon_url = "%s/%s" % (url_root, favicon_url.lstrip('/')) return favicon_url def fetch_favicon(url, retry=3): """ Returns the bytes of the favicon of this site """ favicon_url = get_favicon_url(url, retry=retry) return fetch_url(favicon_url, retry=retry) |
fetch_favicon()
télécharge l’image proprement dite et retourne les bits tels quels. Notez que tout ça est synchrone et bloquant, mais heureusement le traffic de multiboards est sans doute trop faible pour poser problème.
Exposer le résultat au client
Ensuite il faut faire le lien entre le client et le serveur. On fait donc un petit routing (c’est du bottle) qui va retourner tout ça en base64 afin qu’on puisse l’utiliser directement dans un objet Image
JS :
import base64 from bottle import post, HTTPError @post('/favicon') def fetch_favicon_base64(): """ Return the favicon URL from a website """ try: url = request.POST['url'] except KeyError: raise HTTPError(400, "You must pass a site URL") try: # return favicon as base64 url to be easily included in # a img tag favicon = fetch_favicon(url) return b'data:image/x-icon;base64,' + base64.b64encode(favicon) except (IOError): raise HTTPError(400, "Unable to find any favicon URL") |
Récupérer la palette côté client
Avec un peu d’AJAX sur notre vue, on récupère les bits de l’image, et on fout le tout dans un objet Image
qu’on passe à la lib ColorThief. Cette dernière nous renvoie un array avec la palette. On convertit tout ça en code hexadécimal, et on diminue la luminosité de certaines couleurs pour rendre le texte plus lisible.
$.post('/favicon', {url: url}).done(function(data){ // load an image with the base64 data of the favicon var image = new Image; image.src = data; image.onload = function() { // ask ColorThief for the main favicon colors var colorThief = new ColorThief(); var bc = colorThief.getPalette(image); // some tools to convert the RGB array into // rgb hex string function componentToHex(c) { var hex = c.toString(16); return hex.length == 1 ? "0" + hex : hex; } function rgbToHex(array) { return componentToHex(array[0]) + componentToHex(array[1]) + componentToHex(array[2]); } // make the color brigther function increase_brightness(hex, percent){ // convert 3 char codes --> 6, e.g. `E0F` --> `EE00FF` if(hex.length == 3){ hex = hex.replace(/(.)/g, '$1$1'); } var r = parseInt(hex.substr(0, 2), 16), g = parseInt(hex.substr(2, 2), 16), b = parseInt(hex.substr(4, 2), 16); return '' + ((0|(1<<8) + r + (256 - r) * percent / 100).toString(16)).substr(1) + ((0|(1<<8) + g + (256 - g) * percent / 100).toString(16)).substr(1) + ((0|(1<<8) + b + (256 - b) * percent / 100).toString(16)).substr(1); } /* Define board colors */ var header = '#' + rgbToHex(bc[0]); var odd = '#' + increase_brightness(rgbToHex(bc[1]), 90); var even = '#' + increase_brightness(rgbToHex(bc[2]), 90); }); |
Est-ce que la feature est utile ? Je ne sais pas, c’est un peu gadget, mais c’était marrant à faire, et c’est vrai qu’on identifie bien chaque site sur la page du coup.
L’idée ici est que le serveur et le client travaillent en équipe, et que grâce au navigateur on a une stack de traitement image/audio/video à portée de code. D’ailleurs on a viré le plugin flash des radios pour le remplacer par du chteumeuleu 5.
On peut imaginer la même chose avec tous les outils côtés en JS, ceci incluant les préprocesseurs et minifieurs JS ou CSS : pas besoin d’installer Node pour faire tourner tout ça, il suffit de déléguer le boulot à un tab du browser. Pour améliorer la stack, on pourrait par ailleurs utiliser du RPC avec WAMP et même prévenir le navigateur quand un fichier est prêt avec PUB/SUB afin qu’il le recharge. Vous avez vu comme j’arrive toujours à placer crossbar ?
J’ai un pote qui va venir dans quelques jours pour creuser cette idée qui permettrait d’avoir en pur Python une sorte de livereload sans extension, sans gros GUI, qui transpile tout et rafraichit tout avec un simple fichier de config. Peut être aurons nous le temps de faire un proto.
C’était l’inspiration du jour, passez une excellente canicule !
Un article comme d’hab : stylé ! Le projet du livereload like serait vraiment cool !
Pour être aux couleurs du site, vous pouvez pas travailler sur une screenshot de celui-ci ?
http://stackoverflow.com/questions/1197172/how-can-i-take-a-screenshot-image-of-a-website-using-python
Tout ça demande des extensions en C.
2 petites coquilles se sont glissées dans le texte :
sources sonts dégueus -> sources sont dégueus
le traffic de multiboards sans doute -> le traffic de multiboards est sans doute
Sinon sympa comme solution :)
Merci pour l’article.
La couleur est générée à chaque fois du coup ? Ou vous mettez ça en cache d’une manière ou d’une autre ?
On enregistre les boards et leurs couleurs dans une DB, ça évite de se taper le process à chaque fois.
Une coquille : on viré le plugin flash -> on viré a le plugin flash
Et pour le coup c’est super de ne plus avoir ce &é”ç_’&é” de Flash… :-P
Toujours excellents ce genre de billets. Sam, tu dois déjà le savoir, mais tu t’es pas trompé de vocation en faisant de la formation. Tes élèves doivent vraiment kiffer d’avoir un prof comme toi. Être un vrai pédagogue c’est une valeur très rare.
Abject => Merci, c’est corrigé (je te met pas de tampon, je sais pas faire).
@Abject avec firefox qui le bloque par défaut, c’est un peu le signal d’alarme pour le virer.
Les favicons ne sont pas seulement nommées favicon.
est une possibilité parmi d’autres. Voir https://html.spec.whatwg.org/multipage/semantics.html#rel-icon
Chercher link[rel] pour les valeurs de rel ayant icon
si sizes existe prendre la plus faible
en fallback rechercher /favicon
Attention certaines images sont au format SVG.
Tout à fait, mais ces cas sont très rares. Du coup plutôt que de traiter la specs en entier pour un site sur 100, on a préféré faire du pareto et avoir un fallback sur du gris pour les exceptions.