Compressione di arte ASCII con perdita


21

sfondo

PICASCII è uno strumento accurato che converte le immagini in arte ASCII.

Raggiunge diversi gradi di luminosità usando i seguenti dieci caratteri ASCII:

@#+';:,.` 

Diremo che questi charxel (elementi del personaggio) hanno una luminosità da 1 (a segno) a 10 (spazio).

Di seguito, puoi vedere i risultati della conversione di un piccolo codice, la bandiera gallese, un frattale rovesciato, una grande trota e un piccolo golf, visualizzati con il carattere corretto:

ASCII art

Puoi vedere le immagini in questo violino e scaricarle da Google Drive .

Compito

Mentre i risultati finali di PICASCII sono visivamente gradevoli, tutte e cinque le immagini combinate pesano 153.559 byte. Quanto potrebbero essere compresse queste immagini se siamo disposti a sacrificare parte della loro qualità?

Il tuo compito è quello di scrivere un programma che accetta un'immagine d'arte ASCII come quelle sopra e una qualità minima come input e stampare una compressione con perdita dell'immagine - sotto forma di un programma completo o una funzione che restituisce una singola stringa - che soddisfi il requisito di qualità.

Ciò significa che non si riesce a scrivere un decompressore separato; deve essere incorporato in ciascuna delle immagini compresse.

L'immagine originale sarà composta da charxel con luminosità compresa tra 1 e 10, separate da avanzamenti di riga in linee della stessa lunghezza. L'immagine compressa deve avere le stesse dimensioni e utilizzare lo stesso set di caratteri.

Per un'immagine non compressa composta da n charxel, la qualità di una versione compressa dell'immagine è definita come

formula di qualità

dove c i è la luminosità della i esima charxel dell'uscita dell'immagine compressa e u i la luminosità della i esima charxel dell'immagine non compressa.

punteggio

Il codice verrà eseguito con le cinque immagini dall'alto come input e impostazioni di qualità minima di 0,50, 0,60, 0,70, 0,80 e 0,90 per ciascuna delle immagini.

Il tuo punteggio è la media geometrica delle dimensioni di tutte le immagini compresse, ovvero la venticinquesima radice del prodotto delle lunghezze di tutte le venticinque immagini compresse.

Vince il punteggio più basso!

Regole aggiuntive

  • Il tuo codice deve funzionare per immagini arbitrarie, non solo per quelle utilizzate per il punteggio.

    Si prevede che ottimizzi il tuo codice verso i casi di test, ma un programma che non tenta nemmeno di comprimere immagini arbitrarie non otterrà un mio voto.

  • Il compressore può utilizzare compressori di flusso di byte integrati (ad es. Gzip), ma è necessario implementarli da soli per le immagini compresse.

    Sono consentiti bulit-in normalmente utilizzati nei decompressori di flussi di byte (ad es. Conversione di base, decodifica di lunghezza di esecuzione).

  • Il compressore e le immagini compresse non devono essere nella stessa lingua.

    Tuttavia, è necessario scegliere una sola lingua per tutte le immagini compresse.

  • Per ogni immagine compressa, si applicano le regole standard per il golf del codice.

Verifica

Ho realizzato uno script CJam per verificare facilmente tutti i requisiti di qualità e calcolare il punteggio di un invio.

È possibile scaricare l'interprete Java da qui o qui .

e# URLs of the uncompressed images.
e# "%s" will get replaced by 1, 2, 3, 4, 5.

"file:///home/dennis/codegolf/53199/original/image%s.txt"

e# URLs of the compressed images (source code).
e# "%s-%s" will get replaced by "1-50", "1-60", ... "5-90".

"file:///home/dennis/codegolf/53199/code/image%s-%s.php"

e# URLs of the compressed images (output).

"file:///home/dennis/codegolf/53199/output/image%s-%s.txt"

e# Code

:O;:C;:U;5,:)
{
    5,5f+Af*
    {
        C[IQ]e%g,X*:X;
        ISQS
        [U[I]e%O[IQ]e%]
        {g_W=N&{W<}&}%
        _Nf/::,:=
        {
            {N-"@#+';:,.` "f#}%z
            _::m2f#:+\,81d*/mq1m8#
            _"%04.4f"e%S
            @100*iQ<"(too low)"*
        }{
            ;"Dimension mismatch."
        }?
        N]o
    }fQ
}fI
N"SCORE: %04.4f"X1d25/#e%N

Esempio

Bash → PHP, punteggio 30344.0474

cat

Raggiunge il 100% di qualità per tutti gli input.

$ java -jar cjam-0.6.5.jar vrfy.cjam
1 50 1.0000 
1 60 1.0000 
1 70 1.0000 
1 80 1.0000 
1 90 1.0000 
2 50 1.0000 
2 60 1.0000 
2 70 1.0000 
2 80 1.0000 
2 90 1.0000 
3 50 1.0000 
3 60 1.0000 
3 70 1.0000 
3 80 1.0000 
3 90 1.0000 
4 50 1.0000 
4 60 1.0000 
4 70 1.0000 
4 80 1.0000 
4 90 1.0000 
5 50 1.0000 
5 60 1.0000 
5 70 1.0000 
5 80 1.0000 
5 90 1.0000 

SCORE: 30344.0474

Ho qualche problema a capire questa parte: se qualcuno sceglie q = 0,5, allora ogni carattere nel file di input dovrebbe essere sostituito dal carattere con metà della luminosità nell'output, giusto? Ovviamente escludendo lo spazio bianco, ciò rovinerebbe l'intera immagine.
Nicolás Siplis,

1
È tutto troppo confuso e scappatoia. Come si interrompe una voce mattmahoney.net/dc/barf.html ? Il decompressore può leggere anche file diversi dall'immagine compressa? Puoi fornire uno script Python o qualcosa che controlla effettivamente la qualità di un'immagine e calcola un punteggio in modo che non ci possano essere cavilli anche su quel fronte? Etc.
Will

1
@Will Confusion? Può essere. Ma non credo sia una lacuna. Ogni immagine compressa deve essere un programma o una funzione, quindi le battute poco divertenti come BARF vengono automaticamente escluse. Non conosco Python, ma penserò a qualcosa di semplice da verificare.
Dennis,

8
"Ho realizzato uno script CJam per verificare facilmente tutti i requisiti di qualità e calcolare il punteggio di un invio." Le persone usano davvero questa cosa per fare normali script? Caro signore ...
Fatalizza il

Risposte:


4

Java → CJam, punteggio ≈4417,89

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import net.aditsu.cjam.CJam;

public class Compress {
    protected static final char[] DIGITS = "0123456789ABCDEFGHIJK".toCharArray();
    protected static final String CHARS = "@#+';:,.` ";
    protected static final char[] CHR = CHARS.toCharArray();

    private static class Img {
        public final int rows;
        public final int cols;
        public final int[][] a;

        public Img(final int rows, final int cols) {
            this.rows = rows;
            this.cols = cols;
            a = new int[rows][cols];
        }

        public Img(final List<String> l) {
            rows = l.size();
            cols = l.get(0).length();
            a = new int[rows][cols];
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    a[i][j] = CHARS.indexOf(l.get(i).charAt(j));
                }
            }
        }

        public static Img read(final Reader r) {
            try {
                final BufferedReader br = new BufferedReader(r);
                final List<String> l = new ArrayList<>();
                while (true) {
                    final String s = br.readLine();
                    if (s == null || s.isEmpty()) {
                        break;
                    }
                    l.add(s);
                }
                br.close();
                return new Img(l);
            }
            catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        public static Img read(final File f) {
            try {
                return read(new FileReader(f));
            }
            catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        public Img scaleDown(final int fr, final int fc) {
            final int r1 = (rows + fr - 1) / fr;
            final int c1 = (cols + fc - 1) / fc;
            final Img x = new Img(r1, c1);
            final int[][] q = new int[r1][c1];
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    x.a[i / fr][j / fc] += a[i][j];
                    q[i / fr][j / fc]++;
                }
            }
            for (int i = 0; i < r1; ++i) {
                for (int j = 0; j < c1; ++j) {
                    x.a[i][j] /= q[i][j];
                }
            }
            return x;
        }

        public Img scaleUp(final int fr, final int fc) {
            final int r1 = rows * fr;
            final int c1 = cols * fc;
            final Img x = new Img(r1, c1);
            for (int i = 0; i < r1; ++i) {
                for (int j = 0; j < c1; ++j) {
                    x.a[i][j] = a[i / fr][j / fc];
                }
            }
            return x;
        }

        public Img crop(final int r, final int c) {
            if (r == rows && c == cols) {
                return this;
            }
            final Img x = new Img(r, c);
            for (int i = 0; i < r; ++i) {
                for (int j = 0; j < c; ++j) {
                    x.a[i][j] = a[i][j];
                }
            }
            return x;
        }

        public Img rescale(final int fr, final int fc) {
            return scaleDown(fr, fc).scaleUp(fr, fc).crop(rows, cols);
        }

        public double quality(final Img x) {
            if (x.rows != rows || x.cols != cols) {
                throw new IllegalArgumentException();
            }
            double t = 0;
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    final int y = a[i][j] - x.a[i][j];
                    t += y * y;
                }
            }
            t /= 81 * rows * cols;
            t = 1 - Math.sqrt(t);
            return Math.pow(t, 8);
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    sb.append(CHR[a[i][j]]);
                }
                sb.append('\n');
            }
            return sb.toString();
        }

        public Array toArray() {
            final Array x = new Array(rows * cols);
            int k = 0;
            for (int i = 0; i < rows; ++i) {
                for (int j = 0; j < cols; ++j) {
                    x.a[k++] = a[i][j];
                }
            }
            return x;
        }

        public String compress(final double quality) {
            int bi = 1;
            int bj = 1;
            int bs = rows * cols;
            Img bx = this;

            for (int i = 1; i < 3; ++i) {
                for (int j = 1; j < 3; ++j) {
                    Img x = rescale(i, j);
                    if (quality(x) >= quality) {
                        x = scaleDown(i, j);
                        if (x.rows * x.cols < bs) {
                            bi = i;
                            bj = j;
                            bs = x.rows * x.cols;
                            bx = x;
                        }
                    }
                }
            }

            Array a = bx.toArray();
            int bf = 0;
            for (int i = 1; i <= 20; ++i) {
                final int t = a.rle11(i).n;
                if (t < bs) {
                    bs = t;
                    bf = i;
                }
            }

            int b = 10;
            if (bf > 0) {
                b = 11;
                a = a.rle11(bf);
            }

            String s = null;
            for (int i = 92; i < 97; ++i) {
                for (char c = ' '; c < '$'; ++c) {
                    final String t = a.cjamBase(b, i, c);
                    boolean ok = true;
                    for (int j = 0; j < t.length(); ++j) {
                        if (t.charAt(j) > '~') {
                            ok = false;
                            break;
                        }
                    }
                    if (!ok) {
                        continue;
                    }
                    if (s == null || t.length() < s.length()) {
                        s = t;
                    }
                }
            }

            if (bf > 0) {
                s += "{(_A={;()";
                if (bf > 1) {
                    s += DIGITS[bf] + "*";
                }
                s += "\\(a@*}&\\}h]e_";
            }
            if (bi * bj == 1) {
                return s + '"' + CHARS + "\"f=" + cols + "/N*";
            }
            s += bx.cols + "/";
            if (bi > 1) {
                s += bi + "e*";
                if (rows % 2 == 1) {
                    s += "W<";
                }
            }
            if (bj > 1) {
                s += bj + "fe*";
                if (cols % 2 == 1) {
                    s += "Wf<";
                }
            }
            return s + '"' + CHARS + "\"ff=N*";
        }

        public void verify(final String s, final double quality) {
            final String t = CJam.run(s, "");
            final Img x = read(new StringReader(t));
            final double q = quality(x);
            if (q < quality) {
                throw new RuntimeException(q + " < " + quality);
            }
//          System.out.println(q + " >= " + quality);
        }
    }

    private static class Array {
        public final int[] a;
        public final int n;

        public Array(final int n) {
            this.n = n;
            a = new int[n];
        }

        public Array(final int[] a) {
            this.a = a;
            n = a.length;
        }

        public String join() {
            final StringBuilder sb = new StringBuilder();
            for (int x : a) {
                sb.append(x).append(' ');
            }
            sb.setLength(sb.length() - 1);
            return sb.toString();
        }

//      public String cjamStr() {
//          final StringBuilder sb = new StringBuilder("\"");
//          for (int x : a) {
//              sb.append(DIGITS[x]);
//          }
//          sb.append("\":~");
//          return sb.toString();
//      }

        public String cjamBase(final int m, final int b, final char c) {
            final boolean zero = a[0] == 0;
            String s = join();
            if (zero) {
                s = "1 " + s;
            }
            s = CJam.run("q~]" + m + "b" + b + "b'" + c + "f+`", s);
            s += "'" + c + "fm" + b + "b" + DIGITS[m] + "b";
            if (zero) {
                s += "1>";
            }
            return s;
        }

        public Array rle11(final int f) {
            final int[] b = new int[n];
            int m = 0;
            int x = -1;
            int k = 0;
            for (int i = 0; i <= n; ++i) {
                final int t = i == n ? -2 : a[i];
                if (t == x && m < 11 * f) {
                    m++;
                }
                else {
                    if (m >= f && m > 3) {
                        b[k++] = 10;
                        b[k++] = m / f - 1;
                        b[k++] = x;
                        for (int j = 0; j < m % f; ++j) {
                            b[k++] = x;
                        }
                    }
                    else {
                        for (int j = 0; j < m; ++j) {
                            b[k++] = x;
                        }
                    }
                    m = 1;
                    x = t;
                }
            }
            return new Array(Arrays.copyOf(b, k));
        }
    }

    private static void score() {
        double p = 1;
        for (int i = 1; i < 6; ++i) {
            final File f = new File("image" + i + ".txt");
            final Img img = Img.read(f);
            final int n = (int) f.length();
            for (int j = 5; j < 10; ++j) {
                final double q = j / 10.0;
                final String s = img.compress(q);
                System.out.println(f.getName() + ", " + q + ": " + n + " -> " + s.length());
                img.verify(s, q);
                p *= s.length();
            }
        }
        System.out.println(Math.pow(p, 1 / 25.0));
    }

    public static void main(final String... args) {
        if (args.length != 2) {
            score();
            return;
        }
        final String fname = args[0];
        final double quality = Double.parseDouble(args[1]);
        try {
            final Img img = Img.read(new File(fname));
            final String s = img.compress(quality);
            img.verify(s, quality);
            final FileWriter fw = new FileWriter(fname + ".cjam");
            fw.write(s);
            fw.close();
        }
        catch (IOException e) {
            throw new RuntimeException();
        }
    }
}

Richiede il vaso CJam nel percorso di classe. Se gli dai 2 argomenti da riga di comando (nome e qualità del file), aggiunge ".cjam" al nome del file e lì scrive l'immagine compressa. Altrimenti calcola il suo punteggio sulle 5 immagini di prova, che si presume si trovino nella directory corrente. Il programma verifica inoltre automaticamente ogni immagine compressa. Potresti voler ricontrollare il calcolo del punteggio in caso di discrepanze.

Le tecniche utilizzate (finora) sono: ridimensionamento alla metà (orizzontale, verticale o entrambi) se non riduce troppo la qualità, un RLE con codice personalizzato e la conversione di base per comprimere più dati in ciascun carattere rimanendo nel intervallo ASCII stampabile.


Potresti darmi una breve panoramica su come eseguire questo? L'ho compilato (con successo, credo) con javac -cp cjam-0.6.5.jar Compress.java, ma java -cp cjam-0.6.5.jar Compressdice Error: Could not find or load main class Compresse java Compressnon trova la classe CJam.
Dennis,

@Dennis Devi aggiungere la directory che contiene Compress.class al classpath (-cp). Se si trova nella directory corrente, utilizzare -cp .:cjam-0.6.5.jar(nel windoze penso che tu abbia bisogno di un punto e virgola anziché di due punti)
aditsu,

Questo ha funzionato, grazie.
Dennis,

2

Python 3.5 (principale e output) (attualmente non competitivo)

Buon compleanno, sfida! Ecco il tuo regalo: una risposta!

EDIT: output convertito in codice Python, velocità di compressione migliorata (leggermente) EDIT2: reso grezzo quando sizeè 1. Punteggio migliorato, ma il punteggio deve essere nuovamente calcolato. EDIT3: @Dennis ha sottolineato che ho ancora bug da correggere, quindi ho contrassegnato la risposta come non competitiva

Codice:

import sys
LIST = [' ','`','.',',',':',';',"'",'+','#','@']

def charxel_to_brightness(charxel):
    return LIST.index(charxel)

def brightness_to_charxel(bright):
    return LIST[bright]

def image_to_brightness(imagetext):
    return [list(map(charxel_to_brightness,line)) for line in imagetext.split("\n")]

def brightness_to_image(brightarray):
    return '\n'.join([''.join(map(brightness_to_charxel,line)) for line in brightarray])

def split_into_parts(lst,size):
    return [lst[x:x+size] for x in range(0, len(lst), size)]

def gen_updown(startxel,endxel,size):
    return [[int((size-r)*(endxel-startxel)/size+startxel) for c in range(size)] for r in range(size)]

def gen_leftright(startxel,endxel,size):
    return [[int((size-c)*(endxel-startxel)/size+startxel) for c in range(size)] for r in range(size)]

def gen_tlbr(startxel,endxel,size):
    return [[int((2*size-r-c)/2*(endxel-startxel)/size+startxel) for c in range(size)] for r in range(size)]

def gen_bltr(startxel,endxel,size):
    return [[int((size-r+c)/2*(endxel-startxel)/size+startxel) for c in range(size)] for r in range(size)]

def gen_block(code,startxel,endxel,size):
    if code==0:return gen_updown(startxel,endxel,size)
    if code==1:return gen_leftright(startxel,endxel,size)
    if code==2:return gen_bltr(startxel,endxel,size)
    if code==3:return gen_tlbr(startxel,endxel,size)

def vars_to_data(code,startxel,endxel):
    acc=endxel
    acc+=startxel<<4
    acc+=code<<8
    return acc

def data_to_vars(data):
    code=data>>8
    startxel=(data>>4)&15
    endxel=data&15
    return code,startxel,endxel

def split_into_squares(imgarray,size):
    rows = split_into_parts(imgarray,size)
    allsquares = []
    for rowblock in rows:
        splitrows = []
        for row in rowblock:
            row = split_into_parts(row,size)
            splitrows.append(row)
        rowdict = []
        for row in splitrows:
            for x in range(len(row)):
                if len(rowdict)<=x:
                    rowdict.append([])
                rowdict[x].append(row[x])
        allsquares.append(rowdict)
    return allsquares

def calc_quality(imgarray,comparray):
    acc=0
    for row in range(len(imgarray)):
        for col in range(len(imgarray[row])):
            acc+=pow(imgarray[row][col]-comparray[row][col],2)
    return (1-(acc/81.0/sum([len(row) for row in imgarray]))**.5)**8

def fuse_squares(squarray):
    output=[]
    counter=0
    scounter=0
    sqrow=0
    while sqrow<len(squarray):
        if scounter<len(squarray[sqrow][0]):
            output.append([])
            for square in squarray[sqrow]:
                output[counter].extend(square[scounter])
            scounter+=1
            counter+=1
        else:
            scounter=0
            sqrow+=1
    return output

def main_calc(imgarray,threshold):
    imgarray = image_to_brightness(imgarray)
    size = 9
    quality = 0
    compimg=[]
    datarray=[]
    testdata = [vars_to_data(c,s,e) for c in range(4) for s in range(10) for e in range(10)]
    while quality<threshold:
        squares = split_into_squares(imgarray,size)
        compimg = []
        datarray = []
        testblock = [gen_block(c,s,e,size) for c in range(4) for s in range(10) for e in range(10)]
        for row in squares:
            comprow = []
            datrow=[]
            for square in row:
                quality_values = [calc_quality(square,block) for block in testblock]
                best_quality = quality_values.index(max(quality_values))
                comprow.append(testblock[best_quality])
                datrow.append(testdata[best_quality])
            compimg.append(comprow)
            datarray.append(datrow)
        compimg = fuse_squares(compimg)
        quality = calc_quality(imgarray,compimg)
        print("Size:{} Quality:{}".format(size,quality))
        size-=1
    return brightness_to_image(compimg),datarray,size+1

template = '''def s(d,s,e,z):
 x=range(z)
 return d<1 and[[int((z-r)*(e-s)/z+s)for c in x]for r in x]or d==1 and[[int((z-c)*(e-s)/z+s)for c in x]for r in x]or d==2 and[[int((2*z-r-c)/2*(e-s)/z+s)for c in x]for r in x]or d>2 and[[int((z-r+c)/2*(e-s)/z+s)for c in x] for r in x]
i=lambda a:'\\n'.join([''.join(map(lambda r:" `.,:;'+#@"[r],l))for l in a])
def f(a):
 o=[];c=0;s=0;r=0
 while r<len(a):
  if s<len(a[r][0]):
   o.append([])
   for q in a[r]:
    o[c].extend(q[s])
   s+=1;c+=1
  else:
   s=0;r+=1
 return o
t={};z={}
print(i(f([[s(D>>8,(D>>4)&15,D&15,z)for D in R]for R in t])))'''

template_size_1 = '''print("""{}""")'''   

def main(filename,threshold):
    print(filename+" "+str(threshold))
    file = open(filename,'r')
    compimg,datarray,size = main_calc(file.read(),threshold)
    file.close()
    textoutput = open(filename.split(".")[0]+"-"+str(threshold*100)+".txt",'w')
    textoutput.write(compimg)
    textoutput.close()
    compoutput = open(filename.split(".")[0]+"-"+str(threshold*100)+".py",'w')
    datarray = str(datarray).replace(" ","")
    code = ""
    if size==1:
        code = template_size_1.format(compimg)
    else:
        code= template.format(datarray,str(size))
    compoutput.write(code)
    compoutput.close()
    print("done")

if __name__ == "__main__":
    main(sys.argv[1],float(sys.argv[2]))

Questa risposta potrebbe utilizzare molti miglioramenti, quindi probabilmente ci lavorerò di più nel fine settimana.

Come funziona:

  • Dividi l'immagine in blocchi di dimensioni size.
  • Trova il miglior blocco corrispondente
    • I blocchi possono avere gradiente ora!
  • Calcola la qualità (secondo la formula) per l'intera immagine.
  • Se corretto, scrivi l'immagine zippata sul file.
  • Altrimenti, decrementa sizee riprova.

Questo algoritmo funziona bene per bassa qualità (0,5, 0,6) ma non funziona troppo bene su immagini di qualità superiore (si gonfia effettivamente). È anche molto lento.

Qui ho tutti i file generati, quindi non dovrai rigenerarli di nuovo.


Finalmente una risposta! È tecnicamente non competitivo, dal momento che ho creato Bubblegum dopo aver pubblicato questa sfida ... Eseguirò lo script di punteggio in seguito e forse lo porterò in un linguaggio meno esoterico.
Dennis,

@Dennis Ah, beh, non dovrebbe essere troppo difficile trasferire l'output su uno script Python. Grazie per l'heads-up
Blue

Ho appena riletto la mia sfida (dopo un anno, ero un po 'confuso sui dettagli), e dice che il tuo compressore può usare compressori di flusso di byte integrati (ad esempio, gzip), ma devi implementarli tu stesso per il immagini compresse. Ciò significa che Bubblegum è comunque fuori.
Dennis,

Alla fine mi sono ricordato che avevo promesso di segnare questo; scusa per il ritardo. Il tuo codice sembra avere un refuso ( compingdovrebbe essere compimg), che ho corretto per eseguire il programma. A meno che non abbia commesso un errore durante l'esecuzione del codice, le dimensioni di alcune delle immagini generate sono errate (ad esempio, image2.txtha 33.164 byte, ma image2-50.0.txtha 33.329) e altre non generano lo stesso file durante l'esecuzione dei programmi generati ( image3-50.0.txtha una qualità di 0,5110 , ma l'esecuzione del programma generato produce una qualità di 0.4508 ).
Dennis,

Addendum: ho scaricato image3-50.0.pydal tuo Dropbox e corrisponde al file che ho generato.
Dennis,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.