Стиснення мистецтва ASCII втрати


21

Фон

PICASCII - це акуратний інструмент, який перетворює зображення в мистецтво ASCII.

Він досягає різних ступенів яскравості, використовуючи наступні десять символів ASCII:

@#+';:,.` 

Ми скажемо, що ці шрифли (символьні елементи) мають яскравість від 1 (за знаком) до 10 (пробіл).

Нижче ви можете побачити результати перетворення невеликого коду, валлійського прапора, великого фрактала, великої форелі та маленького гольфу, відображеного правильним шрифтом:

ASCII ст

Ви можете побачити зображення в цій скрипці та завантажити їх з Google Диска .

Завдання

Незважаючи на те, що кінцеві результати PICASCII візуально приємні, усі п’ять зображень у сукупності важать 153 559 байт. Наскільки ці зображення можна стиснути, якщо ми готові пожертвувати частиною їх якості?

Ваше завдання полягає в тому, щоб написати програму, яка приймає зображення ASCII типу мистецтва, як зазначені вище, і мінімальну якість, як вхідні дані, і друкує стиснене зображення у вигляді втрати - у вигляді повної програми або функції, що повертає одну рядок - що задовольняє вимога до якості.

Це означає, що вам не вдається написати окремий декомпресор; він повинен бути вбудований у кожне зі стислих зображень.

Оригінальне зображення буде складатися із шарклів із яскравістю від 1 до 10, розділених стрічковими каналами на лінії однакової довжини. Стиснене зображення повинно мати однакові розміри і використовувати один і той же набір символів.

Для нестисненого зображення, що складається з п шекселів, якість стисненої версії зображення визначається як

формула якості

де c i - яскравість i- го шарксела на виході стисненого зображення, а u i - яскравість i- го шарксела нестисненого зображення.

Оцінка балів

Ваш код буде виконуватись із п’ятьма зображеннями зверху як параметри введення та мінімальної якості - 0,50, 0,60, 0,70, 0,80 та 0,90 для кожного із зображень.

Ваш бал - це геометричне середнє значення розмірів усіх стислих зображень, тобто двадцять п’ятий корінь добутку довжини всіх двадцяти п’яти стислих зображень.

Виграє найнижчий бал!

Додаткові правила

  • Ваш код повинен працювати для довільних зображень, а не тільки для тих, що використовуються для підрахунку.

    Очікується, що ви оптимізуєте свій код до тестових випадків, але програма, яка навіть не намагається стиснути довільні зображення, не отримає відгуку від мене.

  • Ваш компресор може використовувати вбудовані компресори потоку байтів (наприклад, gzip), але вам доведеться їх реалізувати самостійно для стислих зображень.

    Допускаються буліти, зазвичай використовувані в декомпресорах потоку байтів (наприклад, перетворення бази, декодування довжини пробігу).

  • Компресорні та стислі зображення не повинні бути однаковою мовою.

    Однак для всіх стислих зображень потрібно вибрати одну мову.

  • Для кожного стисненого зображення застосовуються стандартні правила гольф-коду.

Перевірка

Я створив сценарій CJam, щоб легко перевірити всі вимоги до якості та обчислити бал подання.

Ви можете завантажити інтерпретатор Java звідси або тут .

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

Приклад

Bash → PHP, оцінка 30344.0474

cat

Досягає 100% якості для всіх матеріалів.

$ 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

У мене виникають деякі проблеми з розумінням цієї частини: Якщо хтось обирає q = 0,5, то кожну таблицю у вхідному файлі слід замінити знаком із половиною яскравості у виході, правда? Очевидно, що виключити пробіл, оскільки це зіпсує все зображення.
Nicolás Siplis

1
Його все занадто заплутано і лазівка. Як зупинити запис mattmahoney.net/dc/barf.html ? Чи може декомпресор прочитати будь-який файл, окрім стисненого зображення? Чи можете ви надати сценарій python або щось, що насправді перевіряє якість зображення та обчислює бал, щоб на цьому фронті не було також жодних кайф? І т. Д.
буде

1
@ Будеш заплутаною? Можливо. Але я не думаю, що це лазівка. Кожне стиснене зображення повинно бути програмою або функцією, тому невмілі жарти на зразок BARF автоматично виключаються. Я не знаю Python, але я придумаю щось просте для перевірки.
Денніс

8
"Я створив сценарій CJam, щоб легко перевірити всі вимоги до якості та обчислити бал подання." Чи справді люди використовують цю річ, щоб робити звичайні сценарії? Шановний пане ...
Фаталізувати

Відповіді:


4

Java → CJam, оцінка ≈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();
        }
    }
}

Потрібна баночка CJam на класі. Якщо ви дасте йому два аргументи командного рядка (ім'я та якість файлу), він додає ".cjam" до імені файлу і записує там стиснене зображення. В іншому випадку він обчислює свою оцінку на 5 тестових зображеннях, які, як передбачається, знаходяться в поточному каталозі. Програма також автоматично перевіряє кожне стиснене зображення. Ви можете подвоїти перевірку підрахунку балів, якщо є невідповідність.

Використовувані (поки що) методи: масштабування до половини (по горизонталі, вертикалі або обох), якщо це не надто зменшує якість, призначене на замовлення RLE та базове перетворення, щоб запакувати більше даних у кожен символ, залишаючись у ASCII для друку


Не могли б ви дати мені короткий огляд, як це зробити? Я компілював це (успішно, я думаю) з javac -cp cjam-0.6.5.jar Compress.java, але java -cp cjam-0.6.5.jar Compressкаже Error: Could not find or load main class Compressі java Compressне знаходить клас CJam.
Денніс

@Dennis Вам потрібно додати каталог, що містить Compress.class, до classpath (-cp). Якщо він є в поточному каталозі, використовуйте -cp .:cjam-0.6.5.jar(у windoze, я думаю, вам потрібна крапка з комою замість двокрапки)
aditsu

Це зробило трюк, дякую.
Денніс

2

Python 3.5 (основний та вихідний) (наразі неконкурентний)

З днем ​​народження, виклик! Ось ваш подарунок: відповідь!

EDIT: Перетворений вихід у код python, покращена швидкість стиснення (злегка) EDIT2: Робить його друкованим в сирому вигляді, коли sizeдорівнює 1. Покращений бал, але бал потрібно обчислити заново. EDIT3: @ Денніс вказав, що я все-таки маю помилки в Геєві, тому я позначив відповідь неконкурентною

Код:

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

Ця відповідь може використати багато вдосконалень, тому я, мабуть, буду над цим більше працювати у вихідні.

Як це працює:

  • Розділіть зображення на блоки розміром size.
  • Знайдіть найкращий блок, що відповідає
    • Блоки можуть мати градієнт вже зараз!
  • Обчисліть якість (за формулою) для всього зображення.
  • Якщо це правильно, напишіть заархівоване зображення у файл.
  • Інакше декремент sizeі спробуйте ще раз.

Цей алгоритм добре працює для низької якості (0,5, 0,6), але не надто добре працює на зображеннях більш високої якості (насправді надувається). Це також дуже повільно.

Тут у мене є всі згенеровані файли, тож вам не доведеться знову їх генерувати.


Нарешті, відповідь! Це технічно неконкурентоспроможне, оскільки я створив Bubblegum після публікації цього виклику ... Я запускаю сценарій оцінювання пізніше і, можливо, портую його на менш езотеричну мову.
Денніс

@Dennis Ага, ну це не повинно бути надто важким для порту виводу на сценарій python. Дякую за голову
Blue

Я просто перечитав своє виклик (через рік я трохи розмився над деталями), і він говорить, що ваш компресор може використовувати вбудовані компресори байтових потоків (наприклад, gzip), але вам доведеться їх реалізувати самостійно для стислі зображення. Це означає, що Bubblegum у будь-якому випадку є.
Денніс

Нарешті я згадав, що пообіцяв забити це; Вибачте за затримку. Здається, у вашому коді є помилка друку (яка compingповинна бути compimg), яку я зафіксував для запуску програми. Якщо я не помилився під час запуску вашого коду, розміри деяких згенерованих зображень є невірними (наприклад, image2.txtмає 33 164 байти, але image2-50.0.txtмає 33 339), а інші не генерують той самий файл під час запуску створених програм ( image3-50.0.txtмає якість 0,5110 , але запуск створеної програми призводить до якості 0,4508 ).
Денніс

Додаток: Я завантажив image3-50.0.pyз вашого Dropbox, і він відповідає файлу, який я створив.
Денніс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.