Aux couleurs du site 11


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 !

11 thoughts on “Aux couleurs du site

Leave a comment

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.