Онлайн доска 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.
// IENDconst 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 грузится в память целиком. Вложения грузятся только если их нет в кэше.
Вложения могут быть большими, поэтому их тоже не желательно грузить целиком в память. Подробнее во второй части статьи.
Во второй части статьи про формирование больших файлов в браузере.
ссылка на оригинал статьи https://habr.com/ru/articles/1027666/