Онлайн доска DGRM.net хранит данные в PNG-картинках. Вместе с вложениями файлы получаются большие. Рассказываю как сделано хранение данных в PNG-файлах.

Формат PNG-файла
Файл PNG состоит из блоков. Блоки содержат разную информацию. Например блок tIME содержит дату редактирования.
В конце идет обязательный блок IEND. После IEND можно дописать в файл свои данные и картинка не сломается. Это использует DGRM: пишет свои данные в конец PNG файла.
Получается такой файл - рис 2.

Структура PNG блока - рис 3.

Хранение в DGRM Data организовано также блоками, только немного другого формата. Первый блок - JSON-фигур, потом вложения.
Чтение из файла без загрузки всего файла в память
Получить ссылку на файл на устройстве пользователя можно с помощью HTMLInputElement. При этом данные файла не будут загружены в память - листинг 1.
/** * @param {string} accept * @param {FileCallback} callBack * @param {(evt:Event)=>void} cancelCallBack */ const fileInputOpen = (accept, callBack, cancelCallBack) => { const input = document.createElement('input'); input.type = 'file'; input.multiple = false; input.accept = accept; input.style.display = 'none'; document.body.appendChild(input); const dispose = () => input?.remove(); input.oncancel = evt => { cancelCallBack(evt); dispose(); }; input.onchange = () => { callBack((!input.files?.length) ? null : input.files[0]); dispose(); }; input.click(); }
Листинг 1. Получение ссылки на файл
При открытии файла нужно найти где начинаются DGRM данные, т.е. блок IEND.
Для поиска нужно перебрать блоки от начала файла до искомого блока. При этом не желательно грузить весь файл в память.
Функция pngChunkDataPositionGet последовательно читает только по 8 байт (длина + заголовок) и проматывает до следующего блока пока не найдет нужный - листинг 2.
// IEND const PNG_CHUNK_END_NAME_UINT32 = 1229278788; /** * @param {Blob} pngFile, @param {number} chankNameUint32 * @returns {Promise<[startBytePosition:number, endBytePosition:number]>} */ const pngChunkDataPositionGet = async (pngFile, chankNameUint32) => { /** @param {number} pos */ const uint32Get = async pos => uint32From4BytesBlob(pngFile.slice(pos, pos + 4)); /** @type {number} */ let chunkPosition = 8; // 8 byte - png signature /** @type {number} */ let chunkLenght; /** @type {number} */ let chunkName; /** @type {number} */ let chunkDataStart; /** @type {number} */ let chunkDataEnd; do { chunkLenght = await uint32Get(chunkPosition); chunkName = await uint32Get(chunkPosition + 4); chunkDataStart = chunkPosition + 8; chunkDataEnd = chunkDataStart + chunkLenght; if (chunkName === chankNameUint32) { return [chunkDataStart, chunkDataEnd]; } chunkPosition = chunkDataEnd + 4; } while (chunkName !== PNG_CHUNK_END_NAME_UINT32); // looking for end chunk if (chunkName === chankNameUint32) { return [chunkDataStart, chunkDataEnd]; } return null; };
Листинг 2. Поиск блока в PNG файле
Мотать до IEND большие PNG не быстро. Поэтому имеет смысл добавить свой блок в начало файла. В этом блоке указать кол-во байт до конца DGRM Data, т.е. размер PNG картинки без дополнительной DGRM Data.

В DGRM Data первый блок это JSON-фигур, потом идут блоки вложений - рис 5.

Блок JSON грузится в память целиком. Вложения грузятся только если их нет в кэше.
Вложения могут быть большими, поэтому их тоже не желательно грузить целиком в память. Подробнее во второй части статьи.
Во второй части статьи про формирование больших файлов в браузере.
GCU
Почему отказались от блока (чанка) PNG? Кажется графические редакторы сохраняют неизвестные блоки внутри PNG, а вот "хвост" могут и потерять
Mingun
Вот тоже интересно, если автор всё равно свой чанк в PNG засовывает, то зачем данные в конец файла пихать? Почему их в этом чанке и не разместить?
Alex_BBB Автор
Хранить вложения в PNG чанках не стал из-за ограничения на длину названия. Можно целиком всю DGRM data со свой структурой в один чанк положить. Но самодельные чанки тоже вырезают. Поэтому не знаю как лучше. Может вообще на свои .dgrm файлы перейти. И пользователей не путать, и вырезаться данные не будут и файлы меньше весить.
Mingun
Не понял, ограничение на длину названия чего? Типа чанка? Так а зачем вам длинное, достаточно какой-то уникальный для себя придумать. А внутри чанка ограничений никаких и нет, это же полностью ваша структура. Ну и если кто-то вырезает самодельные чанки из PNG, то уж и хвост-то им точно отрезать не проблема, так что всё равно не видно смысла в выбранной вами реализации.
Alex_BBB Автор
Да. В моем комменте выше это отметил.
У чанка есть ограничение на размер - поле с размером чанка 4 байта. Много больших вложений могут не поместиться.
subzey
В этом случае делайте так же, как IDAT: если все данные не влезают в один 2 Гб чанк, создайте второй
Alex_BBB Автор
Можно. А зачем? Чем PNG чанк лучше чем писать в конец файла?