Экспорт карты в изображение

В MapLibre GL JS визуализация карты строится на WebGL-контексте, где итоговый кадр всегда доступен через HTMLCanvasElement, связанный с экземпляром карты. Основной точкой доступа выступает метод получения canvas:

const canvas = map.getCanvas();

Полученный canvas содержит текущий отрендеренный кадр карты, включая стили, тайлы, слои и все визуальные эффекты, сформированные на момент последнего рендера.

Ключевое свойство: изображение не является статическим артефактом, а представляет собой результат WebGL-пайплайна, поэтому экспорт всегда зависит от состояния рендеринга.


Базовый экспорт через data URL

Самый прямой способ получения изображения — преобразование canvas в DataURL:

const canvas = map.getCanvas();
const dataURL = canvas.toDataURL('image/png');

Полученный dataURL можно использовать для:

  • загрузки изображения в <img>
  • скачивания файла
  • передачи на сервер

Пример скачивания:

function downloadMapImage(map) {
  const canvas = map.getCanvas();
  const url = canvas.toDataURL('image/png');

  const link = document.createElement('a');
  link.href = url;
  link.download = 'map.png';
  link.click();
}

Ограничение этого подхода связано с тем, что результат фиксируется в текущем размере canvas, без контроля качества и без возможности асинхронного ожидания завершения рендера.


Экспорт через toBlob для оптимальной памяти

Метод toBlob предпочтительнее toDataURL, так как не создаёт громоздкую base64-строку:

map.getCanvas().toBlob((blob) => {
  const url = URL.createObjectURL(blob);

  const link = document.createElement('a');
  link.href = url;
  link.download = 'map.png';
  link.click();

  URL.revokeObjectURL(url);
}, 'image/png');

Преимущества:

  • меньшая нагрузка на память
  • более быстрый экспорт больших изображений
  • удобная работа с файлами

Контроль завершения рендеринга

Экспорт изображения напрямую после вызова изменения карты приводит к неполному результату. MapLibre GL JS рендерит асинхронно, поэтому необходимо дождаться завершения кадра.

Используются события:

  • idle — карта полностью завершила рендер
  • render — происходит отрисовка
  • load — завершена загрузка стиля

Пример корректного ожидания:

map.once('idle', () => {
  const canvas = map.getCanvas();
  const url = canvas.toDataURL('image/png');

  const link = document.createElement('a');
  link.href = url;
  link.download = 'map.png';
  link.click();
});

Событие idle является наиболее надёжным индикатором готовности изображения.


Важность preserveDrawingBuffer

WebGL по умолчанию очищает буфер после отрисовки кадра, что может привести к пустому изображению при экспорте. Для корректного сохранения изображения используется параметр инициализации карты:

const map = new maplibregl.Map({
  container: 'map',
  style: 'https://demotiles.maplibre.org/style.json',
  preserveDrawingBuffer: true
});

Этот флаг заставляет WebGL сохранять содержимое буфера между кадрами, что делает возможным стабильный экспорт.

Недостатки:

  • снижение производительности
  • увеличение потребления памяти
  • возможные артефакты при сложных сценах

Экспорт высокого разрешения (HiDPI)

Для получения изображения с повышенной детализацией используется масштабирование canvas через pixel ratio.

function exportHighRes(map, scale = 2) {
  const canvas = map.getCanvas();
  const width = canvas.width;
  const height = canvas.height;

  const exportCanvas = document.createElement('canvas');
  exportCanvas.width = width * scale;
  exportCanvas.height = height * scale;

  const ctx = exportCanvas.getContext('2d');

  ctx.drawImage(canvas, 0, 0, exportCanvas.width, exportCanvas.height);

  exportCanvas.toBlob((blob) => {
    const url = URL.createObjectURL(blob);

    const link = document.createElement('a');
    link.href = url;
    link.download = 'map@2x.png';
    link.click();

    URL.revokeObjectURL(url);
  });
}

Этот метод не увеличивает реальную детализацию данных карты, но улучшает плотность пикселей для печати и экранов Retina.


Перерисовка перед экспортом

При динамическом изменении стиля или слоёв требуется принудительно дождаться стабилизации рендера:

function exportAfterUpdate(map) {
  map.triggerRepaint();

  map.once('render', () => {
    map.once('idle', () => {
      const canvas = map.getCanvas();
      const url = canvas.toDataURL('image/png');

      const link = document.createElement('a');
      link.href = url;
      link.download = 'map.png';
      link.click();
    });
  });
}

Важно учитывать, что render не гарантирует завершённость данных, тогда как idle обеспечивает завершение всех асинхронных операций.


Проблемы CORS и «загрязнённого canvas»

При экспорте карты часто возникает ошибка безопасности canvas:

Tainted canvas cannot be exported

Причина — загрузка ресурсов (тайлы, спрайты, шрифты) с серверов без корректных CORS-заголовков.

Требования:

  • тайловые серверы должны возвращать Access-Control-Allow-Origin
  • sprites и glyphs должны быть доступны с CORS
  • в MapLibre необходимо включать:
const map = new maplibregl.Map({
  container: 'map',
  style: style,
  crossOrigin: 'anonymous'
});

Если хотя бы один ресурс не поддерживает CORS, экспорт становится невозможным.


Экспорт с изменением размера карты

Иногда требуется экспортировать карту в размере, отличном от отображаемого на экране.

Подход заключается в временном изменении размера контейнера:

function exportWithSize(map, width, height) {
  const container = map.getContainer();

  container.style.width = width + 'px';
  container.style.height = height + 'px';

  map.resize();

  map.once('idle', () => {
    const canvas = map.getCanvas();
    const url = canvas.toDataURL('image/png');

    const link = document.createElement('a');
    link.href = url;
    link.download = 'map.png';
    link.click();
  });
}

После экспорта обычно требуется вернуть исходные размеры и снова вызвать resize().


Использование offscreen canvas-подходов

В высоконагруженных системах экспорт выполняется через перенос рендера в OffscreenCanvas (при поддержке окружения):

const offscreen = new OffscreenCanvas(1024, 768);
const gl = offscreen.getContext('webgl');

Однако MapLibre GL JS напрямую не управляет OffscreenCanvas, поэтому такой подход требует кастомной интеграции WebGL-контекста и не является стандартным сценарием.


Формирование многостраничных изображений

При необходимости экспорта больших областей карты применяется разбиение на тайлы с последующей склейкой:

  • карта последовательно перемещается через map.setCenter
  • фиксируется каждый кадр
  • изображения объединяются через canvas 2D

Пример логики:

const images = [];

async function captureStep(center) {
  map.setCenter(center);

  await new Promise(resolve => map.once('idle', resolve));

  const canvas = map.getCanvas();
  images.push(canvas.toDataURL());
}

Дальнейшая сборка выполняется через отдельный canvas-композитинг.


Особенности рендеринга WebGL в MapLibre GL JS

Экспорт изображения напрямую связан с архитектурой рендеринга:

  • слои рендерятся в порядке z-index
  • тайлы загружаются асинхронно
  • текст и иконки проходят отдельный pipeline
  • финальный кадр формируется только после завершения всех WebGL-pass

Это означает, что любое вмешательство в момент загрузки приводит к неполным или артефактным изображениям.


Практические ограничения экспорта

  • максимальный размер canvas ограничен видеопамятью GPU
  • сложные стили увеличивают время рендеринга
  • большое количество растровых тайлов замедляет экспорт
  • шрифты могут загружаться позже тайлов, задерживая idle

Подготовка карты к стабильному экспорту

Для предсказуемого результата применяются фиксированные настройки:

  • отключение анимаций
  • фиксированный zoom
  • запрет вращения и pitch
  • предварительная загрузка ресурсов
map.setBearing(0);
map.setPitch(0);
map.setZoom(10);

После стабилизации состояния карта становится детерминированной для рендеринга изображения.