Compresión de arte ASCII con pérdida


21

Fondo

PICASCII es una herramienta ordenada que convierte imágenes en arte ASCII.

Alcanza diferentes grados de brillo utilizando los siguientes diez caracteres ASCII:

@#+';:,.` 

Diremos que estos charxels (elementos de carácter) tienen brillos de 1 (en el signo) a 10 (espacio).

A continuación, puede ver los resultados de la conversión de un pequeño código, la bandera galesa, un fractal sobrecargado, una trucha grande y un pequeño golf, que se muestran con la fuente correcta:

Arte ASCII

Puede ver las imágenes en este violín y descargarlas de Google Drive .

Tarea

Si bien los resultados finales de PICASCII son visualmente agradables, las cinco imágenes combinadas pesan 153.559 bytes. ¿Cuánto podrían comprimirse estas imágenes si estamos dispuestos a sacrificar parte de su calidad?

Su tarea es escribir un programa que acepte una imagen artística ASCII como las anteriores y una calidad mínima como entrada e imprima una compresión con pérdida de la imagen, en forma de un programa completo o una función que devuelve una sola cadena, que satisfaga el Requisito de calidad.

Esto significa que no puede escribir un descompresor separado; debe estar integrado en cada una de las imágenes comprimidas.

La imagen original consistirá en charxels con brillos entre 1 y 10, separados por avances de línea en líneas de la misma longitud. La imagen comprimida debe tener las mismas dimensiones y usar el mismo conjunto de caracteres.

Para una imagen sin comprimir que consta de n charxels, la calidad de una versión comprimida de la imagen se define como

fórmula de calidad

donde c i es el brillo del i th charxel de la salida de la imagen comprimida y u i el brillo del i th charxel de la imagen sin comprimir.

Tanteo

Su código se ejecutará con las cinco imágenes de arriba como configuraciones de entrada y calidad mínima de 0.50, 0.60, 0.70, 0.80 y 0.90 para cada una de las imágenes.

Su puntaje es la media geométrica de los tamaños de todas las imágenes comprimidas, es decir, la raíz vigésimo quinta del producto de las longitudes de las veinticinco imágenes comprimidas.

¡El puntaje más bajo gana!

Reglas adicionales

  • Su código tiene que funcionar para imágenes arbitrarias, no solo las que se usan para la puntuación.

    Se espera que optimice su código para los casos de prueba, pero un programa que ni siquiera intenta comprimir imágenes arbitrarias no recibirá un voto positivo de mi parte.

  • Su compresor puede usar compresores de flujo de bytes integrados (por ejemplo, gzip), pero debe implementarlos usted mismo para las imágenes comprimidas.

    Los bulit-ins normalmente utilizados en descompresores de flujo de bytes (por ejemplo, conversión de base, decodificación de longitud de ejecución) están permitidos.

  • El compresor y las imágenes comprimidas no tienen que estar en el mismo idioma.

    Sin embargo, debe elegir un solo idioma para todas las imágenes comprimidas.

  • Para cada imagen comprimida, se aplican reglas de golf de código estándar.

Verificación

Hice un script de CJam para verificar fácilmente todos los requisitos de calidad y calcular el puntaje de un envío.

Puede descargar el intérprete de Java desde aquí o aquí .

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

Ejemplo

Bash → PHP, puntaje 30344.0474

cat

Logra 100% de calidad para todas las entradas.

$ 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

Tengo problemas para entender esta parte: si alguien elige q = 0.5, entonces cada carácter en el archivo de entrada debe ser reemplazado por el carácter con la mitad del brillo en la salida, ¿verdad? Obviamente excluyendo el espacio en blanco ya que eso estropearía toda la imagen.
Nicolás Siplis

1
Es todo demasiado confuso y vacío. ¿Cómo se detiene una entrada de mattmahoney.net/dc/barf.html ? ¿El descompresor puede leer cualquier archivo que no sea la imagen comprimida también? ¿Puede proporcionar una secuencia de comandos de Python o algo que realmente verifique la calidad de una imagen y calcule una puntuación para que no haya objeciones en ese frente también? Etc.
Será el

1
@ ¿Será confuso? Tal vez. Pero no creo que sea la laguna. Cada imagen comprimida tiene que ser un programa o función, por lo que los chistes no divertidos como BARF se excluyen automáticamente. No conozco Python, pero pensaré en algo para la verificación simple.
Dennis

8
"Hice un script CJam para verificar fácilmente todos los requisitos de calidad y calcular la puntuación de una presentación". ¿La gente realmente usa esto para hacer scripts normales? Estimado señor ...
Fatalize

Respuestas:


4

Java → CJam, puntaje ≈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();
        }
    }
}

Requiere el jar de CJam en el classpath. Si le da 2 argumentos de línea de comando (nombre y calidad del archivo), agrega ".cjam" al nombre del archivo y escribe allí la imagen comprimida. De lo contrario, calcula su puntuación en las 5 imágenes de prueba, que se supone que están en el directorio actual. El programa también verifica automáticamente cada imagen comprimida. Es posible que desee verificar dos veces el cálculo de puntaje en caso de que haya alguna discrepancia.

Las técnicas utilizadas (hasta ahora) son: escalar a la mitad (horizontalmente, verticalmente o ambas) si no reduce demasiado la calidad, un RLE codificado a medida y una conversión de base para empaquetar más datos en cada personaje mientras permanece en el gama ASCII imprimible.


¿Podría darme una breve descripción de cómo ejecutar esto? Lo compilé (con éxito, creo) con javac -cp cjam-0.6.5.jar Compress.java, pero java -cp cjam-0.6.5.jar Compressdice Error: Could not find or load main class Compressy java Compressno encuentra la clase CJam.
Dennis

@Dennis Debe agregar el directorio que contiene Compress.class al classpath (-cp). Si está en el directorio actual, use -cp .:cjam-0.6.5.jar(en Windows, creo que necesita un punto y coma en lugar de dos puntos)
aditsu

Eso hizo el truco, gracias.
Dennis

2

Python 3.5 (principal y de salida) (actualmente sin competencia)

Feliz cumpleaños, desafío! Aquí está tu regalo: una respuesta!

EDITAR: Salida convertida a código python, tasa de compresión mejorada (ligeramente) EDITAR2: Hizo que se imprima sin formato cuando sizees 1. Puntaje mejorado, pero el puntaje debe calcularse nuevamente. EDIT3: @Dennis señaló que todavía tengo errores que corregir, así que marqué la respuesta como no competitiva

Código:

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]))

Esta respuesta podría usar muchas mejoras, por lo que probablemente trabajaré más durante el fin de semana.

Cómo funciona esto:

  • Divide la imagen en bloques de tamaño size.
  • Encuentra el mejor bloque coincidente
    • ¡Los bloques pueden tener gradiente ahora!
  • Calcular la calidad (de acuerdo con la fórmula) para toda la imagen.
  • Si es correcto, escriba la imagen comprimida en el archivo.
  • De lo contrario, disminuya sizee intente nuevamente.

Este algoritmo funciona bien para baja calidad (0.5, 0.6) pero no funciona muy bien en las imágenes de mayor calidad (en realidad se infla). También es muy lento.

Aquí tengo todos los archivos generados, por lo que no tendrá que volver a generarlos nuevamente.


Finalmente, una respuesta! Sin embargo, técnicamente no compite, ya que creé Bubblegum después de publicar este desafío ... Ejecutaré el script de puntuación más tarde y tal vez lo porte a un lenguaje menos esotérico.
Dennis

@ Dennis Ah, bueno, no debería ser demasiado difícil portar la salida a un script de Python. Gracias por el aviso
Azul

Acabo de volver a leer mi desafío (después de un año, estaba un poco confuso en los detalles), y dice que su compresor puede usar compresores de flujo de bytes integrados (por ejemplo, gzip), pero debe implementarlos usted mismo para el Imágenes comprimidas. Eso significa que Bubblegum está fuera de todos modos.
Dennis

Finalmente recordé que había prometido marcar esto; Perdón por el retraso. Su código parece tener un error tipográfico ( compingdebería ser compimg), que arreglé para ejecutar el programa. A menos que haya cometido un error al ejecutar su código, las dimensiones de algunas de las imágenes generadas son incorrectas (por ejemplo, image2.txttiene 33,164 bytes, pero image2-50.0.txttiene 33,329) y otras no generan el mismo archivo al ejecutar los programas generados ( image3-50.0.txttiene una calidad de 0.5110 , pero ejecutar el programa generado da como resultado una calidad de 0.4508 ).
Dennis

Anexo: Lo descargué image3-50.0.pyde su Dropbox y coincide con el archivo que generé.
Dennis
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.