Programació GIMP

De Wikijoan
Dreceres ràpides: navegació, cerca

Contingut

Introducció

Per què necessito saber programar plugins de GIMP, i interactuar amb GIMP des d'altres programes escrits en C? Això lliga amb el projecte Història de la Música i similars, en què es vol explicar una història mentre es dibuixa amb la Wacom sobre el GIMP, i es fa sonar música. Tot plegat ha d'haver una sèrie d'accions interactives i multimèdia.

El que es busca en aquest tutorial és sentar les bases per poder executar, des d'una aplicació escrita en C, comandes que apliquin filtres/plugins/scripts sobre una imatge del GIMP que estigui oberta.

El GIMP per a mi és l'eina elegida per poder fer una performance tipus Història de la Música i similars (dibuixar sobre la wacom). El nucli d'aquest projecte ha de ser una aplicació escrita en C/C++ que utilitzi les llibreries de JACK, de tinyXML, de GIMP. Jo vaig dibuixant amb la Wacom sobre la imatge del GIMP, però l'aplicació C va escoltant les accions que faig, i va disparant accions sobre el GIMP i accions d'audio (audio i MIDI). Hi haurà un fitxer XML que és el guió a seguir, a partir d'unes accions seqüencials.

L'objectiu és, doncs, aconseguir interactivitat tot dibuixant amb el GIMP, per tal d'aconseguir una performance atractiva.

Programar un plugin de GIMP

Segueixo

$ sudo apt-get install libgimp2.0-dev

Per instal.lar un plugin escrit en C:

$ gimptool-2.0 --install plugin.c
o
$ gimptool-2.0 --install-admin plugin.c

Compilem el codi hello.c com es comenta:

$ gimptool-2.0 --install hello.c

$ ~/.gimp-2.6/plug-ins$ ls
hello

Arrenco el GIMP, obro o creo un dibuix, i ja puc executar el plug-in, que el trobaré a Filtres > Misc > Hello World

Ara anem a implementar un Blur (difuminar):

codi d'exemple:

$ gimptool-2.0 --install myblur2.c

i ara en la tercera part del tutorial anem millorar aquest plugin:

Dreceres del teclat

Per executar ràpidament un plugin seria bo associar-lo a una drecera del teclat. Estan dins de Editar > Dreceres del teclat. Els plugins que tinc programats els puc associar a dreceres del teclat (buscar-los dins de connectors)

GIMP Batch Mode

GIMP és pot executar sense interfície gràfica i en batch mode per fer operacions que afectin a molts fitxers. Per exemple, ens podem plantejar d'aplicar un filtre sobre totes les imatges que hi ha en una carpeta (també podríem fer un resize en bloc, però recordem que això ja ho faig amb convert).

gimp --batch <command> <filename>

alguna cosa similar a (TBD):

$ gimp -i -b '(myblur3 "prova.xcf" 5.0 0.5 0)' -b '(gimp-quit 0)'
$ gimp -i -b '(simple-unsharp-mask "prova.xcf" 5.0 0.5 0)' -b '(gimp-quit 0)'

$ gimp -i -b \(python-fu-do-it \)

Des de la línia de comandes puc aplicar un filtre com ara myblur3, o executar un script, per exemple programat amb scheme, que és el llenguatge que utilitza GIMP (integrat amb C o Python).

example_script.scm

(
   define (example-script inputFilename outputFileName)
   (
      let* (
         (image (car (gimp-file-load RUN-NONINTERACTIVE inputFilename inputFilename)))
         (drawable (car (gimp-image-get-active-layer image)))
      )
      (plug-in-illusion RUN-NONINTERACTIVE image drawable 10 0)
      (gimp-file-save RUN-NONINTERACTIVE image drawable outputFileName outputFileName)
   )
)

En el següent script utilitzo un dels plugins que he programat (és el plugin myblur, però fixem-nos que he de posar plug-in-myblur2). Fitxer example-script2.scm:

(
   define (example-script2 inputFilename outputFileName)
   (
      let* (
         (image (car (gimp-file-load RUN-NONINTERACTIVE inputFilename inputFilename)))
         (drawable (car (gimp-image-get-active-layer image)))
      )
      (plug-in-myblur2 RUN-NONINTERACTIVE image drawable)
      (gimp-file-save RUN-NONINTERACTIVE image drawable outputFileName outputFileName)
   )
)

Els scripts que estic programant s'han de posar en la carpeta scripts/ (~/.gimp-2.6/scripts):

$ gimp -i -b "(crop-jpg confirmacion.jpg)" -b "(gimp-quit 0)"
$ gimp -i -b "(example-script \"confirmacion.jpg\" \"confirmacion_out.jpg\")" -b "(gimp-quit 0)"
batch command executed successfully

$ gimp -i -b "(example-script2 \"confirmacion.jpg\" \"confirmacion_out.jpg\")" -b "(gimp-quit 0)"
batch command executed successfully

funciona!!

Els plug-ins que vénen amb el GIMP per defecte estan a /usr/lib/gimp/2.0/plug-ins


Script-Fu Scripts

Consola del Script-Fu: (dins del GIMP: Filtres > Script-Fu > Consola)

(plug-in-myblur2 RUN-NONINTERACTIVE 1 2)
Funciona!

Per aplicar el filtre es pot fer d'aquesta manera, ara bé, evidentment, millor és aplicar el filtre directament: Filtres > Difumina > My Blur 2 (està dins de Difumina perquè així ho hem progrmat en el plugin).

Per executsr aquest plugin des de la línia de comandes: (diverses comandes, algunes funcionen i d'altres no)

$ gimp -i -b "(plug-in-myblur2 RUN-NONINTERACTIVE 1 2)"
$ gimp -i -b "(plug-in-myblur2 RUN-NONINTERACTIVE 0 (gimp-image-get-active-layer 0))"

$ gimp -i -b "(gimp-displays-flush)"
$ gimp -i -b "(example-script3 \"confirmacion.jpg\")" -b "(gimp-quit 0)"

Perquè he de posar el valor 1 per a la image i el 2 per al drawable està explicat més avall.

programació amb Python

interessant!

Veig que la consola de Python (o de Script-Fu) són dimonis:

$ ps aux | grep gimp
joan      3656  0.4  4.5 516108 177820 ?       Sl   14:14   0:08 gimp-2.6
joan      3661  0.0  0.1 137820  6660 ?        S    14:14   0:00 /usr/lib/gimp/2.0/plug-ins/script-fu -gimp 11 10 -run 0
joan      3915  0.1  0.6 225412 25596 ?        S    14:36   0:00 /usr/bin/python /usr/lib/gimp/2.0/plug-ins/python-console.py -gimp 14 13 -run 0

i la pregunta seria, podria enviar comandes a la consola de python directament des de la shell, tenint en compte quin és el dimoni? Però això no funciona...:

/usr/lib/gimp/2.0/plug-ins/script-fu -gimp 11 10 -run 0 < echo "(plug-in-myblur2 RUN-NONINTERACTIVE 1 2)"

Servidor Script-Fu

Filtres > Script-Fu > Inicia el servidor

El servidor escolta peticions TCP que arriben a través de la xarxa (socket). Escolta pel port 10008. Per aquí vindrà la solució que persegueixo.

Primer de tot engeguem el servidor: Filtres > Script-Fu > Engegar el servidor

Un cop tenim encès el servidor podem veure el dimoni que corre:

$ ps aux | grep gimp
joan      2389  1.0  5.0 635692 198248 ?       Sl   00:22   0:46 gimp-2.6
joan      3628  5.5  0.3 162040 13080 ?        S    01:35   0:00 /usr/lib/gimp/2.0/plug-ins/script-fu -gimp 12 11 -run 0

Puc connectar-me al servidor per telnet, però això no serveix per res, perquè hi ha un protocol que s'ha d'acomplir:

$ telnet localhost 10008

Protocol del Servidor Script-Fu, està explicat aquí:

no és un protocol difícil d'entendre o implementar. El que està clar és que per enviar missatges al servidor (peticions a través de la xarxa), i per escoltar el que ens respon el servidor (success o failure), hem d'implementar el protocol.

La solució més fàcil per implementar el protocol és executar el script (i modificar-lo a partir d'aquest) servertest.py, que es troba per la xarxa:

script servertest.py:

#!/usr/bin/env python

import readline, socket, sys

if len(sys.argv) < 1 or len(sys.argv) > 3:
   print >>sys.stderr, "Usage: %s <host> <port>" % sys.argv[0]
   print >>sys.stderr, "       (if omitted connect to localhost, port 10008)"
   sys.exit(1)

HOST = "localhost"
PORT = 10008

try:
    HOST = sys.argv[1]
    try:
        PORT = int(sys.argv[2])
    except IndexError:
        pass
except IndexError:
    pass

addresses = socket.getaddrinfo(HOST, PORT, socket.AF_UNSPEC, socket.SOCK_STREAM)

connected = False

for addr in addresses:
    (family, socktype, proto, canonname, sockaddr) = addr

    numeric_addr = sockaddr[0]

    if canonname:
        print "Trying %s ('%s')." % (numeric_addr, canonname)
    else:
        print "Trying %s." % numeric_addr

    try:
        sock = socket.socket(family, socket.SOCK_STREAM)
        sock.connect((HOST, PORT))
        connected = True
        break
    except:
        pass

if not connected:
    print "Failed."
    sys.exit(1)

try:
   cmd = raw_input("Script-Fu-Remote - Testclient\n> ")

   while len(cmd) > 0:
      sock.send('G%c%c%s' % (len(cmd) / 256, len(cmd) % 256, cmd))

      data = ""
      while len(data) < 4:
         data += sock.recv(4 - len(data))

      if len(data) >= 4:
         if data[0] == 'G':
            l = ord(data[2]) * 256 + ord(data[3])
            msg = ""
            while len(msg) < l:
               msg += sock.recv(l - len(msg))
            if ord(data[1]):
               print "(ERR):", msg
            else:
               print " (OK):", msg
         else:
            print "invalid magic: %s\n" % data
      else:
         print "short response: %s\n" % data
      cmd = raw_input("> ")

except EOFError:
   print

sock.close()

I la manera com s'executa:

$ python servertest.py
Trying 127.0.0.1.
Script-Fu-Remote - Testclient
> (+ 3 5)
 (OK): Success

Veig que em mostra una línia de comandes, i puc ficar comandes scheme'

coses que funcionen:

$  python servertest.py
Trying ::1.
Trying 127.0.0.1.
Script-Fu-Remote - Testclient
> (plug-in-myblur2 RUN-NONINTERACTIVE 1 2)
 (OK): Success
> 

i efectivament ha funcionat, ha fet el blur. Allò important és que el plug-in-myblur2 l'he programat jo mateix a partir del llenguatge C. I també allò important és que per una banda tinc obert el GIMP amb una imatge, i per altra banda des del terminal del sistema envio comandes a un servidor de Script-Fu (GIMP), i per tant ja tinc la manera d'aplicar efectes sobre una imatge de forma remota (des de la shell, i eventualment des de qualsevol llenguatge de programació).

A partir d'aquí es tracta de pensar com puc jo llençar ordres d'execució de plugins des de la consola (bash, llenguatge C). Es tractaria de modificar el script servertest.py per admetre un altre argument que sigui l'ordre a executar. Així ho he fet:

script servertest_v2.py:

#!/usr/bin/env python

import readline, socket, sys

if len(sys.argv) <> 4 :
   print >>sys.stderr, "Usage: %s <host> <port> <command>" % sys.argv[0]
   sys.exit(1)

try:
	HOST = sys.argv[1]
	try:
		PORT = int(sys.argv[2])
		try:
			COMMAND = sys.argv[3]
		except IndexError:
			pass
	except IndexError:
	    pass
except IndexError:
    pass

print "HOST: %s" % HOST
print "PORT: %s" % PORT
print "COMMAND: %s" % COMMAND

addresses = socket.getaddrinfo(HOST, PORT, socket.AF_UNSPEC, socket.SOCK_STREAM)

connected = False

for addr in addresses:
    (family, socktype, proto, canonname, sockaddr) = addr

    numeric_addr = sockaddr[0]

    if canonname:
        print "Trying %s ('%s')." % (numeric_addr, canonname)
    else:
        print "Trying %s." % numeric_addr

    try:
        sock = socket.socket(family, socket.SOCK_STREAM)
        sock.connect((HOST, PORT))
        connected = True
        break
    except:
        pass

if not connected:
    print "Failed."
    sys.exit(1)


try:
   #cmd = raw_input("Script-Fu-Remote contra un servidor. GIMP\n> ")
	cmd = COMMAND
	print cmd

	while len(cmd) > 0:
		sock.send('G%c%c%s' % (len(cmd) / 256, len(cmd) % 256, cmd))

		data = ""
		while len(data) < 4:
			data += sock.recv(4 - len(data))

		if len(data) >= 4:
			if data[0] == 'G':
				l = ord(data[2]) * 256 + ord(data[3])
				msg = ""
				while len(msg) < l:
					msg += sock.recv(l - len(msg))
				if ord(data[1]):
					print "(ERR):", msg
				else:
					print " (OK):", msg
			else:
				print "invalid magic: %s\n" % data
		else:
			print "short response: %s\n" % data

		sock.close()
		quit()

except EOFError:
   print

i ja puc executar el script des de la consola:

$ python servertest_v2.py localhost 10008 "(plug-in-myblur2 RUN-NONINTERACTIVE 1 2)"
HOST: localhost
PORT: 10008
COMMAND: (plug-in-myblur2 RUN-NONINTERACTIVE 1 2)
Trying ::1.
Trying 127.0.0.1.
(plug-in-myblur2 RUN-NONINTERACTIVE 1 2)
 (OK): Success

i efectivament ha fet un blur sobre la imatge del GIMP que tinc oberta.

Discussió sobre els valors dels paràmetres image i drawable

Tinc dues imatges obertes. La primera la difumino fent

(plug-in-myblur2 RUN-NONINTERACTIVE 1 2)

i la segona la difumino, però he de canviar els valors dels dos paràmetres. El primer argument té a veure amb la imatge, i el segon argument té a veure amb les capes.

Els tres paràmetres són:

A GIMP image is a structure that contains, among others, guides, layers, layer masks, and any data associated to the image. The word "drawable" is often used in GIMP internal structures. A "drawable" is an object where you can get, and sometimes modify, raw data. So : layers, layer masks, selections are all "drawables".

Quin ordre tenen imatges i capes (drawable)

Creem una imatge nova, amb una capa. La image té idimage=1, i la cada idcapa=2

(plug-in-myblur2 RUN-NONINTERACTIVE 1 2)

Quan creem successives capes, els números del idcapa augmenten

(plug-in-myblur2 RUN-NONINTERACTIVE 1 3)
(plug-in-myblur2 RUN-NONINTERACTIVE 1 4)

Creem un nova imatge. el idimage se li suma 1, però el idcapa se li suma 2: (creem dues capes)

(plug-in-myblur2 RUN-NONINTERACTIVE 2 6)
(plug-in-myblur2 RUN-NONINTERACTIVE 2 7)

Creem una nova imatge amb dues capes:

(plug-in-myblur2 RUN-NONINTERACTIVE 3 9)
(plug-in-myblur2 RUN-NONINTERACTIVE 3 10)

Ara afegim una nova capa a la imatge primera:

(plug-in-myblur2 RUN-NONINTERACTIVE 1 11) (no se li suma 2, se li suma 1)

Ara afegim una nova capa a la imatge segona:

(plug-in-myblur2 RUN-NONINTERACTIVE 2 12) (no se li suma 2, se li suma 1)

Executar el plugin des de llenguatge C

Des del meu codi C faig una crida de systema al script servertest_v2.py de python:

// gcc -o execute_blur execute_blur.c
int main(int argc, char *argv[])
{ 
	system("python servertest_v2.py localhost 10008 \"(plug-in-myblur2 RUN-NONINTERACTIVE 1 2)\"");
}
$ ./execute_blur 

HOST: localhost
PORT: 10008
COMMAND: (plug-in-myblur2 RUN-NONINTERACTIVE 1 2)
Trying ::1.
Trying 127.0.0.1.
(plug-in-myblur2 RUN-NONINTERACTIVE 1 2)
 (OK): Success

i la imatge que tinc oberta en el GIMP queda difuminada.

Programant un plugin

Escriure pixels per la pantalla

M'he proposat escriure pixels sueltos a la imatge GIMP, i ha estat més difícil del que em pensava. Finalment el fitxer gimp_pixel_fetcher_put_pixel.c escriu 5 pixels sobre la imatge GIMP de diferents maneres: el primer és el mateix color que el foreground; el segon utilitza la funció gimp_rgb_set per definir el color; el tercer té el mateix color que el pixel que hi a (0,0); el quart i el cinquè fico directament les components del color.

Com sempre, per compilar:

$ gimptool-2.0 --install gimp_pixel_fetcher_put_pixel.c

gimp_pixel_fetcher_put_pixel.c

// compilar: $ gimptool-2.0 --install gimp_pixel_fetcher_put_pixel.c

//mirar gradient-flare.c
//mirar whirl_and_pinch.c
//mirar blur-motion.c

#include <libgimp/gimp.h>

static void query (void);
static void run   (const gchar      *name,
                   gint              nparams,
                   const GimpParam  *param,
                   gint             *nreturn_vals,
                   GimpParam       **return_vals);

static void pintar_pixel  (GimpDrawable     *drawable);

GimpPlugInInfo PLUG_IN_INFO =
{
  NULL,
  NULL,
  query,
  run
};

MAIN()

static void
query (void)
{
  static GimpParamDef args[] =
  {
    {
      GIMP_PDB_INT32,
      "run-mode",
      "Run mode"
    },
    {
      GIMP_PDB_IMAGE,
      "image",
      "Input image"
    },
    {
      GIMP_PDB_DRAWABLE,
      "drawable",
      "Input drawable"
    }
  };

  gimp_install_procedure (
    "plug-in-putpixel",
    "PutPixel",
    "Pinta un pixel",
    "Joan Quintana (joanillo)",
    "Copyright Joan Quintana",
    "2012",
    "_Put Pixel",
    "RGB*, GRAY*",
    GIMP_PLUGIN,
    G_N_ELEMENTS (args), 0,
    args, NULL);

  gimp_plugin_menu_register ("plug-in-putpixel",
                             "<Image>/Filters/Blur");
}

static void
run (const gchar      *name,
     gint              nparams,
     const GimpParam  *param,
     gint             *nreturn_vals,
     GimpParam       **return_vals)
{
  static GimpParam  values[1];
  GimpPDBStatusType status = GIMP_PDB_SUCCESS;
  GimpRunMode       run_mode;
  GimpDrawable     *drawable;

  /* Setting mandatory output values */
  *nreturn_vals = 1;
  *return_vals  = values;

  values[0].type = GIMP_PDB_STATUS;
  values[0].data.d_status = status;

  /* Getting run_mode - we won't display a dialog if
   * we are in NONINTERACTIVE mode
   */
  run_mode = param[0].data.d_int32;

  /*  Get the specified drawable  */
  drawable = gimp_drawable_get (param[2].data.d_drawable);

  gimp_progress_init ("Pintant un pixel...");
	

  /* Let's time blur
   *
   *   GTimer timer = g_timer_new time ();
   */

  pintar_pixel (drawable);

  gimp_displays_flush ();
  gimp_drawable_detach (drawable);

  return;
}

static void
pintar_pixel (GimpDrawable *drawable)
{
	//void gimp_pixel_fetcher_put_pixel (GimpPixelFetcher *pf, gint x, gint y, const guchar *pixel);

	//http://developer.gimp.org/api/2.0/libgimp/libgimp-gimppixelfetcher.html
	//pf: a pointer to a previously initialized GimpPixelFetcher.
	//pixel: the memory location where to return the pixel. the pixel to set.

	GimpPixelFetcher *pf;
	gint x1, y1;
	guchar *px; //pixel is an array of bpp bytes. 
	GimpRGB color;

	x1=0;
	y1=0;

	pf = gimp_pixel_fetcher_new (drawable,FALSE);
	//gimp_pixel_fetcher_set_edge_mode (pf, GIMP_PIXEL_FETCHER_EDGE_BLACK);

	px = g_new (guchar, 1);

	//1. escric un pixel amb el color del foreground
	gimp_context_get_foreground (&color); //ja sé la manera de recuperar els valors del foreground (i del background)
	//g_message ("%d,%d,%d",(int)(&color)->r,(int)(&color)->g,(int)(&color)->b); 
	px[0] = 255*(&color)->r; //vermell (0-255)
	px[1] = 255*(&color)->g; //verd (0-255)
	px[2] = 255*(&color)->b; //blau (0-255)
	gimp_pixel_fetcher_put_pixel (pf, x1+20, y1, px);

	//2. escric un pixel i dic quin color vull amb la funció gimp_rgb_set
	gimp_rgb_set (&color, 0.0, 1.0, 1.0);
	px[0] = 255*(&color)->r; //vermell (0-255)
	px[1] = 255*(&color)->g; //verd (0-255)
	px[2] = 255*(&color)->b; //blau (0-255)
	gimp_pixel_fetcher_put_pixel (pf, x1+21, y1, px);

	//3. escric un pixel igual que el de la posició (0,0)
	gimp_pixel_fetcher_get_pixel (pf,x1, y1, px);
	g_message ("%d,%d,%d",(int)px[0],(int)px[1],(int)px[2]); //puc obtenir informació del pixel (components RGB)
	gimp_pixel_fetcher_put_pixel (pf, x1+22, y1, px);

	//4. escric un pixel i dic quin color vull
	px[0] = 255; //vermell (0-255)
	px[1] = 0; //verd (0-255)
	px[2] = 0; //blau (0-255)
	gimp_pixel_fetcher_put_pixel (pf, x1+23, y1, px);
	// o bé
	*px=0; //vermell (0-255)
	*(px+1)=0; //verd (0-255)
	*(px+2)=255; //blau (0-255)
	gimp_pixel_fetcher_put_pixel (pf, x1+24, y1, px);

	g_free (px);

	gimp_pixel_fetcher_destroy (pf);

	gimp_drawable_flush (drawable);
	gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
	gimp_drawable_update (drawable->drawable_id,20,0,5,1);

}

El següent objectiu seria accedir als brush i dibuixar una línea amb el brush que sigui més o menys gruixuda (controlant els paràmetres del brush).

Mostrar missatges

Els missatges poden servir, entre d'altres coses, per depurar en el moment de la programació. Veig com puc enviar missatges que apareixen en la imatge a baix de tot:

g_message ("%d,%d,%d",(int)px[0],(int)px[1],(int)px[2]); 

En l'anterior punt hem vist com executar aquest plugin des de la consola. Per exemple:

python servertest_v2.py localhost 10008 "(plug-in-putpixel RUN-NONINTERACTIVE 1 2)"

Doncs bé, quan ho executo d'aquesta manera, els missatges surten en forma de caixa de diàleg. Ara bé, si hi ha varis g_message, només apareix una sola vegada.

Dibuixar una línia amb un pinzell (i dibuixar un pentagrama)

A gimp_pintar_ratlla.c dibuixo ja un pentagrama (que consisteix en dibuixar 5 línies paral.leles a part de fer alguna cosa més...)

S'executa des de la consola:

$ python servertest_v2.py localhost 10008 "(plug-in-pintar-ratlla RUN-NONINTERACTIVE 1 2 )"

i el següent pas és parametritzar el dibuix del pentagrama. Amb 4 paràmetres puc parametritzar el dibuix d'un pentagrama: a,b,c,d,e:

$ python servertest_v2.py localhost 10008 "(plug-in-pintar-ratlla3 RUN-NONINTERACTIVE 1 2 \"150,150,1000,10\")"

Hi ha molta informació susceptible de ser parametritzada, no només les coordenades. Per exemple, el color, el pinzell a utilitzar (que determina el gruix),,,, Jo ara he fet la suposició de què tots els paràmetres són enters, però si els paràmetres que passo són combinació d'enters i cadenes, aleshores ja és un tractament una mica diferent. El més senzill seria en aquest cas passar dos conjunts de paràmetres: un conjunt numèric, i un conjunt de cadena. Per exemple. la versió 4 parametritza el tipus de pinzell i el color del pinzell, a part dels paràmetres purament geomètrics:

$ python servertest_v2.py localhost 10008 "(plug-in-pintar-ratlla4 RUN-NONINTERACTIVE 1 2 \"50,50,300,8\" \"Circle (01),ff0000\")"

Descarregar:

I ara continuo amb el projecte projectes/historia_wacom_minim. que aprofundeix aquesta idea. La idea és que llegeixo un fitxer XML on hi ha la seqüència de totes les funcions/plugins que s'han d'executar i els paràmetres que s'han de passar; i que aquestes accions es disparen a partir d'events provocats pel ratolí (en principi el botó mig del ratolí, després serà la wacom)



creat per Joan Quintana Compte, novembre 2012

Eines de l'usuari
Espais de noms
Variants
Accions
Navegació
IES Jaume Balmes
Màquines recreatives
CNC
Informàtica musical
joanillo.org Planet
Eines