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

Рис 1. Онлайн доска DGRM.net открывает схемы из PNG картинок
Рис 1. Онлайн доска DGRM.net открывает схемы из PNG картинок

Формат PNG-файла

Файл PNG состоит из блоков. Блоки содержат разную информацию. Например блок tIME содержит дату редактирования.

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

Получается такой файл - рис 2.

Рис 2. Файл картинки с данными DGRM
Рис 2. Файл картинки с данными DGRM

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

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

Хранение в 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.

Рис 4. Свой блок dgRp в начале PNG-файла. Указывает на начало DGRM data.
Рис 4. Свой блок dgRp в начале PNG-файла. Указывает на начало DGRM data.

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

Рис 5. Блоки DGRM Data
Рис 5. Блоки DGRM Data

Блок JSON грузится в память целиком. Вложения грузятся только если их нет в кэше.
Вложения могут быть большими, поэтому их тоже не желательно грузить целиком в память. Подробнее во второй части статьи.

Во второй части статьи про формирование больших файлов в браузере.

Комментарии (7)


  1. GCU
    25.04.2026 05:36

    Почему отказались от блока (чанка) PNG? Кажется графические редакторы сохраняют неизвестные блоки внутри PNG, а вот "хвост" могут и потерять


    1. Mingun
      25.04.2026 05:36

      Вот тоже интересно, если автор всё равно свой чанк в PNG засовывает, то зачем данные в конец файла пихать? Почему их в этом чанке и не разместить?


    1. Alex_BBB Автор
      25.04.2026 05:36

      Хранить вложения в PNG чанках не стал из-за ограничения на длину названия. Можно целиком всю DGRM data со свой структурой в один чанк положить. Но самодельные чанки тоже вырезают. Поэтому не знаю как лучше. Может вообще на свои .dgrm файлы перейти. И пользователей не путать, и вырезаться данные не будут и файлы меньше весить.


      1. Mingun
        25.04.2026 05:36

        Не понял, ограничение на длину названия чего? Типа чанка? Так а зачем вам длинное, достаточно какой-то уникальный для себя придумать. А внутри чанка ограничений никаких и нет, это же полностью ваша структура. Ну и если кто-то вырезает самодельные чанки из PNG, то уж и хвост-то им точно отрезать не проблема, так что всё равно не видно смысла в выбранной вами реализации.


        1. Alex_BBB Автор
          25.04.2026 05:36

          Так а зачем вам длинное, достаточно какой-то уникальный для себя придумать.

          Да. В моем комменте выше это отметил.

          У чанка есть ограничение на размер - поле с размером чанка 4 байта. Много больших вложений могут не поместиться.


          1. subzey
            25.04.2026 05:36

            В этом случае делайте так же, как IDAT: если все данные не влезают в один 2 Гб чанк, создайте второй


            1. Alex_BBB Автор
              25.04.2026 05:36

              Можно. А зачем? Чем PNG чанк лучше чем писать в конец файла?