Maurice Bloggue

Aller au contenu | Aller au menu | Aller à la recherche

vendredi, juin 19 2009

Face detection in pure PHP (without OpenCV)

Une résumé en français est disponible en fin d'article.

Lately, I've been looking for ways to detect faces in photos with PHP. Nowadays, face detection is built in many consumer products (camera obviously, but also Google and iPhoto), and seems to be a pretty common job. So I expected to find many solutions for doing it with PHP. Surprisingly, the only one I could find is OpenCV, an opensource lib that was originally developed by Intel. OpenCV seems to perform well but you need to be able to install it on your server. In my case, I wanted to have a pure PHP solution, so it can work with most hosts.

Learning about face detection

So I started to think about implementing it myself. I read a articles, scientific papers, etc. The website http://www.facedetection.com/ is a great resource by the way. From this short research, I learnt that one of the most popular solutions is to use Viola Jones training with a Haar classifier. Very informative, but tedious to implement. I'm lazy, you know.

Always look at what others are doing

Then I looked for existing implementations in other languages. Let's say Flash and Javascript. With Canvas, Javascript developers will certainly look at what flash developers do. There seem to be a few libs for face detection in AS3. They even work in real time. Pretty cool. I kept searching and finally found a canvas+javascript implementation of face detection at http://blog.kpicturebooth.com/?p=8. The code looked fairly compact and simple. Shouldn't be hard to port to PHP.

The code

Once the code converted to PHP, here's the result:

<?php
// 
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.     
// 
// @Author Karthik Tharavaad 
//         karthik_tharavaad@yahoo.com
// @Contributor Maurice Svay
//              maurice@svay.Com

class Face_Detector {
    
    protected $detection_data;
    protected $canvas;
    protected $face;
    private $reduced_canvas;
    
    public function __construct($detection_file = 'detection.dat') {
        if (is_file($detection_file)) {
            $this->detection_data = unserialize(file_get_contents($detection_file));
        } else {
            throw new Exception("Couldn't load detection data");
        }
        //$this->detection_data = json_decode(file_get_contents('data.js'));
    }
    
    public function face_detect($file) {
        if (!is_file($file)) {
            throw new Exception("Can not load $file");
        }
        
        $this->canvas = imagecreatefromjpeg($file);
        $im_width = imagesx($this->canvas);
        $im_height = imagesy($this->canvas);

        //Resample before detection?
        $ratio = 0;
        $diff_width = 320 - $im_width;
        $diff_height = 240 - $im_height;
        if ($diff_width > $diff_height) {
            $ratio = $im_width / 320;
        } else {
            $ratio = $im_height / 240;
        }

        if ($ratio != 0) {
            $this->reduced_canvas = imagecreatetruecolor($im_width / $ratio, $im_height / $ratio);
            imagecopyresampled($this->reduced_canvas, $this->canvas, 0, 0, 0, 0, $im_width / $ratio, $im_height / $ratio, $im_width, $im_height);
            
            $stats = $this->get_img_stats($this->reduced_canvas);
            $this->face = $this->do_detect_greedy_big_to_small($stats['ii'], $stats['ii2'], $stats['width'], $stats['height']);
            $this->face['x'] *= $ratio;
            $this->face['y'] *= $ratio;
            $this->face['w'] *= $ratio;
        } else {
            $stats = $this->get_img_stats($this->canvas);
            $this->face = $this->do_detect_greedy_big_to_small($stats['ii'], $stats['ii2'], $stats['width'], $stats['height']);
        }
        return ($this->face['w'] > 0);
    }
    
    
    public function toJpeg() {
        $color = imagecolorallocate($this->canvas, 255, 0, 0); //red
        imagerectangle($this->canvas, $this->face['x'], $this->face['y'], $this->face['x']+$this->face['w'], $this->face['y']+ $this->face['w'], $color);
        header('Content-type: image/jpeg');
        imagejpeg($this->canvas);
    }
    
    public function toJson() {
        return "{'x':" . $this->face['x'] . ", 'y':" . $this->face['y'] . ", 'w':" . $this->face['w'] . "}";
    }
    
    public function getFace() {
        return $this->face;
    }
    
    protected function get_img_stats($canvas){
        $image_width = imagesx($canvas);
        $image_height = imagesy($canvas);     
        $iis =  $this->compute_ii($canvas, $image_width, $image_height);
        return array(
            'width' => $image_width,
            'height' => $image_height,
            'ii' => $iis['ii'],
            'ii2' => $iis['ii2']
        );         
    }
    
    protected function compute_ii($canvas, $image_width, $image_height ){
        $ii_w = $image_width+1;
        $ii_h = $image_height+1;
        $ii = array();
        $ii2 = array();      
                                
        for($i=0; $i<$ii_w; $i++ ){
            $ii[$i] = 0;
            $ii2[$i] = 0;
        }                        
                                    
        for($i=1; $i<$ii_w; $i++ ){  
            $ii[$i*$ii_w] = 0;       
            $ii2[$i*$ii_w] = 0; 
            $rowsum = 0;
            $rowsum2 = 0;
            for($j=1; $j<$ii_h; $j++ ){
                $rgb = ImageColorAt($canvas, $j, $i);
                $red = ($rgb >> 16) & 0xFF;
                $green = ($rgb >> 8) & 0xFF;
                $blue = $rgb & 0xFF;
                $grey = ( 0.2989*$red + 0.587*$green + 0.114*$blue )>>0;  // this is what matlab uses
                $rowsum += $grey;
                $rowsum2 += $grey*$grey;
                
                $ii_above = ($i-1)*$ii_w + $j;
                $ii_this = $i*$ii_w + $j;
                
                $ii[$ii_this] = $ii[$ii_above] + $rowsum;
                $ii2[$ii_this] = $ii2[$ii_above] + $rowsum2;
            }
        }
        return array('ii'=>$ii, 'ii2' => $ii2);
    }
    
    protected function do_detect_greedy_big_to_small( $ii, $ii2, $width, $height ){
        $s_w = $width/20.0;
        $s_h = $height/20.0;
        $start_scale = $s_h < $s_w ? $s_h : $s_w;
        $scale_update = 1 / 1.2;
        for($scale = $start_scale; $scale > 1; $scale *= $scale_update ){
            $w = (20*$scale) >> 0;
            $endx = $width - $w - 1;
            $endy = $height - $w - 1;
            $step = max( $scale, 2 ) >> 0;
            $inv_area = 1 / ($w*$w);
            for($y = 0; $y < $endy ; $y += $step ){
                for($x = 0; $x < $endx ; $x += $step ){
                    $passed = $this->detect_on_sub_image( $x, $y, $scale, $ii, $ii2, $w, $width+1, $inv_area);
                    if( $passed ) {
                        return array('x'=>$x, 'y'=>$y, 'w'=>$w);
                    }
                } // end x
            } // end y
        }  // end scale
        return null;
    }
    
    protected function detect_on_sub_image( $x, $y, $scale, $ii, $ii2, $w, $iiw, $inv_area){
        $mean = ( $ii[($y+$w)*$iiw + $x + $w] + $ii[$y*$iiw+$x] - $ii[($y+$w)*$iiw+$x] - $ii[$y*$iiw+$x+$w]  )*$inv_area;
        $vnorm =  ( $ii2[($y+$w)*$iiw + $x + $w] + $ii2[$y*$iiw+$x] - $ii2[($y+$w)*$iiw+$x] - $ii2[$y*$iiw+$x+$w]  )*$inv_area - ($mean*$mean);    
        $vnorm = $vnorm > 1 ? sqrt($vnorm) : 1;
        
        $passed = true;
        for($i_stage = 0; $i_stage < count($this->detection_data); $i_stage++ ){
            $stage = $this->detection_data[$i_stage];  
            $trees = $stage[0];  

            $stage_thresh = $stage[1];
            $stage_sum = 0;
                              
            for($i_tree = 0; $i_tree < count($trees); $i_tree++ ){
                $tree = $trees[$i_tree];
                $current_node = $tree[0];    
                $tree_sum = 0;
                while( $current_node != null ){
                    $vals = $current_node[0];
                    $node_thresh = $vals[0];
                    $leftval = $vals[1];
                    $rightval = $vals[2];
                    $leftidx = $vals[3];
                    $rightidx = $vals[4];
                    $rects = $current_node[1];
                    
                    $rect_sum = 0;
                    for( $i_rect = 0; $i_rect < count($rects); $i_rect++ ){
                        $s = $scale;
                        $rect = $rects[$i_rect];
                        $rx = ($rect[0]*$s+$x)>>0;
                        $ry = ($rect[1]*$s+$y)>>0;
                        $rw = ($rect[2]*$s)>>0;  
                        $rh = ($rect[3]*$s)>>0;
                        $wt = $rect[4];
                        
                        $r_sum = ( $ii[($ry+$rh)*$iiw + $rx + $rw] + $ii[$ry*$iiw+$rx] - $ii[($ry+$rh)*$iiw+$rx] - $ii[$ry*$iiw+$rx+$rw] )*$wt;
                        $rect_sum += $r_sum;
                    } 
                     
                    $rect_sum *= $inv_area;
                         
                    $current_node = null;
                    if( $rect_sum >= $node_thresh*$vnorm ){
                        if( $rightidx == -1 ) 
                            $tree_sum = $rightval;
                        else
                            $current_node = $tree[$rightidx];
                    } else {
                        if( $leftidx == -1 )
                            $tree_sum = $leftval;
                        else
                            $current_node = $tree[$leftidx];
                    }
                } 
                $stage_sum += $tree_sum;
            } 
            if( $stage_sum < $stage_thresh ){
                return false;
            }
        } 
        return true;
    }
}

And you simply use the class this way:

$detector = new Face_Detector('detection.dat');
$detector->face_detect('maurice_svay_150.jpg');
$detector->toJpeg();

Which gives the following result:

Face detection result

The code requires GD and is a bit slow but should work on most PHP servers. You'll also need the data file: http://svay.com/experiences/face-detection/detection.dat. Let me know if you have ideas for improving the code :)

Résumé en français

J'ai récemment cherché une solution pour faire de la détection de visage en PHP. Après avec écarté plusieurs solutions qui ne sont pas facilement utilisable sur la plupart des hébergeurs, j'ai finalement porté du code Javascript+Canvas vers PHP+GD. Le résultat une class PHP qui est un peu lente, mais qui fonctionne sur la plupart des serveurs PHP. Si vous avez des idées pour améliorer le code, n'hésitez-pas!

vendredi, avril 3 2009

Générer des anaglyphes avec javascript et canvas

Il n'y a pas très longtemps, j'ai trouvé des lunettes 3D sur une boîte de Chocapics. Ca m'a rappelé les essais d'images en 3D que je faisais il y a quelques années. La recette est super simple: on prend deux images, une pour l'oeil gauche et l'autre pour l'oeil droit. De l'une, on prend la composante rouge et de l'autre, la composante cyan (bleu+vert). On mélange le tout et... paf! Ca fait un anaglyphe!

Ayant joué récemment avec Javascript et l'élément canvas, je me suis dit que ça pourrait se faire facilement avec ces deux technologies. Après quelques essais, j'ai obtenu un Anaglyph generator qui fonctionne à peu près avec un Firefox, un Safari ou un Opera récent. Concernant Internet Explorer, aucune chance pour le moment: excanvas ne supporte pas encore getImageData ni putImageData.

Pour générer un anaglyphe, il suffit d'indiquer deux URLs d'images et de presser le bouton. Pour simplifier, le resultat est toujours affiché en 640x480 (N'hésitez pas à poster l'URL de votre anaglyphe dans les commentaires!).

Anaglyphe

Ceux que ça intéresse, peuvent jeter un oeil au code. C'est plutôt simple:

  • on charge deux images;
  • on attend qu'elles soient complètement chargées;
  • on crée deux canvas pour y copier les images;
  • on remplace la composante rouge d'un canvas par la composante rouge de l'autre;
  • et on affiche le résultat!

Les pièges à éviter

Même si la technique est simple, il y'a tout de même quelques pièges à contourner.

Le premier est la politique de sécurité du navigateur. Comme pour les requêtes XMLHttpRequest, le navigateur ne permet d'accéder aux pixel que des images du même domaine. Du coup, pour pouvoir afficher une image venant d'ailleurs, il faut passer un proxy.

Voilà à quoi ressemble le proxy:

<?php
if (isset($_GET['url']) && preg_match('/^http/i', $_GET['url'])) {
    $url = $_GET['url'];
    if (preg_match('/.png$/i', $url)) {
        header('Content-type: image/png');
    }elseif (preg_match('/.jpg$/i', $url)) {
        header('Content-type: image/jpg');
    }
    readfile($url);
}

Le second, est qu'il faut s'assurer que les images sont bien chargées. En local, ça va forcément très vite et on n'a jamais de problème. Avec des images distantes, ça peut mettre un peu de temps et empêcher le script de fonctionner. Pour résoudre ce problème, deux trucs:

  • utiliser setTimeout pour attendre en boucle que les images soient chargées. La fonction Anaglyph.wait vérifie si les images sont totalement chargée en regardant l'attribut complete. Si ce n'est pas le cas, elle va attendre un peu et s'appeler elle-même à nouveau. Une fois que les deux images sont chargées, elle peut appeler Anaglyph.make qui va générer l'anaglyphe.
  • imbriquer le code qui manipules les canvas dans un try/catch pour éviter que le code ne casse.

Si l'élément canvas vous intéresse, il y'a déjà pas mal de ressources intéressantes sur le sujet:

Plein de choses intéressantes ont déjà été faites avec canvas: de la 3D, de la visualisation de musique, de la retouche d'images, des trucages vidéos, etc. Il y'a sûrement encore plein d'autres choses à faire. De quoi me faire réfléchir toute la nuit encore...

mardi, mars 24 2009

Concours Mozilla / Dotclear

Du 23 mars au 16 mai 2009 se déroule un concours de thèmes Dotclear2, organisé conjointement par Mozilla Europe, Dotclear et DotAddict.org.

mozilla-dotclear.png

Il y a plusieurs prix à gagner:

  • 1er : une tablette graphique Wacom Intuos3 A5 Wide
  • 2ème : une sonde de calibration d'écran Spyder 2 Express
  • 3ème : un super sac Firefox

Les trois gagnants recevront aussi un livre « CSS2 Pratique du design web », dédicacé par son auteur Raphaël Goetter. Et pour les dix finalistes, un T-shirt Mozilla sera offert.

Les meilleurs thèmes seront choisis par un jury composé de Tara Shahian (Mozilla), Olivier Meunier (Dotclear), Anne Cavalier (Dotclear), Raphaël Goetter et moi-même.

Pour tous les détails, je vous invite à consulter le site du concours: http://mozilla.dotclear.org/.

Faites passer le mot!

jeudi, février 19 2009

Créer un client Twitter offline pour l'iPhone avec HTML5

Cet article va expliquer comment créer une application Twitter simple, qui fonctionne qu’on soit connecté ou non. Pour celà, nous allons utiliser des technologies Web, en particulier des nouveautés de HTML5 qui permettent de stocker des données côté client et de mettre des fichiers en cache. Le navigateur Safari de l’iPhone se prête bien à l’exercice, puisqu’il supporte ces technologies.

Les plus pressés peuvent tester immédiatement la démo.

Le squelette de l'application

Tout d’abord, il faut créer le squelette HTML de notre application. Ce fichier HTML devrait faire l’affaire:

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; UTF-8" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta http-equiv="Content-Language" content="en" />
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=1;"/>
<style type="text/css">
    body {
        font-family: sans-serif;
        margin: 0;
        padding: 0;
    }
    #toolbar {
        background: #EFEFEF;
        padding: 5px;
        border-bottom: 1px solid #a3a3a3;
    }
    #toolbar button {
        border: 1px solid #a1a1a1;
        -webkit-border-radius: 3px;
        background: #F3F3F3;
        padding: 0.5em;
    }
    ol {
        margin: 0; padding: 0;
    }
    ol li{
        list-style: none;
        border-bottom: 1px solid #CCC;
        margin: 0;
        padding: 0.5em;
        overflow: auto;
    }
    span.text {
        display: block;
        margin-left: 29px;
    }
    li img {
        float: left;
    }
    .date {
        font-size: 0.8em;
        color: #AAA;
    }
</style>
<title>iPhone Twitter</title>
</head>

<body>
<div id="toolbar">
    <button id="load">&#8635; Refresh</button>
</div>
<ol id="timeline"></ol>

<script type="text/javascript" src="twitter.js"></script>
<script type="text/javascript">
<!--
window.onload = function() {
    Twitter.init();
    document.getElementById('load').onclick = function() {
        Twitter.load();
    }
}
-->
</script>
</body>
</html>

Ce fichier se décompose de la manière suivante:

  • L’entête HEAD avec le doctype HTML5, une balise META spécifique à l’iPhone, un peu de CSS (et même un peu de CSS3);
  • Le corps BODY qui contient une toolbar et une liste ordonnée vide;
  • L’inclusion d’un fichier JS et un peu de code pour démarrer l’application et ajouter un le comportement à notre toolbar.

Passons maintenant au fichier twitter.js

Accès à la base de données

La première chose, est d’ouvrir la base de données. Si elle n’existe pas, elle sera créée automatiquement.

var db = null;
try {
    if (window.openDatabase) {
        db = openDatabase("Twitter", "1.0", "Twitter Feed", 200000);
    }
} catch(err) {}

La fonction openDatabase prend 4 paramètres:

  • le nom de la base
  • la version de la base
  • le nom long (ou description)
  • la taille attendue de la base

La base sera accessible à travers la variable globale db.

L'objet Twitter

On va ensuite créer un objet Twitter qui va contenir le code pour le chargement et l’affichage des tweets.

var Twitter = {
    count: 20
}

Pour le moment il ne contient qu’un attribut de configuration: le nombre de tweets à télécharger à chaque rafraîchissement. Mais nous allons tout de suite lui ajouter des méthodes dans le chapitre suivant.

Création de la table et affichage des tweets

Au chargement de l’application, nous allons tenter de créer la table (si elle n’existe pas) et lire les tweets qu’elle contient pour les afficher. Comme les données sont stockée en local, les tweets s’afficheront, même si la connexion Internet est interrompue.

En revanche, si la base de données n’est pas disponible, nous allons récupérer les tweets directement en ligne.

On va donc ajouter la méthode init qui suit:

Création de la table:

//Create table
init : function() {
    console.log('init');
    
    if (db) {
        //Have database? Read data
        db.transaction(function(tx) {
            tx.executeSql(
                "CREATE TABLE IF NOT EXISTS status (id REAL UNIQUE, username TEXT, created_at TEXT, text TEXT, avatar TEXT)", 
                [], 
                function(result) {
                    Twitter.readStatus();
                }, 
                function(tx, error) {
                    Twitter.readStatus();
                }
            );
        });
    } else {
        //No database? Just load data and display
        Twitter.load();
    }
},

Si la base de données est bien là et la table créée, nous pouvons faire une requête pour récupérer les tweets:

//Read statuses
readStatus : function() {
    console.log('read');
    db.transaction(function(tx) {
        tx.executeSql("SELECT id, username, created_at, text, avatar FROM status ORDER BY id DESC", [], function(tx, result) {
            
            var timeline = document.getElementById('timeline');
            timeline.innerHTML = '';
            
            for (var i = 0; i < result.rows.length; ++i) {
                var row = result.rows.item(i);
                Twitter.display(row);
            }
        }, function(tx, error) {
            // Couldn't retrieve tweets
            return;
        });
    });
},

Rien de très compliqué: la requête de type SELECT récupère les données et on passe chaque ligne résultat à une fonction qui va se charger de les afficher.

L’affichage d’un tweet est effectué par une fonction qui va simplement généré un LI pour l’ajouter à notre liste ordonnée:

//Display statuses
display : function(row) {
    console.log('display');
    var timeline = document.getElementById('timeline');
    
    var li = document.createElement('LI');
    var d = new Date();
    d.setTime(Date.parse(row['created_at']));
    li.innerHTML = 
        '<img src="'+row['avatar']+'" alt="" height="24" widht="24"/>'+
        '<span class="text">'+
        '<strong>'+row['username'] + '</strong> ' +
        row['text'] +
        ' <span class="date">' + prettyDate(d) + '</span>'+
        '</span>';
    timeline.appendChild(li);
},

Notez que cette fonction utilise une autre fonction prettyDate qui formatte la date pour la rendre lisible. Je ne documenterai pas cette fonction mais vous la trouverez dans la démo.

Remplissage de la base de données

Pour le moment, notre table ne contient aucun tweet, donc rien ne s’affiche. Nous allons charger les données twitter, les stocker et les afficher.

Pour faire simple, nous allons utiliser l’API JSONP de Twitter pour charger les 20 derniers tweets. La méthode load fait plusieurs choses:

  • nettoyer les <script> précédents qui auraient pu être ajouté par un précédent chargement
  • donnée un feedback visuel pour indiquer que le chargement a démarré
  • charger les tweets en insérer le fichier JSONP en tant que <script> dans le <head>
  • indiquer que le chargement est terminé lorsque les données sont là

Comme la timeline est protégée par un login et un password, il faudra les entrer lorsque le navigateur le demande.

load : function() {
    console.log('load');
    //Remove JSONP scripts
    var head = document.getElementsByTagName('head')[0];
    var scripts = null;
    while ((scripts = head.getElementsByTagName('script')).length > 0 ) {
        head.removeChild(scripts[0]);
    }
    
    document.getElementById('load').innerHTML = 'Refreshing...';
    
    var now = new Date();
    var url = 'http://twitter.com/statuses/friends_timeline.json?callback=Twitter.twitterCallback&count={count}&random={random}';
    var script = document.createElement('script');
    script.setAttribute(
        'src', 
        url.replace('{count}',Twitter.count)
           .replace('{random}',now.getTime())
    );
    script.onload = function() {
        document.getElementById('load').innerHTML = '&#8635; Refresh';
    }
    console.log(script);
    head.appendChild(script);
},

Une fois le chargement terminé, un callback est automatiquement appelé. Si la base de données existe, il va simplement extraire les données qui nous intéressent et les stocker dans la base de données. Sinon, il va afficher chaque tweet.

twitterCallback : function(obj) {
    console.log('callback');
    
    if (db) {
        var inserts = [];
        for (var i=0, l=obj.length; i<l; i++) {
            var status = [
                obj[i].id,
                obj[i].user.screen_name,
                obj[i].text,
                obj[i].created_at,
                obj[i].user.profile_image_url
            ]
            inserts.push(status);
        }
        Twitter.insert(inserts, Twitter.readStatus);
    } else {
        //No database? just display
        for (var i=0, l=obj.length; i<l; i++) {
            Twitter.display({
                'id' : obj[i].id,
                'username' : obj[i].user.screen_name,
                'text' : obj[i].text,
                'created_at' : obj[i].created_at,
                'avatar' : obj[i].user.profile_image_url
            });
        }
    }
},

Il faut savoir que l‘INSERT est asynchrone et risque de lancer l’affichage avant que nos données soient entièrement stockées. Nous utilisons donc notre propre fonction Twitter.insert qui va faire les insertions et n’afficher qu’à la fin.

Voilà notre méthode d’insertion qui simule une insertion synchrone. Elle prend deux paramètres: le tableau de données et un callback à executer en find d’insertion.

//Synchronous insert
insert : function(arStatus, callback) {
    var status = arStatus.pop();
    var sql = "INSERT INTO status (id, username, text, created_at, avatar) VALUES (?,?,?,?,?)";
    
    db.transaction(
        function (tx) {
            tx.executeSql(
                sql,
                status,
                function(tx, result){
                    if (arStatus.length > 0) {
                        Twitter.insert(arStatus, callback);
                    } else {
                        callback();
                    }
                },
                function(tx, error){
                    if (arStatus.length > 0) {
                        Twitter.insert(arStatus, callback);
                    } else {
                        callback();
                    }
                }
            );
        }
    );
}

Voilà, nous avons tout le code nécessaire pour récupérer des tweets, les stockers localement et les afficher sans repasser par le serveur.

Cache d'applications offline

Jusque là, nous avons créé une application web qui conserve des données en local. Une connexion reste néanmoins nécessaire pour afficher la page HTML et charger le javascript. Heureusement, HTML5 offre un mécanisme qui permet de garder ces deux fichiers en cache et d’y accéder même sans connexion Internet. Cette fonctionnalité est offerte par le cache-manifest, un fichier qui indique au navigateur la liste des fichiers à garder en cache. Ces fichiers ne seront plus jamais rafraichis, à moins que le cache-manifest ne change.

Ce fichier doit être envoyé avec le bon type MIME Content-type: text/cache-manifest (avec PHP par exemple) pour fonctionner. Notez qu’on inclue un commentaire avec la date pour versionner ce fichier. Il suffira de changer la date pour forcer un rechargement complet de l’application.

CACHE MANIFEST
#20090201-1731
index.html
twitter.js

Un fois ce fichier créé, il suffit de l’inclure dans le fichier HTML en modifiant la balise HTML:

<html manifest="cache-manifest.php">

Conclusion

Si vous avez bien suivi toutes les étapes (et si j’ai bien expliqué), vous devriez avoir une application qui fonctionne totallement offline. Lors de la première connexion, la page HTML et le JS se chargent et sont mis en cache. Ils pourront être accédés n’importe quand par la suite, même sans connexion. L’application permet ensuite d’afficher des tweets stockés localement, sans téléchargement quelque donnée.

Pour télécharger les fichiers complet et tester l’application, tout est disponible à cette adresse: http://svay.com/experiences/iphone-twitter/

Cet exemple est plutôt basique et vous aurez certainement des idées d’applications plus élaborées. D’ailleurs, il semblerait que Google lance dans peu de temps une version offline de GMail pour iPhone en utilisant les même technologies.

Enfin, voilà les liens qui ont servi à préparer ce tuto:

mardi, octobre 10 2006

Fin

C'est décidé, ce blog arrive à sa fin. Comme les cool URI don't change, tout restera en ligne. Pas de suppression sauvage. Les commentaires resteront ouverts pendant une semaine encore. Je ne sais pas quoi dire de plus. Ça tombe bien.

vendredi, octobre 6 2006

Ornements pour vos sites web, partie 2

Cette série est la suite du précédent pack d'ornements publiés il y'a quelques mois. Je suis content de voir qu'ils ont pu servir dans quelques projets déjà.

Ornements

Télécharger les ornements (fichiers SVG, zippés, 14Ko)

Comme d'habitude, c'est libre et gratuit. Si vous ne savez pas comment ouvrir les fichiers SVG, essayez Inkscape, ça déchire sa race, comme on dit.

samedi, septembre 30 2006

Blog et dépôt SVN pour moonmoon

À partir de maintenant, vous n'entendrez plus parler de moonmoon sur ce blog. Non, je n'abandonne pas le projet, j'ai simplement décidé d'ouvrir un blog à part entière pour en parler. Désormais, le nouveau site de moonmoon dispose donc d'un blog mais aussi d'un dépot SVN couplé à Trac gentillement fourni par webs. Il ne reste plus qu'à faire avancer le projet.

dimanche, septembre 24 2006

3e bloganniversaire

Pfiou, j'ai failli oublier le troisième anniversaire de ce blog.

- page 1 de 51