Index of UNOFFICIAL Circumvesuviana Home Page La mia tesi di laurea main index Importante: le mie pagine sul linguaggio Ruby sono qui!

Urgentissimo: bisognava mettere in piedi una libreria per creare grafici da porre in qualche pagina con statistiche; bastava dunque che avesse qualche routine essenziale per:

  1. creare un'immagine bitmap, salvarla o caricarla da disco
  2. disegnarvi punti, linee orizzontali/verticali, e ovviamente testo
  3. salvare in formato non-lossy, cioè PNG.

Lavoriamo in PBM!

Partiamo perciò dal punto più difficile: il terzo. Se la libreria creasse file dal formato super-semplificato (per esempio PNM/PBM, portable bitmap) e delegassimo ad altri la conversione da/verso PNG, abbiamo già aggirato un ostacolo.

Ci viene incontro la libreria di programmi netpbm, disponibile praticamente per ogni sistema Unix e altri sistemi operativi. Il formato PBM non è solo utilizzabile in binario ma anche in ASCII, semplificando tutto il nostro lavoro e l'eventuale debugging. Siccome abbiamo "gigahertz e gigabytes", non sentiamo il bisogno di trovare soluzioni più complicate e velocizzate.

Il formato PBM monocromatico è decisamente semplice da manipolare: la prima riga contiene un identificatore ("P1" nel caso di bitmap bianco/nero), la seconda riga contiene un commento (default "#."), la terza riga contiene la risoluzione (per esempio "640 480"), e dalla quarta riga in poi ci sono i valori dei pixel ("0" oppure "1"); qualche software si preoccupa di salvarli in righe non troppo lunghe e di cominciare una riga di testo nuova ad ogni riga nuova di pixel ed evitando righe maggiori di 72 bytes, ma noi non abbiamo bisogno di essere così precisi.

Il modo più pantofolaio per salvare in memoria una simile "mappa di bit" è di usare valori true e false per indicare "pixel colorato" (nero) oppure "pixel non colorato" (bianco, cioè colore di sfondo); questo ci semplifica la vita rispetto all'utilizzo di un tipo numerico per rappresentare i diversi colori. A noi basta il bianco e nero (vedi foto a fine pagina).

Detto, fatto: cominciamo a scrivere la libreria raster.rb contenente la classe Raster:
class Raster
  def initialize x, y
    @xsiz, @ysiz, @mat = x, y, Array.new(x*y, false)
  end

Magico Ruby! Chi dunque invoca r=Raster.new(320,200) otterrà un'istanza x già pronta con le sue variabili @xsiz e @ysiz che ricordano le dimensioni della bitmap su cui lavorare (nel nostro caso 320 e 200), e la matrice @mat di dimensioni 320×200 già riempita di tutti valori "colore di sfondo".

Abbiamo usato "@xsiz" anziché "xsiz" perché nel primo caso sono variabili "di istanza" (come le dichiarazioni "public:" del C++), nel secondo caso sarebbero state considerate variabili locali.

Esatto! Uno dei tanti vantaggi di Ruby è il cosiddetto duck-typing, "tipizzazione in stile papera": se qualcosa somiglia ad una papera, si muove come una papera e starnazza come una papera, beh... dev'essere proprio una papera, senza bisogno di certificati del notaio (e di prototipi e definizioni e di file header e tutto il resto) per confermarlo!

Per di più (magico Ruby!) abbiamo perfino risparmiato sulle parentesi tonde durante la dichiarazione della initialize: dato che lì sono ovvie (e ridondanti), Ruby permette di farne a meno!

In C++ per fare la stessa cosa di quelle poche righe di codice avremmo dovuto scrivere questo pomposo proclama bizantino:
class Raster
{
  public:
    int xsiz, ysiz;
    bool *mat;

    Raster::Raster(int x, int y)
    {
      xsiz = x;
      ysiz = y;
      mat = new bool [ x*y ];
      if (! mat)  throw std::bad_alloc();
      for(int i=0; i<x*y; i++) mat[i] = false;
    }

È vero: in C++ ci mette un millisecondo anziché quattordici millisecondi. Ma probabilmente nessuno di voi si accorgerà di aver atteso tredici millisecondi in più!


Disegnare nella bitmap

Per disegnare un punto (plot) basta cambiare il valore true/false del corrispondente valore nell'array @mat - ma dato che per la maggior parte delle volte staremo davvero disegnando "colorato", allora gli diciamo che il default è true (cioè "punto colorato", cioè "nero").

Dato che y è il numero di riga, e dato che in ogni riga ci sono @xsiz pixel, l'elemento da modificare sarà quello alla posizione x+y*@xsiz (stiamo assumendo che il primo punto è alle coordinate x=0 y=0, per cui l'elemento da modificare sarà 0+0*640=0, cioè l'elemento alla posizione 0 dell'array, cioè il primo; l'ultimo punto sarà invece a 639+479*640, cioè proprio l'ultimo valore utile dell'array).

Scriviamo poi anche una funzione point per leggere il valore di quel punto.

Magico Ruby! Quando le parentesi tonde sono ovvie, allora possono essere anche trascurate: guardate come viene comodamente chiamata la funzione plot all'interno della scanline, guardate com'è pulito il codice:
  def point x, y
    @mat[ x + y * @xsiz ]
  end

  def plot x, y, c=true
    @mat[ x + y * @xsiz ] = c
  end

  def scanline y, c=true
    y += @ysiz  if y < 0     # rettifica se negativo
    for i in 0...@xsiz
      plot i, y, c
    end
  end

Visto che spesso abbiamo bisogno di tracciare una linea orizzontale da un estremo all'altro dell'immagine, ecco la funzione scanline, che si limita a fare un ciclo for per tutti i valori validi dell'asse delle x (cioè da 0 fino a @xsiz-1: quei tre puntini significano "un range che va da 0 a @xsiz escluso quest'ultimo valore": magico Ruby! Ti risparmia perfino la rogna di dover calcolare tutti quei "meno uno" per non debordare rispetto alle dimensioni degli array!)

Dato che spesso vorremo tracciare una linea partendo dalla parte bassa dell'immagine senza dover fare troppi calcoli, tanto vale utilizzare i numeri negativi: -1 per indicare l'ultima scanline utile dell'immagine, -2 per la penultima, etc.

Traduzione in pseudocodice: "correggi aggiungendo la dimensione esatta, qualora la scanline di partenza era negativa".

Magico Ruby! Basta dirgli in modo naturale:
y += @ysiz  if y < 0


Linee e rettangoli

Per tracciare una linea orizzontale procediamo come nella scanline, dando un punto di partenza (coordinate x,y), una lunghezza (h) e il solito colore (c) che di default è "colorato".

Magico Ruby! Il codice non poteva essere più pulito:
  def vline x, y, h, c=true
    for i in 0...h
      plot x, y+i, c
    end
  end

Per tracciare un rettangolo dalle coordinate x,y di larghezza w ed altezza h ci limitiamo a due cicli for, uno per tracciare le due linee orizzontali, e l'altro per le due linee verticali:
  def rect x, y, w, h, c=true
    for i in 0...w
      plot x+i, y, c
      plot x+i, y+h-1, c
    end
    for i in 0...h
      plot x, y+i, c
      plot x+w-1, y+i, c
    end
  end

Notare come non c'è bisogno di usare inutili parentesi e complesse notazioni neppure per qualificare un ciclo for: in linguaggio C al posto di "for i in 0...h" avremmo dovuto scrivere "for(i=0; i<h; i++)". Quale delle due notazioni è più leggibile? (magico Ruby!)


Un'inizializzazione più seria

Vediamo un po'... cosa si fa quando vogliamo rinviare l'inizializzazione al momento in cui faremo il load di un'altra bitmap? Non è il caso di sprecare memoria allocando subito l'array.

E se poi volessimo inizializzare chiedendo di caricare direttamente la bitmap? Dovremmo passare come primo parametro una stringa contenente il path/filename della bitmap PBM da caricare...

E se poi volessimo passare una matrice di pixel già pronta con tutti i suoi true/false al posto giusto?

E se poi volessimo creare la classe senza allocare nessuna matrice di pixel, in attesa di decidere come allocarla o caricarla?

Morale della favola: si risolve tutto in poche righe.
  def initialize x=0, y=0, rast=nil
    if x.class == String
      load x
    else
      @xsiz, @ysiz = x, y
      if rast
        @mat = rast
      else
        @mat = x*y > 0 ? Array.new(x*y, false) : nil
      end
    end
  end

Vediamo un po':

Certo, c'è un Piccolo Bug: l'inizializzazione con due valori negativi creerebbe qualche strana matrice di pixel... Ma noi abbiamo fretta di completare la classe e non stiamo lì ad aggiungere controlli paranoici, vero?

Tremendo! Una funzione di inizializzazione di ben dodici righe! Di cui la metà occupate solo da end oppure else (!!) Chi vuole urlare che Ruby sarebbe complicato, può farlo: ogni tanto abbiamo pur bisogno di farci due risate.

Anzi, no, non è complicato. Guardate quel comodissimo if rast - Ruby fa la cosa giusta: se rast non è un valore nullo (in Ruby diremmo nil) allora esegue il blocco di codice seguente.

E che dire di quella riga in stile "linguaggio C"?
"Se x*y dà un valore maggiore di zero" (cioè se entrambi sono positivi e diversi da zero, o entrambi negativi), allora la @mat viene inizializzata con un Array di x*y oggetti tutti marcati false (cioè "colore dello sfondo"). Altrimenti niente array, lasciamo solo un nil.

Come sempre, Ruby prende il meglio (ed evita il peggio) degli altri linguaggi di programmazione. È più sintetico del Perl, è più chiaro del Pascal.


Stavolta andiamo sul complicato

Per cambiare il colore di tutti i pixel di un blocco rettangolare (a partire dalla posizione x,y per una larghezza w ed un'altezza h) basterebbe colorare i singoli pixel:
  def block xp, yp, w, h, c=true
    for x in 0...w
      for y in 0...h
        plot x+xp, y+yp, c
      end
    end
  end

Per esempio si può invocare con r.block 130,140,80,70 (le parentesi le useremo solo nei casi in cui questo stile pantofolaio potrebbe condurre ad ambiguità).

Mettiamo il caso che a seconda della posizione all'interno del blocco, io voglia decidere che colore dargli. In teoria basterebbe che al momento del trattamento di un singolo pixel, venisse invocata una funzione di callback, eseguendo un blocco di codice...

Magico Ruby! Anziché puntatori di funzioni e arzigogoli di callback, al termine di ogni chiamata di funzione si può aggiungere un blocco di codice (con eventuali parametri) che può essere utilizzato all'interno della funzione chiamata!

Per esempio, vorrei che utilizzasse una griglia di punti: se ci troviamo su un punto "dispari" allora colorare; altrimenti "lasciare colore sfondo".

Il blocco di codice che dovremo usare è { |x,y| ((x+y)&1) != 0 } (cioè, dati x e y, se il bit meno significativo della loro somma è diverso da zero - cioè "coordinata dispari" - allora il risultato è true, cioè "colorare").

Vogliamo dunque chiamarla con:
r.block(130,140,80,70) { |x,y| ((x+y)&1) != 0 }

Cosa fare? Basta sostituire l'ultimo parametro della chiamata a plot: prima gli passavamo il colore c, ora invece gli passeremo il risultato dell'esecuzione del blocco di codice (wow, era dai tempi del compilatore Clipper che non riversavo blocchi di codice a chiamate di funzioni!)

Una funzione si accorge di avere a disposizione un blocco di codice quando la "block_given?" restituisce true (notare l'elegante punto interrogativo, che fa parte del nome e lascia immediatamente capire al programmatore che restituirà un valore true/false: eh, proprio per veri comodissimi pantofolai!)

Per semplicità e velocità dividiamo i due casi, il primo per l'uso del blocco di codice e il secondo per il caso normale (quello già mostrato sopra), in modo da vedere con i nostri stessi occhi la "grande" differenza tra le due chiamate al metodo plot:
  def block xp, yp, w, h, c=true
    if block_given?
      for x in 0...w
        for y in 0...h
          plot x+xp, y+yp, yield(point(x+xp, y+yp))
        end
      end
    else
      for x in 0...w
        for y in 0...h
          plot x+xp, y+yp, c
        end
      end
    end
  end

Magico Ruby! Per implementare questo colossale meccanismo di callback parametrizzato... è bastato utilizzare "block_given?" e "yield"! Con Ruby la programmazione è talmente semplice che i programmatori urlano spesso: "uàh! ha funzionato tutto al primo colpo!"


Coi blocchi di codice ci abbiamo preso gusto

Facciamo un po' di bit-blitting: non ci accontenteremo mica di una funzione per copiare un'area da una bitmap all'altra, no? Non ci dispiacerebbe mica di avere la possibilità durante la copia di invertire i colori o calcolarceli a piacer nostro, vero?

Detto, fatto:
  def blit xdst, ydst, img, xsrc, ysrc, w, h
    for y in 0...h
      for x in 0...w
        a = img.point xsrc+x, ysrc+y      # preleva pixel sorgente
        b = point xdst+x, ydst+y          # preleva pixel destinatario
        c = a | b                         # default: operazione "or"
        c = yield(a,b)  if block_given?   # cambia operazione se block given
        plot xdst+x, ydst+y, c            # plotta il punto appena calcolato
      end
    end
  end

Come parametri, le coordinate x,y della destinazione nella nostra bitmap corrente; poi la bitmap da cui attingere, le coordinate x,y da cui attingere, e infine larghezza e altezza.

Il tutto si fa con due cicli for innestati (per ognuna delle h righe, per ognuno dei w punti sulla riga).

Anzitutto preleva il pixel dalla sorgente (applica il metodo point al parametro img) e quello della destinazione. Per default vogliamo l'operazione di or tra i due punti ("a|b"): se almeno uno dei due è colorato di nero, allora vogliamo un nero.

Ma... se c'era stato passato un blocco di codice? Facile, lo eseguiamo (yield) solo se c'era stato dato (if block_given?).

A questo punto possiamo disegnare il punto nella nostra bitmap (plot) e continuare fino al termine dei cicli for.


Come dice la canzone: "adesso viene il bello"

Abbiamo abbastanza primitive per tracciare linee e copiare/modificare blocchi di codice. A questo punto dobbiamo solo implementare qualche font e la libreria è completa.

Esempio di font Bando alle complicazioni: un font sarà un file PBM di tre righe per trentadue caratteri, rappresentanti i valori ascii da 32 a 127, tutti disegnati in una griglia fissa (per esempio 7×14 pixel).

Per tenercelo in memoria utilizzeremo la stessa classe Raster (già: una Raster che ingloba dentro di sè un'altra Raster che rappresenta un font... comodo, no?)

Attiviamo perciò anche tre nuove variabili d'istanza per tener traccia delle dimensioni del font e della classe. Piccolo Bug: non viene fatto nessun controllo sul font; ma stiamo pur sempre supponendo che lo utilizzeremo solo noi, senza andare a pasticciare nella nostra stessa directory dei font PBM...
  def setfont fontname, x, y
    @fxsiz = x
    @fysiz = y
    @fnt = Raster.new fontname
  end

Quando dunque c'è bisogno di disegnare un carattere nella bitmap, cosa si fa? Basterebbe fare il "blit" dal carattere in questione (in @fnt) alla bitmap originale.

Come si ricavano le posizioni a,b del font? Semplice: per i caratteri da 32 a 63, siamo nella riga 0; per quelli da 64 a 95 siamo nella riga 1, e per quelli da 96 a 127 siamo nella riga 2. Ogni carattere è largo @fxsiz, ogni riga è alta @fysiz.

Vediamo la prima versione del tracciamento di una stringa nella bitmap corrente attraverso il font @fnt:
def puts x, y, str
  str.each_byte do |i|
    a = i % 32
    b = case i
          when 32..63:  0
          when 64..95:  1
          when 96..127: 2
          else          return
        end

    blit x, y, @fnt,  a * @fxsiz, b * @fysiz,       @fxsiz, @fysiz
## blit: dove, font,  posizione a/b del carattere,  dimensioni carattere
  end
end

Magico Ruby! Anziché rintronarci con i cicli "per ogni carattere della stringa", abbiamo già pronto il metodo each_byte (per ogni byte).

Notare come il valore di b venga assegnato da un pezzo di codice. E guardate come "parla" quel codice Ruby: "considera i: se da 32 a 63 allora il risultato è 0... se non è nessuno dei precedenti allora esci dalla puts senza protestare". Altro che la switch del linguaggio C e dei suoi parenti!

Infine, notare l'uso elegante della blit, che va a pescare dati dal @fnt alle posizioni appena calcolate.

Nota: tutto quel case si può sostituire evidentemente con una espressione più semplice: "b = (i / 32) - 1" (ma l'esempio era ovviamente per capire come funziona il case e come si può inserire un pezzo di codice per cavarne fuori un'espressione).


Con Ruby si possono fare anche cose più complicate...

Spesso mi capita di dover "infilare" scritte in spazi angusti della bitmap, per cui la copia dell'intero carattere (compreso il suo sfondo) mi sembra un po' eccessiva. Riscrivo qui dunque il metodo puts in modo che vengano trattati dal font solo i pixel effettivamente colorati (cosicché il loro sfondo non vada a cancellare eventuali punti già disegnati in precedenza, come riquadri o linee delimitatrici).

Dato che tanti anni fa ho scritto più volte routine assembler per tracciare font (scrivendo demo/intro per Zx Spectrum e PC 286), ho ancor oggi la mania fissa di usare espressioni come "i & 31" (cioè un "and" fatto bit a bit) per ottenere il resto della divisione per 32 (per la quale uso invece espressioni come "i >> 5", cioè shift di cinque bit a destra).

Ecco dunque il calcolo: per ogni carattere i basterà fare:
  def puts x, y, str
    y += @ysiz  if y < 0          # "normalizza" le coordinate
    x += @xsiz  if x < 0

    str.each_byte do |i|
      next  if i < 32 || i > 127  # ignora i caratteri non 32-127

      a = (i&31) * @fxsiz         # calcola coordinate nella
      b = ((i>>5)-1) * @fysiz     #   bitmap del font

      for yp in 0...@fysiz        # plotta solo i pixel colorati
        for xp in 0...@fxsiz
          plot(x+xp, y+yp)  if @fnt.point(xp+a, yp+b)
        end
      end

      x += @fxsiz                 # avanza il cursore di un carattere
      unless x < @xsiz
        x = 0                     # fine riga? cursore alla successiva
        y += @fysiz
      end

      y = 0  if y >= @ysiz        # fine bitmap? cursore in cima
    end
  end

Argh! Funzione lunghissima... ma ancora decisamente semplice (e ulteriormente semplificabile!)

Certo, ogni cosa si può scrivere in tanti modi diversi. Ma una delle più simpatiche feature di Ruby è di poter dare un comando e poi aggiungere la clausola "solo se" oppure "a meno che": per esempio, "y=0 if y>=siz" (esegui "y=0" solo se "y era maggiore o uguale di...").

Il contrario della if è unless: "a meno che". Come sopra, "unless x < siz": esegui il blocco seguente tranne nel caso in cui x sia minore di siz.


Salvare in formato PBM: sarà mica una cosa difficile?

No, proprio no. Anzi, se come filename viene dato nil anziché una stringa, allora manda tutto su standard output. E durante il salvataggio, manda in output le singole righe.

Dato che Ruby ha una spettacolare garbage collection, possiamo comodamente creare un array di righe da salvare, spedirlo sul file indicato, e poi quando c'è l'end del metodo save non dobbiamo preoccuparci: Ruby penserà a sfrattare la memoria non più utilizzata (più "tecnico": quando il reference count dell'array appena creato diventa zero, significa che nessuno lo sta più usando, e perciò la sua memoria occupata è liberabile).

All'inizio creiamo nell'array x le tre righe iniziali per il file PBM: "P1" (che definisce il formato), "#." (commento di default), "640 480" (pardon, le dimensioni @xsiz,@ysiz della bitmap).

Notare con che comodità si crea una stringa con l'embedding delle espressioni: se all'interno delle virgolette compare una sequenza "#{ }" allora verrà valutata l'espressione al suo interno e rimpiazzata nella stringa.

Come, non capisci? Facciamo subito un esempio:
in Ruby:   puts "ci sono #{tot+1} elementi"
in C:      printf("ci sono %d elementi\n", tot+1);
in C++:    cout << "ci sono " << tot+1 << " elementi" << endl;

Ecco dunque il codice del metodo save. Osserviamo l'array f come viene creato... Magico Ruby! Dato che @mat è un array, allora lo possiamo trasformare in un array equivalente, ricollezionando (collect) ogni suo elemento i (originariamente true o false) come '1' oppure '0', dopodiché tutti questi uni e zeri li uniamo in un'unica stringa (join) separandoli con degli spazi (' ').
  def save filename
    x = [ "P1", "#.", "#{@xsiz} #{@ysiz}" ]

    f = @mat.collect { |i|  i ? '1' : '0' }.join(' ')

    n = @xsiz + @xsiz     # lunghezza di una riga di output
    a = n-1               # ultimo byte utile per estrarre la riga corrente
    k = 0                 # primo byte utile per estrarre la riga corrente
    while k < f.size      # finche' c'e' da leggere nella stringa:
      x << f[k..a].strip  # aggiungi all'output la riga corrente
      a, k = a+n, k+n     # aggiorna i puntatori
    end

    if filename
      File.open(filename, "w").puts(x)
    else
      STDOUT.puts x
    end
  end

Il ciclo while è solo uno dei tanti modi per estrarre, dalla stringa f (che rappresenta l'intera bitmap), le sequenze di uni e zeri (e relativi spazi accanto) che compongono una singola riga di output.

Esempio: se l'array @mat contenesse l'equivalente di 4 righe da 3 pixel ciascuno di cui "colorati" solo i quattro agli angoli, avremmo:

Magico Ruby! Non c'è bisogno di nessuna complessa sequenza per creare un file, scriverci dentro un array di stringhe separandole con un fine linea e poi chiuderlo: si fa tutto in una sola riga. Dalla classe File si invoca il metodo open (con filename e modalità "w", cioè "write"), al quale si applica poi la sua puts (la sua, non quella della classe Raster! duck-typing: quale sia la puts che ci serve, lo si capisce dal contesto). Naturalmente la puts sugli stream sa bene che in caso di array deve farne l'output di tutti gli elementi.

La STDOUT è una costante predefinita (come tutte le costanti in Ruby, comincia con una maiuscola).

Se avessimo voluto mandare in output senza il carriage-return a fine riga, avremmo usato la print anziché la puts.


Il bombardone finale: ecco il metodo load

Finalmente la mega-funzione (si fa per dire) per caricare un file PBM e convertirlo in un array di valori true/false così come serve alle funzioni sopra citate.

Magico Ruby! Notare come è facile prelevare il contenuto di un file in un array x: basta usare il metodo readlines applicato ad un File appena aperto in lettura (open). In tal caso avremo x[0] contenente la prima riga del file (compreso il suo "\n" finale), x[1] la seconda riga, e così via.

Piccolo Bug: cosa succederà se al posto del file PBM di input gli venisse passato un enorme malloppo di un giga, anzi, di quarantanove giga di testo? Ma dai...! :-)

Un controllino però glielo aggiungiamo: gli diciamo di uscire con clamore e subbuglio nel caso il file di input non sia un PBM in bianco e nero (cioè se la prima riga, ad eccezione di eventuale sequenza LF o CR/LF, è diversa da "P1").

Notare il recupero delle coordinate dalla terza riga (x[2]): prima chomp (elimina eventuali sequenze CR/LF), poi strip (elimina eventuali spazi iniziali e finali), poi split (che per default separa sulle sequenze di spazi, quindi avremo ora un array con due stringhe), poi collect (l'array di due stringhe viene valutato in due numeri), infine... l'operatore asterisco, che converte un array in una lista di numeri, per cui si può fare in una sola riga l'assegnamento multiplo, per cui il tutto sarà come aver fatto: @xsiz,@ysiz = 640,480 (e ora chiedo: quante righe di C++ ci vogliono per estrarre due numeri interi dalla terza riga di un file?)
  def load filename
    x = File.open(filename).readlines
    fail "il file non e' un PBM b/n"  if x.first.chomp != 'P1'
    @xsiz, @ysiz = * ( x[2].chomp.strip.split.collect { |i| eval i } )
    @mat = ( x[3..-1].
             collect { |i| i.chomp.strip }.
             join(' ') ).
           split.
           collect { |i|  i=='1' }
  end

L'assegnamento della matrice @mat è spettacolare: anzitutto prende l'array dalla quarta riga alla fine (dato che x[0] è la prima riga, allora la quarta riga sarà x[3]).

Dopodiché lo "colleziona", sostituendo ad ogni riga i il suo valore senza CR/LF finali (chomp) e senza spazi a destra e sinistra (strip).

Quindi lo unisce con la join, utilizzando gli spazi come giunzione.

Con queste operazioni, qualunque sia stata precedentemente l'organizzazione del file di input (spazi di troppo, CR, CR/LF, righe più o meno lunghe, etc), abbiamo ridotto tutto ad un'unica stringa di zeri e uni separati da spazi.

A questo punto possiamo dividerla (split), ottenendo un array di stringhe "0" e "1" e ricollezionare il tutto sostituendo agli zeri il false e agli uni il true.

Questo metodo parrà un pò brutale, ma garantisce la compatibilità permettendo di leggere sia i file salvati da noi (senza fronzoli), sia i file salvati da altri software (come il Graphic Converter o le utilities del Netpbm: provare per credere!)

A questo punto, col codice, abbiamo finito. C'è voluto un sacco di tempo per scrivere questa pagina, ma vi garantisco che per scrivere il solo codice mi è bastata una pausa pranzo: quasi tutti i metodi hanno funzionato al primo colpo, senza debugging, senza errori di sintassi - non per mia bravura, ma per la semplicità concettuale e sintattica di Ruby.

Anche su una macchina non particolarmente potente, questa classe lavora in tempi ragionevolmente brevi anche quando si trattano bitmap abbastanza grosse, per esempio di 1280×1024 pixel.


Esempio di utilizzo

Per provare un po' tutte le funzioni principali:
require 'raster'

img = Raster.new(256,192)

img.setfont("font7x14.pbm", 7,14)

img.puts 1,0, "Benvenuto!"
img.puts 115,155, "Questo e' un test"
img.scanline 19
img.scanline 21
img.scanline -5
img.scanline -2

img.blit 0,140,  img, 0,0,40,35
img.blit 1,140,  img, 0,0,40,35
img.blit(51,141, img, 0,0,40,35) { |a,b| a^b }

img.block 10,52,40,40
img.block(50,52,40,40) { |x,y| ((x+y)&1) != 0 }

for i in 0..3
  q = 80+i*3+1
  w = 120-i*7
  h = 78+i*5
  img.rect q+30,i*4,w,h
end

x, y, z = 130, 30, 45
for i in 0..1000
  img.plot x+rand(75), y+rand(30)
end

img.save "completato.pbm"

Non dimentichiamo alla fine di eseguire: pnmtopng < completato.pbm > completato.png


Conclusioni

Lo so, lo so che qualcuno correrà a dirsi perplesso su qualcosa, qualsiasi cosa.

Ma il codice che vedete sopra l'ho scritto durante una pausa pranzo (spesso con una delle due mani impegnata a reggere il panino). Sfido chiunque a scrivere in qualche altro linguaggio di programmazione qualcosa di altrettanto comprensibile, funzionante e manutenibile in così poco tempo e così poche righe di codice.

Ruby è un linguaggio interpretato, con tutti i suoi svantaggi (uno solo: la velocità di esecuzione quando si tratta di risolvere problemi molto complessi), e i suoi vantaggi (una vera marea, impossibili da elencare tutti qui).

Altri semplici esempi di programmazione Ruby sono qui.