оглавление

WebGLFundamentals.org

WebGL - Кросс-доменные изображения

Эта статья продолжает серию статей о WebGL. Если вы их ещё не читали, рекомендую начать с ранних уроков.

При работе с WebGL часто приходится скачивать изображения и загружать их в видеокарту, чтобы в дальнейшем использовать в текстуре. Уже было рассмотрено несколько подобных примеров. Например, в статье об обработке изображений, в статье о текстурах и в статье о реализация функции DrawImage.

Обычно мы загружаем изображение примерно следующим образом:

// создаём параметры текстуры { width: w, height: h, texture: tex }
// Изначально размер текстуры составляет 1x1 пикселей, а
// при завершении загрузки изображения размер изменяется
function loadImageAndCreateTextureInfo(url) {
  var tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  // заполняем текстуру синим пикселем 1x1
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
                new Uint8Array([0, 0, 255, 255]));

  // предполагаем, что размеры изображения не являются степенью двойки
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

  var textureInfo = {
    width: 1,   // мы не знаем размера изображения до его загрузки
    height: 1,
    texture: tex,
  };
  var img = new Image();
  img.addEventListener('load', function() {
    textureInfo.width = img.width;
    textureInfo.height = img.height;

    gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
  });
  img.src = url;

  return textureInfo;
}

Проблема в том, что изображения могут содержать в себе личные данные (например, капча, подпись, ню и т.д.). Веб-страница часто содержит рекламу и другие подобные вещи, которые не находятся под контролем страницы, поэтому браузер должен ограничить эти вещи от доступа к одержимому приватных изображений.

Простое использование <img src="private.jpg"> не представляет проблемы, ведь несмотря на то, что изображение отобразится в браузере, скрипт не сможет получить доступ к его содержимому. А вот Canvas2D API обладает инструментами заглянуть внутрь изображения. Для начала отображаем изображение в canvas

ctx.drawImage(someImg, 0, 0);

Затем получаем данные

var data = ctx.getImageData(0, 0, width, heigh);

Но если изображение пришло с другого домена, браузер отметит canvas запятнанным и вы получите ошибку безопасности при вызове ctx.getImageData.

В WebGL всё ещё сложнее. Функция gl.readPixels в WebGL является эквивалентом ctx.getImageData, поэтому, казалось бы, блокирование этой функции было бы достаточным в плане безопасности, но, оказывается, даже при невозможности прочитать пиксели напрямую можно создать шейдеры, которые потребуют более долгого времени выполнения на основе цветов изображения. Используя эту информацию, вы сможете использовать временной интервал, чтобы заглянуть внутрь изображения и получить доступ к его содержимому.

Поэтому WebGL просто блокирует все изображения, которые приходят с других доменов. Например, следующий небольшой пример с вращающимся прямоугольником использует текстуру с другого домена. Обратите внимание, что текстура не загружается, а в консоли браузера выводится ошибка.

Как обойти это ограничение?

Введение в CORS

CORS = Cross Origin Resource Sharing (совместное использование ресурсов между разными источниками). Веб-страница может запросить у сервера, на котором расположено изображение, разрешение на использование изображения.

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

...
+    img.crossOrigin = "";   // спрашиваем разрешение CORS
    img.src = url;

crossOrigin может принимать 3 значения. Первое из них - undefined, это значение по умолчанию, и оно означает "не запрашивать разрешения". Второе значение - anonymous, которое значит "запрашивать разрешения, но не отправлять дополнительную информацию". Последнее значение - use-credentials, означающее "отправлять куки и другую информацию, по которой сервер сможет определить, стоит ли давать разрешения или нет". При установке любого другого значения сервер будет считать, что установлено значение anonymous.

Почему бы нам всегда не запрашивать разрешение? Потому что запрос разрешения требует двух HTTP-запросов, поэтому он медленнее. Если мы знаем, что мы находимся на том же домене, или мы не планируем использовать изображение для чего-то помимо тега img, нам не нужно устанавливать crossDomain, так как он замедлит получение ресурса.

Мы можем написать функцию, которая проверит, находится ли наше изображение на том же домене, и в зависимости от этого установит атрибут crossOrigin.

function requestCORSIfNotSameOrigin(img, url) {
  if ((new URL(url)).origin !== window.location.origin) {
    img.crossOrigin = "";
  }
}

Использовать функцию можно следующим образом:

...
+requestCORSIfNotSameOrigin(img, url);
img.src = url;

Следует отметить, что запрос разрешение вовсе НЕ означает, что это разрешение будет дано. Это зависит от сервера. Github-страницы дадут разрешение, как и flickr.com или imgur.com, но большинство других сайтов такого разрешения не дадут. Под "разрешением" здесь понимаются HTTP-заголовки, которые сервер отправляет вместе с изображением.

Также важно помнить, что разрешений от сервера недостаточно. Если изображение приходит с другого домена, вам нужно установить атрибут crossOrigin, в противном случае вы не сможете использовать изображение, даже если сервер пришлёт нужные заголовки.

Настройка Apache для выдачи разрешений CORS

Если ваш веб-сайт работает на веб-сервере Apache и модуль mod_rewrite установлен, вы можете разрешить CORS-запросы через установку

    Header set Access-Control-Allow-Origin "*"

в соответствующем файле .htaccess.

Вопросы? Спросите на stackoverflow.
Нашли ошибку? Создайте задачу на github.
comments powered by Disqus