Dans un précédent article nous avons affiché une image personnalisée sur le LCD de l’EV3.
Comme on peut s’y attendre, l’EV3 ne fournit rien à la base pour interagir facilement avec l’afficheur et il faut donc passer par les fichiers virtuels /dev/lms_xxx, le frame buffer et toutes ces sortes de choses pour cela.
Heureusement, les auteurs de leJOS ont fait le travail difficile pour nous, et comme vous avez pu le constater en analysant le source de la démo incluse dans l’article précédent, afficher une image sur le LCD est très simple.
Oui mais...
...le format des données graphiques est assez spécial.
Pour faire court :
- il s’agit d’images monochromes uniquement, le LCD de l’EV3 ne sachant pas faire autre chose
- les données binaires contiennent les différents pixels ligne par ligne
- comme ça aurait été trop simple sinon, chaque byte est inversé. Autrement dit, le bit de poids faible est celui qui correspond au pixel le plus à gauche du groupe de 8
- les données d’une ligne sont paddées sur une frontière d’octet
Pour tout savoir dans le détail, un petit tour sur le forum leJOS s’impose.
C’est pas gagné :/
Heureusement, il y a Python...
... qui va nous permettre d’écrire un petit script fort pratique, capable de charger l’image depuis n’importe quel fichier graphique supporté par la librairie Python Imaging Library (aka PIL) [1] et de générer les données au format voulu.
Il est prévu deux options en termes de format de sortie :
- un source Java qui définit une classe qui contient les données de l’image
- un fichier ressource binaire que l’application peut charger à la demande
Le format du fichier ressource binaire est le suivant :
- entête
- largeur de l’image en pixels (2 bytes)
- hauteur de l’image en pixels (2 bytes)
- taille des données en bytes (4 bytes)
- données de l’image
Les entiers sont au format big endian de manière à pouvoir être lus directement depuis un stream Java.
Et voilà le travail
#!/usr/bin/python
# -*- coding: utf8 -*-
import sys
import os
import argparse
import logging
import datetime
import math
from struct import pack
import Image
JAVA_CODE_PACKAGE = 'package %s;\n'
JAVA_CODE_PROLOG = '''import javax.microedition.lcdui.Image;
// Generated on %(date)s from file %(infile)s
public class %(classname)s {
static public final Image image = new Image(%(width)d, %(height)d, new byte[]{
'''
JAVA_CODE_EPILOG = ''' });
}
'''
JAVA_CODE_BYTE = '(byte)0x%02x'
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format='[%(levelname).1s] %(message)s'
)
class Converter(object):
def run(self, run_args):
try:
method = getattr(self, 'export_as_' + run_args.output_format)
except AttributeError:
raise ConvertError('format not yet implemented : %s' % run_args.output_format)
else:
try:
logging.info('Loading image from %s...', run_args.infile)
img = Image.open(run_args.infile)
except Exception as e:
raise ConvertError(e)
else:
method(img, run_args)
logging.info('Done.')
def execute(self, img, emit_byte):
img_w, img_h = img.size
line_padcnt = img_w % 8
byte = 0
for y in xrange(img_h):
for x in xrange(img_w):
byte = (byte >> 1) | (0x80 if img.getpixel((x, y)) > 0 else 0)
if x % 8 == 7:
emit_byte(byte)
byte = 0
if line_padcnt:
emit_byte(byte >> line_padcnt)
byte = 0
def export_as_java(self, img, run_args):
img_w, img_h = img.size
line_padcnt = img_w % 8
outfile = run_args.class_name + '.java'
logging.info('Generating file %s...', outfile)
out = open(os.path.join(run_args.output_dir, outfile), 'wt')
try:
if run_args.package_name:
out.write(JAVA_CODE_PACKAGE % run_args.package_name)
out.write(JAVA_CODE_PROLOG % {
'date':datetime.datetime.now().isoformat(' ')[:-7],
'infile':run_args.infile,
'classname':run_args.class_name,
'width':img_w,
'height':img_h
}
)
src = []
self.execute(img, lambda b : src.append(JAVA_CODE_BYTE % b))
lines = []
for offs in xrange(0, len(src), 8):
lines.append(' ' + ','.join(src[offs:offs+8]))
out.write(',\n'.join(lines) + '\n')
out.write(JAVA_CODE_EPILOG)
finally:
out.close()
def export_as_resource(self, img, run_args):
img_w, img_h = img.size
line_padcnt = img_w % 8
outfile = run_args.res_fname
logging.info('Generating file %s...', outfile)
out = open(os.path.join(run_args.output_dir, outfile), 'wb')
try:
lg = int(math.ceil(img_w * img_h / 8.))
out.write(pack('>HHI', img_w, img_h, lg))
self.execute(img, lambda b : out.write(chr(b)))
finally:
out.close()
class ConvertError(Exception):
pass
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Converts a graphic file into an EV3 image.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
'--class-name',
help='the name of the generated class (if --output-format is "java")',
dest='class_name',
default='EV3Image'
)
parser.add_argument(
'--resource-filename',
help='the name of the generated resource file (if --output-format is "resource")',
dest='res_fname',
default='image.bin'
)
parser.add_argument(
'--package',
help='name of the parent package',
dest='package_name'
)
parser.add_argument(
'--output-format',
help='output format selector',
dest='output_format',
choices=['java','resource'],
default='java'
)
parser.add_argument(
'--output-dir',
help='output directory path',
dest='output_dir',
default='.'
)
parser.add_argument(
help='input graphic file',
dest='infile'
)
args = parser.parse_args()
try:
Converter().run(args)
except ConvertError as e:
logging.error(e)
sys.exit(2)
Vous pourrez noter l’utilisation des lambda fonctions pour factoriser l’algorithme global dans la méthode execute tout en lui fournissant les fonctions d’émission des données produites correspondant au format généré.
Et le mode d’emploi ?
Pour le savoir, il suffit de demander ./make_ev3_image.py —help pour obtenir les informations attendues :
usage: make_ev3_image.py [-h] [--class-name CLASS_NAME]
[--resource-filename RES_FNAME]
[--package PACKAGE_NAME]
[--output-format {java,resource}]
[--output-dir OUTPUT_DIR]
infile
Converts a graphic file into an EV3 image.
positional arguments:
infile input graphic file
optional arguments:
-h, --help show this help message and exit
--class-name CLASS_NAME
the name of the generated class (if --output-format is
"java") (default: EV3Image)
--resource-filename RES_FNAME
the name of the generated resource file (if --output-
format is "resource") (default: image.bin)
--package PACKAGE_NAME
name of the parent package (default: None)
--output-format {java,resource}
output format selector (default: java)
--output-dir OUTPUT_DIR
output directory path (default: .)
Difficile de faire plus clair non ?
Pour illustrer les choses, voici le fichier graphique qui a servi à générer la classe utilisée dans le programme de démonstration de l’article précédent :
Vous pouvez noter que l’affichage sur le LCD de l’EV3 reproduit fidèlement cette image.
Il n’y a plus qu’à placer le fichier généré dans le projet qui utilise l’image. Et pour savoir comment l’utiliser dans l’application hôte, reportez-vous à l’article précédent.
A noter que l’inclusion des données de l’image sous forme de ressource binaire diminue grandement la taille du jar résultat. Dans notre application exemple, le jar de la version avec l’image sous forme de classe Java pèse 9.6kb alors que celle avec l’image sous forme de ressource binaire n’en pèse que 3.7kb. Je soupçonne l’initialisateur statique du tableau de bytes d’être le coupable.
Et c’est là qu’on mesure l’intérêt du script Ant qui y est présenté : pour voir le résultat d’une modification de l’image il suffit juste de relancer la commande Run d’Eclipse ou de taper ant run en ligne de commande. Je vous avais bien dit que ça vous simplifierait la vie.
Have fun ;)