Використання HTML5 / JavaScript для створення та збереження файлу


317

Останнім часом я спіткнувся з WebGL і змусив читача Collada працювати. Проблема в тому, що вона досить повільна (Collada - це дуже багатослівний формат), тому я збираюся розпочати перетворення файлів у більш простий у використанні формат (можливо, JSON). У мене вже є код для розбору файлу в JavaScript, тому я можу також використовувати його як мій експортер! Проблема - економія.

Тепер я знаю, що я можу проаналізувати файл, надіслати результат на сервер, і браузер вимагає повернути файл назад із сервера як завантаження. Але насправді сервер не має нічого спільного з цим конкретним процесом, тож навіщо його включати? У мене вже є вміст потрібного файлу в пам'яті. Чи є спосіб, щоб я міг представити користувачеві завантаження за допомогою чистого JavaScript? (Я сумніваюся в цьому, але, можливо, запитаю ...)

І щоб було зрозуміло: я не намагаюся отримати доступ до файлової системи без відома користувачів! Користувач надасть файл (можливо, за допомогою перетягування), сценарій перетворить файл у пам'ять, а користувачеві буде запропоновано завантажити результат. Все це повинно бути "безпечним" заходом, що стосується браузера.

[EDIT]: Я не згадував про це вперед, тому плакати, які відповіли "Flash", є досить дійсними, але частина того, що я роблю, - це спроба виділити те, що можна зробити з чистим HTML5 ... тому Flash - це прямо в моєму випадку. (Хоча це абсолютно правильна відповідь для тих, хто робить "справжній" веб-додаток.) ​​У цьому випадку, схоже, мені не пощастило, якщо я не хочу залучати сервер. Все одно, дякую!


8
Ви можете подумати про зміну прийнятої відповіді. Здається, зараз це суто HTML-спосіб
Pekka

Відповіді:


256

Гаразд, створення даних: URI, безумовно, робить фокус для мене, завдяки Метью та Деннкстеру, які вказали на цей варіант! Ось як я це роблю:

1) отримуйте весь вміст у рядку під назвою "content" (наприклад, створивши його там спочатку або прочитавши innerHTML тегу вже вбудованої сторінки).

2) Створіть URI даних:

uriContent = "data:application/octet-stream," + encodeURIComponent(content);

Існують обмеження довжини залежно від типу браузера тощо, але, наприклад, Firefox 3.6.12 працює, щонайменше, до 256k. Кодування в Base64 натомість використання encodeURIComponent може зробити речі більш ефективними, але для мене це було нормально.

3) відкрити нове вікно та "перенаправити" його на цей запит URI для місцезнаходження завантаження моєї сторінки, створеної JavaScript:

newWindow = window.open(uriContent, 'neuesDokument');

Це воно.


34
Якщо ви не хочете використовувати спливаюче вікно, яке може бути заблоковано, ви можете встановити location.hrefвміст. location.href = uriContent.
Алекс Турпін

12
Привіт, я спробував це, але він завантажує файл у форматі .part. Як я можу встановити тип файлу?
Седат Башар

6
@ SedatBaşar Data URI не підтримують встановлення імені файлу, вам потрібно покластися на налаштування браузера відповідного розширення, використовуючи тип mime. Але якщо тип mime може відобразити браузер, він не завантажить його, а відобразить його. Є кілька інших способів зробити це, але жоден з них не працює в IE <10.
панци

7
IE насправді не спортує URI, а Firefox, здається, зберігає файли з випадковим іменем.
nylund

25
Я думаю, що ми робимо це складніше, ніж потрібно. Відкрийте консоль JS на цій сторінці та поставте її, location.href = "data:application/octet-stream," + encodeURIComponent(jQuery('#answer-4551467 .post-text').first().text());і вона збереже вміст відповіді @ Nøk у файлі. У мене немає IE, щоб перевірити його, але він працює для webkit.
Бруно Броноський

278

Просте рішення для готових браузерів HTML5 ...

function download(filename, text) {
    var pom = document.createElement('a');
    pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
    pom.setAttribute('download', filename);

    if (document.createEvent) {
        var event = document.createEvent('MouseEvents');
        event.initEvent('click', true, true);
        pom.dispatchEvent(event);
    }
    else {
        pom.click();
    }
}

Використання

download('test.txt', 'Hello world!');

2
Якщо ви знаєте URL-адресу джерела, що ви хочете завантажити це найкраще рішення!
sidonaldson

31
Можливість встановити ім’я файлу робить це переможцем.
Омн

9
Цей метод працював у Chrome до останнього оновлення, яке я отримав кілька днів тому (35.0.1916.114 м). Тепер він частково працює в тому, що ім'я файлу та розширення більше не працюють (він завжди називає файл download.txt незалежно від того, що передано.)
Sevin7,

6
У мене є Chrom 42.0.2311.90, і це працює для мене з очікуваним іменем файлу.
Саурабх Кумар

4
Якщо є обмеження щодо кількості даних, які можна включити?
Каспар Лі

80

HTML5 визначив window.saveAs(blob, filename)метод. Зараз він не підтримується жодним браузером. Але є бібліотека сумісності під назвою FileSaver.js, яка додає цю функцію до більшості сучасних браузерів (включаючи Internet Explorer 10+). Internet Explorer 10 підтримує navigator.msSaveBlob(blob, filename)метод ( MSDN ), який використовується у FileSaver.js для підтримки Internet Explorer.

Я написав публікацію в блозі з детальнішою інформацією щодо цієї проблеми.


1
А що з блокуванням спливаючих вікон? Поведінка цієї бібліотеки схожа на рішення @ Nøk. Звичайний текст у Firefox відкривається в новому. Тільки Chrome намагається зберегти його, але він змінює розширення (мені потрібен dotfile без розширення).
ciembor

1
@ciembor варіант (URL-адреса об’єкта +) завантажити атрибут (який я використовую з chrome) дозволяє встановити ім’я файлу. Це працює для мене в хромі.
panzi

1
@ciembor aha і спливаюче вікно не блокується, якщо натискання безпосередньо спричинило його.
panzi

6
FileSaver.js підтримує IE зараз
Елі Сірий

15
W3C зазначає: Робота над цим документом припинена, і він не повинен посилатися або використовуватись як основа для реалізації.
WaiKit Kung

48

Збереження великих файлів

Довгі URI даних можуть створювати проблеми з роботою у веб-переглядачах. Інший варіант збереження файлів, створених на базі клієнта, - розмістити їх вміст у об'єкті Blob (або Файл) та створити посилання для завантаження за допомогою URL.createObjectURL(blob). Це повертає URL-адресу, яку можна використовувати для отримання вмісту блобу. Клоба зберігається всередині браузера, поки не URL.revokeObjectURL()буде викликано або URL-адресу, або документ, який її створив, не закриється. Більшість веб-браузерів підтримують URL-адреси об’єктів , Opera Mini - єдиний, який не підтримує їх.

Примусове завантаження

Якщо дані є текстом або зображенням, браузер може відкрити файл, а не зберегти його на диску. Щоб заставити файл завантажитися після натискання на посилання, ви можете використовувати downloadатрибут. Однак не всі веб-браузери підтримують атрибут завантаження . Іншим варіантом є використання application/octet-streamяк mime-типу файлу, але це призводить до того, що файл буде представлений у вигляді двійкового блобу, що особливо непривітне для користувачів, якщо ви не можете або не можете вказати ім’я файлу. Дивіться також " Змусити відкрити" Зберегти як ... "спливаюче вікно відкрити за текстовим посиланням, натисніть на pdf у HTML ".

Вказання імені файлу

Якщо крапка створена за допомогою конструктора файлів, ви також можете встановити ім'я файлу, але лише кілька веб-браузерів (включаючи Chrome і Firefox) підтримують конструктор файлів . Ім’я файлу також може бути вказане як аргумент downloadатрибуту, але це залежить від тони безпеки . Internet Explorer 10 і 11 надає власний метод, msSaveBlob , для визначення імені файлу.

Приклад коду

var file;
var data = [];
data.push("This is a test\n");
data.push("Of creating a file\n");
data.push("In a browser\n");
var properties = {type: 'text/plain'}; // Specify the file's mime-type.
try {
  // Specify the filename using the File constructor, but ...
  file = new File(data, "file.txt", properties);
} catch (e) {
  // ... fall back to the Blob constructor if that isn't supported.
  file = new Blob(data, properties);
}
var url = URL.createObjectURL(file);
document.getElementById('link').href = url;
<a id="link" target="_blank" download="file.txt">Download</a>


1
Чи можу я показати діалогове вікно (спливаюче вікно), щоб вказати папку (каталог) для збереження файлу?
Кальвін

@Calvin: я оновив відповідь, щоб пояснити, як змусити завантажити та вказати ім'я файлу. Для IE я вважаю, що ви можете використовувати msSaveBlobдля відкриття діалогового вікна "Зберегти як". Для інших браузерів ваш єдиний варіант - вручну вибрати "Зберегти як" у контекстному меню посилання для завантаження.
bcmpinc

1
@ Jek-fdrv: У Safari працюють лише Blob-urls. Атрибут завантаження та конструктор файлів не підтримується Safari, тому ви не можете примусово завантажувати, тобто блоб, ймовірно, буде відкритий у самому браузері, і ви не можете вказати ім'я файлу. Для даного прикладу ви все ще можете мати можливість завантажити файл із Safari за допомогою контекстного меню посилання.
bcmpinc


Це дуже корисна та інформативна відповідь. Ще одна річ, яка може допомогти таким людям, як я: після встановлення document.getElementById('link').href = url;вашого коду можна продовжити та запустити посилання за допомогою .click()методу елемента .
LarsH

36
function download(content, filename, contentType)
{
    if(!contentType) contentType = 'application/octet-stream';
        var a = document.createElement('a');
        var blob = new Blob([content], {'type':contentType});
        a.href = window.URL.createObjectURL(blob);
        a.download = filename;
        a.click();
}

3
Який ефект має contentType? Для чого він використовується?
Урі

3
Цей відмінно працює навіть в останньому Chrome, на відміну від відповіді @ Матея Покорна. Дякую.
Олександр Амелькін

2
Це не працює для мене на FF36 або IE11. Якщо я замінюю a.clickкодом, який використовує document.createEvent()Матей Покорні, він працює на FF, але не на IE. Я не пробував Chrome.
Пітер Халл

25

Погляньте на Дуга Neiner в Downloadify , який є на основі флеш - інтерфейс JavaScript , щоб зробити це.

Downloadify - це крихітна бібліотека JavaScript + Flash, яка дозволяє генерувати та зберігати файли під час польоту, у веб-переглядачі, без взаємодії із сервером.


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

2
@Toji ах, я бачу. Може, перепросити і переформулювати фразу під HTML 5банером і відповідно до тегу? Це, ймовірно, привабить тих користувачів, які знають про цю конкретну сферу (все-таки порівняно невелика натовп, мабуть). Я впевнений, що це можна зробити в HTML 5, але я поняття не маю.
Пекка

1
Чи було придбано / передано домен downloadify.info Downloadify, і якщо так, чи є нове місце? Цей веб-сайт здається абсолютно не пов'язаним із наданою відповіддю.
Крістос Хейвард

17
Це не відповідає з допомогою HTML5 ... - названий питання.
Ixx

5
@Ixx добре, щоб бути справедливим, що було додано після публікації відповіді. Все-таки ти маєш рацію. Відповідь нижче слід прийняти
Pekka

17

Просте рішення!

<a download="My-FileName.txt" href="data:application/octet-stream,HELLO-WORLDDDDDDDD">Click here</a>

Працює у всіх сучасних браузерах.


Привіт, чи знаєте ви, як вказати поведінку атрибута "завантажити" за допомогою window.open або іншої функції javascript?
Юсер

2
@yucer З атрибутом завантаження (або будь-яким атрибутом для цього питання) немає window.open(). Це не має значення. Ви можете скористатися цим методом і застосувати .click()його, але спостерігайте за тим, як Firefox не сподобається, якщо зателефонувати, .click()перш ніж дозволити елементу прикріпитися до тіла.
Бред

Але, на жаль, всі пробіли видаляються. У моєму випадку це дуже погано, оскільки я хочу завантажити вихідний код для файлу SVG.
франкенапс

пробіли зберігаються, якщо ви використовуєте encodeURIComponent (content)
Майк Redrobe

не можете вибрати ім’я у firefox, але хром працює
Bhikkhu Subhuti

10

Ви можете створити URI даних . Однак існують обмеження щодо браузера.


1
Це цікаво. Я детальніше розберуся, коли отримаю можливість. Дякую!
Тоджі

@quantumpotato, насправді генерування URL-адрес - трохи складно пояснити. Вся азотна крупка знаходиться в RFC 2397 . Ви можете використовувати такі інструменти , як це для тестування. Потім для вашого реального додатка ви можете шукати URI даних або базову бібліотеку 64 для вашої мови. Якщо ви цього не знайдете, не соромтеся задати наступне запитання. Деякі обмеження щодо браузера наведені у статті Вікіпедії . Наприклад, IE обмежує довжину та тип (наприклад, не текст / html).
Метью Флашен

Генерування URL-адрес даних не так вже й складно: "data:text/plain;charset=UTF-8,"+encodeURIComponent(text)Але так, IE обмежує розмір URL-адрес даних до непридатної кількості, і це не підтримує їх для таких речей, як window.open(...)iframes (я думаю).
panzi

@panzi, це не так просто. З одного боку, це не правильний тип MIME для Collada або JSON.
Метью Флашен

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

10

Я використовував FileSaver ( https://github.com/eligrey/FileSaver.js ), і він працює чудово.
Наприклад, я зробив цю функцію для експорту журналів, відображених на сторінці.
Ви маєте пройти масив для отримання екземпляра Blob, тому я просто, можливо, не написав це правильно, але це працює для мене.
На всякий випадок будьте обережні з заміною: це синтаксис, щоб зробити це глобальним, інакше він замінить лише перший, з яким він зустрічається.

exportLogs : function(){
    var array = new Array();

    var str = $('#logs').html();
    array[0] = str.replace(/<br>/g, '\n\t');

    var blob = new Blob(array, {type: "text/plain;charset=utf-8"});
    saveAs(blob, "example.log");
}

2
FileSaver чудовий, ось IE Shim для функції pre-IE10 preIE10SaveAs: (ім'я файлу, вміст файлу, міметик) {var w = window.open (); var doc = w.документ; doc.open (міметик, 'замінити'); doc.charset = "utf-8"; doc.write (filecontent); doc.close (); doc.execCommand ("SaveAs", null, ім'я файлу); }
aqm

Попередження про захист @ aqm: воно виконує теги сценаріїв.
GameFreak

Також, можливо, хочеться поставити w.close();післяexecCommand
GameFreak

8

Я знайшов два простих підходи, які працюють для мене. По-перше, за допомогою вже натиснутого aелемента та введення даних для завантаження. По-друге, генеруючи aелемент із завантаженими даними, виконуючи a.click()та видаляючи їх знову. Але другий підхід працює лише в тому випадку, якщо він також викликається дією натискання користувача. (Деякі) Блок браузера click()з інших контекстів, наприклад, під час завантаження або спрацьовування після таймауту (setTimeout).

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="UTF-8">
    <script type="text/javascript">
      function linkDownload(a, filename, content) {
        contentType =  'data:application/octet-stream,';
        uriContent = contentType + encodeURIComponent(content);
        a.setAttribute('href', uriContent);
        a.setAttribute('download', filename);
      }
      function download(filename, content) {
        var a = document.createElement('a');
        linkDownload(a, filename, content);
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      }
    </script>
   </head>
  <body>
    <a href="#" onclick="linkDownload(this, 'test.txt', 'Hello World!');">download</a>
    <button onclick="download('test.txt', 'Hello World!');">download</button>
  </body>
</html>

2
Ви також можете вставити aдокумент у документ (можливо, за допомогою "display:none"), клацнути його та видалити.
Teepeemm

1
чи буде це працювати в браузерах, де атрибут завантаження не підтримується, як навіть сучасний, тобто сафарі. caniuse.com/#feat=download
Мухаммад Умер

1
Щойно перевірений Safari 5.0 під вином. Перша версія працює, друга - ні. Я також перевірив IE 8 (вино), і він не працює.
maikel

6

Ось посилання на метод URI даних, запропонований Метью, він працював на сафарі, але це не добре, тому що я не зміг встановити тип файлу, він зберігається як "невідомий", і тоді мені доведеться піти туди пізніше і змінити його в порядку для перегляду файлу ...

http://www.nihilogic.dk/labs/canvas2image/


4

Ви можете використовувати localStorage. Це еквівалент файлів cookie Html5. Здається, він працює на Chrome і Firefox, АЛЕ на Firefox, мені потрібно було завантажити його на сервер. Тобто тестування безпосередньо на домашньому комп’ютері не працювало.

Я опрацьовую приклади HTML5. Перейдіть на сторінку http://facturing.purchase.edu/jeanine.meyer/html5/html5explain.html та перейдіть до лабіринту. Інформація для відновлення лабіринту зберігається за допомогою localStorage.

Я прийшов до цієї статті, шукаючи HTML5 JavaScript для завантаження та роботи з XML-файлами. Це те ж саме, що і старші html та JavaScript ????


4

спробуйте

let a = document.createElement('a');
a.href = "data:application/octet-stream,"+encodeURIComponent('"My DATA"');
a.download = 'myFile.json';
a.click(); // we not add 'a' to DOM so no need to remove

Якщо ви хочете завантажити двійкові дані, подивіться тут

Оновлення

2020.06.14 я оновлюю Chrome до 83.0 і вище, так що зупинка фрагмента працює (через обмеження безпеки пісочниці ) - але версія JSFiddle працює - тут


3

Як уже згадувалося раніше, API File разом з API FileWriter і FileSystem можуть використовуватися для зберігання файлів на машині клієнта з контексту вкладки / вікна браузера.

Однак є кілька речей, що стосуються останніх двох API, про які слід пам’ятати:

  • Зараз реалізація API існує лише у веб-переглядачах Chromium (Chrome & Opera)
  • Обидва API були вилучені з треку стандартів W3C 24 квітня 2014 року, і на сьогоднішній день є власником
  • В майбутньому можливе видалення (зараз фірмових) API з веб-переглядачів
  • Для зберігання файлів, створених за допомогою API, використовується пісочниця (розташування на диску, поза яким файли не дають ефекту)
  • Віртуальна файлова система (структура каталогів , яка не обов'язково існує на диску в тій же формі , що робить , коли доступ з браузера) використовується представляють файли , створені з допомогою API -

Ось прості приклади того, як API, прямо та опосередковано, використовуються в тандемі для цього:

Запечені продукти *

bakedGoods.get({
        data: ["testFile"],
        storageTypes: ["fileSystem"],
        options: {fileSystem:{storageType: Window.PERSISTENT}},
        complete: function(resultDataObj, byStorageTypeErrorObj){}
});

Використання сировини API, FileWriter та FileSystem

function onQuotaRequestSuccess(grantedQuota)
{

    function saveFile(directoryEntry)
    {

        function createFileWriter(fileEntry)
        {

            function write(fileWriter)
            {
                var dataBlob = new Blob(["Hello world!"], {type: "text/plain"});
                fileWriter.write(dataBlob);              
            }

            fileEntry.createWriter(write);
        }

        directoryEntry.getFile(
            "testFile", 
            {create: true, exclusive: true},
            createFileWriter
        );
    }

    requestFileSystem(Window.PERSISTENT, grantedQuota, saveFile);
}

var desiredQuota = 1024 * 1024 * 1024;
var quotaManagementObj = navigator.webkitPersistentStorage;
quotaManagementObj.requestQuota(desiredQuota, onQuotaRequestSuccess);

Хоча API-файли FileSystem та FileWriter вже не відповідають стандартам, на мою думку, їх використання може бути виправданим у деяких випадках, оскільки:

  • Поновлений інтерес з боку постачальників веб-переглядачів, які не реалізують, можуть повернути їх на себе
  • Проникнення на ринок веб-переглядачів (на основі хрому) є високим
  • Google (основний внесок у Chromium) не вказав API та дату закінчення терміну служби

Однак, чи стосується "деяких випадків" ваше власне, вирішувати.

* BakedGoods тут підтримує не хто інший, як цей хлопець :)


2

Цей потік був неоціненним для того, щоб з'ясувати, як генерувати двійковий файл та спонукати завантажувати названий файл, і все це в коді клієнта без сервера.

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

saveAnimation: function() {

    var device = this.Device;
    var maxRow = ChromaAnimation.getMaxRow(device);
    var maxColumn = ChromaAnimation.getMaxColumn(device);
    var frames = this.Frames;
    var frameCount = frames.length;

    var writeArrays = [];


    var writeArray = new Uint32Array(1);
    var version = 1;
    writeArray[0] = version;
    writeArrays.push(writeArray.buffer);
    //console.log('version:', version);


    var writeArray = new Uint8Array(1);
    var deviceType = this.DeviceType;
    writeArray[0] = deviceType;
    writeArrays.push(writeArray.buffer);
    //console.log('deviceType:', deviceType);


    var writeArray = new Uint8Array(1);
    writeArray[0] = device;
    writeArrays.push(writeArray.buffer);
    //console.log('device:', device);


    var writeArray = new Uint32Array(1);
    writeArray[0] = frameCount;
    writeArrays.push(writeArray.buffer);
    //console.log('frameCount:', frameCount);

    for (var index = 0; index < frameCount; ++index) {

      var frame = frames[index];

      var writeArray = new Float32Array(1);
      var duration = frame.Duration;
      if (duration < 0.033) {
        duration = 0.033;
      }
      writeArray[0] = duration;
      writeArrays.push(writeArray.buffer);

      //console.log('Frame', index, 'duration', duration);

      var writeArray = new Uint32Array(maxRow * maxColumn);
      for (var i = 0; i < maxRow; ++i) {
        for (var j = 0; j < maxColumn; ++j) {
          var color = frame.Colors[i][j];
          writeArray[i * maxColumn + j] = color;
        }
      }
      writeArrays.push(writeArray.buffer);
    }

    var blob = new Blob(writeArrays, {type: 'application/octet-stream'});

    return blob;
}

Наступним кроком є ​​те, щоб браузер запропонував користувачу завантажити цю крапку із заздалегідь заданою назвою.

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

<a id="lnkDownload" style="display: none" download="client.chroma" href="" target="_blank"></a>

Останній крок - спонукати користувача завантажити файл.

var data = animation.saveAnimation();
var uriContent = URL.createObjectURL(data);
var lnkDownload = document.getElementById('lnkDownload');
lnkDownload.download = 'theDefaultFileName.extension';
lnkDownload.href = uriContent;
lnkDownload.click();

1

Ось підручник з експорту файлів у вигляді ZIP:

Перед початком роботи є бібліотека для збереження файлів, назва бібліотеки - fileSaver.js, цю бібліотеку можна знайти тут. Почнемо, тепер включіть необхідні бібліотеки:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.4/jszip.min.js"  type="text/javascript"></script>
<script type="text/javascript" src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.js" ></script>

Тепер скопіюйте цей код, і цей код завантажить поштовий файл з файлом hello.txt із вмістом Hello World. Якщо все спрацює нормально, це завантажить файл.

<script type="text/javascript">
    var zip = new JSZip();
    zip.file("Hello.txt", "Hello World\n");
    zip.generateAsync({type:"blob"})
    .then(function(content) {
        // see FileSaver.js
        saveAs(content, "file.zip");
    });
</script>

Це завантажить файл під назвою file.zip. Більше ви можете прочитати тут: http://www.wapgee.com/story/248/guide-to-create-zip-files-using-javascript-by-using-jszip-library

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