Sobriété numérique – cas pratique: compression d’images pour les emails

Temps de lecture : 5 minutes

Chez Revolve, chaque consultant a son compte AWS relié à notre organisation, néanmoins, nous demandons à chacun de faire de l’expérimentation responsable, en particulier, faire attention aux coûts. Dans cette optique, un rapport personnalisé est envoyé par email chaque semaine pour permettre à chacun d’être alerté et de voir où il se situe par rapport aux coûts totaux.

Récemment, j’ai voulu ajouter une image à ces emails pour permettre d’afficher le positionnement de chacun et l’évolution sur 4 semaines. En effet, la position par rapport à la moyenne que nous envoyions n’était pas à mon sens une information suffisamment exhaustive.

Sobriété

Ce changement signifiait de passer d’un email texte à un contenu multipart, c’est à dire, avec une pièce jointe contenant l’image. Rien d’insurmontable en terme technique, mais aussitôt dans ma tête j’ai anticipé les râleurs du canal #sobriete_numerique de notre Slack, par exemple:

Une image de 50ko par personne multiplié par 150 emails sur une année (52 rapports) c’est 390 méga octets de stockage !!

Le « mathématicien » râleur

Tu te rend compte toute la pollution que tu génères à cause des matériaux rares qu’il faut extraire pour construire les disques qui serviront à stocker tes images ??!

L’écologiste râleur

Non mais moi j’en ai pas besoin de cette info, il faudrait pouvoir choisir de pas avoir l’image

Le « feature-killer » râleur

Allez, je vous aime tous quand même les râleurs, et puis ça m’oblige à trouver des solutions nouvelles.

Optimisation de la taille

Bref, il me fallait à minima faire des efforts pour pouvoir claquer apaiser les éventuels râleurs, sans devoir avoir recours à des attaques bassement personnelles comme: « toi qui râle, elle fait combien de kilo octets l’image que tu mets en signature dans TOUS tes emails pro ??« 

Ca tombe bien, compresser des images c’est rigolo. Partons donc de l’image PNG source que je veux intégrer dans l’email, représentant les coûts d’un consultant. Cette image est générée par plotly (ici sur des données de test):

Cette image fait environ 25 kilo octets sur mon système de fichier:

$ ls -l
-rw-rw-r-- 1 nicolas nicolas 24870 août  22 17:24 test.png

C’est beaucoup pour une image qui a 4 couleurs et des lignes assez droites. plotly génère visiblement des images non-optimales, cette librairie étant axée avant tout sur la performance, et aucune option de compression de sortie n’existe, à part générer du SVG au lieu du PNG.

PNG optimal ?

Je dégaine donc pillow et rapidement, je lui demande de me refaire un png optimisé à partir du résultat de plotly:

from PIL import Image
from io import BytesIO
im = Image.open(BytesIO(fig.to_image("png")))
im.save('test-optim.png', 'PNG', optimize = True)

C’est mieux, on est tombé à 13 kilo octet, soit presque la moitié !

$ ls -l
-rw-rw-r-- 1 nicolas nicolas 12844 août  22 17:24 test-optim.png

GIF optimal ?

Mais je peux faire encore mieux avec une image indexée, j’essaie donc le gif:

from PIL import Image
from io import BytesIO
im = Image.open(BytesIO(fig.to_image("png"))).convert("P")
im.save('test-optim.gif', 'GIF', optimize = True)

De manière assez surprenante, c’est pire ! On est arrivé à 50 kilo octets:

$ ls -l
-rw-rw-r-- 1 nicolas nicolas 50854 août  22 17:49 test-optim.gif

Là en pratique, c’est probablement une erreur de la librairie Pillow. Il semble qu’elle ne génère pas de palette optimale pour les gif, y compris avec optimize = True. Un essai de compression GIF avec Gimp me donne de bien meilleurs résultats, autour de 8 kilo-octet.

$ ls -l
-rwxrwx--- 1 nicolas nicolas  7821 août  22 17:51 test-optim-with-gimp.gif

PNG indexé et palette optimale !

Bref, je suis embêté, je peux pas faire de GIF, et 13 kilo octet en PNG c’est quand même pas ouf. Néanmoins, le PNG peut supporter l’indexation avec une palette (comme le format GIF) je peux donc générer une palette minimale et sauvegarder le tout en PNG, ça devrait améliorer les choses :

from PIL import Image
from io import BytesIO
im = Image.open(BytesIO(fig.to_image("png"))).convert("P")
im.save('test-optim-indexed.png', 'PNG', optimize = True)

Visiblement, la recherche d’une palette optimale se passe mieux en PNG et je suis tombé je pense au minimum que je puisse faire, c’est à dire 4.6 kilo octets, ce qui est même mieux que le gif optimisé par Gimp. Cela s’explique par la possibilité du PNG de faire une seconde passe d’optimisation sur le chunk IDAT et donc de compresser aussi les données sorties de la palette (DEFLATE)

$ ls -l
-rw-rw-r-- 1 nicolas nicolas  4612 août  22 17:49 test-optim-indexed.png

Si vous cherchez des détails sur la passionnante structure d’un PNG, la page wikipédia en Anglais est un très bon début.

En résumé

Sur une image originale de 24870 octets, je suis descendu à 4612, soit une économie de 81.5% ! Je vais donc pouvoir sans vergogne claquer les râleurs qui viendront me troller sur Slack à ce propos.

Alors évidemment, si la taille de l’image est bien réduite, j’ai ici un traitement supplémentaire pour passer l’image en mode indexé, et l’optimiser en PNG, ce qui consomme du temps de calcul (et me coûtera des sous car c’est un traitement qui s’exécute dans une fonction lambda AWS).

Ce qui est sûr, c’est qu’il aurait été indubitablement “moins cher” pour Revolve de pousser le mail tel quel avec mon image de 25ko sans la compresser: ça m’aurait économisé du temps de développement et plus tard du calcul lambda dans AWS. En outre, l’avantage écologique de la solution “image optimale” reste très incertain: ce sont des économies en terme de trafic Internet et de stockage chez Gmail, deux choses que je ne peux mesurer.

A noter que cette technique de passage en “indexé” des PNG fonctionne sur toutes les images PNG, et pourrait également servir à compresser les icônes d’un site web avant mise en ligne, ce que je vous encourage à faire dans tous les cas (plein d’outils existent).

Mais pourquoi ?…

Pourquoi ne pas avoir fait une image externe avec un lien ?

La plupart des fournisseurs de mail, et en particulier Google bloquent les images externes et elle n’aurait donc pas été affichée. En outre, il y a peu d’intérêt à mutualiser les images ici, celles-ci étant toutes différentes car générées pour chaque personne.

Pourquoi pas du JPEG ?

Le format JPEG est bien adapté aux photos mais sur une image aux lignes très nettes comme celle-ci, j’ai tout de suite eu des artefact disgracieux lorsque je me suis approché d’une taille de fichier comparable à celle des PNG.

Pourquoi pas du SVG ?

Une version SVG aurait écrasé tous ses concurrent en terme de performance une fois compressé (1.8 Ko compressé en GZIP), mais nous utilisons GSuite, et GMail n’est pas capable d’afficher des images SVG directement dans un email, il aurait fallu que chacun ouvre la pièce jointe et personne ne l’aurait fait.

Commentaires :

A lire également sur le sujet :