оглавление

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 отправляется на сервер. Сервер смотрит на это значение и решает, давать разрешение или нет. Большинство серверов с поддержкой CORS не обращают внимания на эту строку, они просто дают разрешение всем. Поэтому пустое значение работает. Его смысл - просто запросить разрешение, в отличие от img.crossOrigin = "bob", что будет означать "запросить разрешение для 'bob'".

Почему бы нам всегда не запрашивать разрешение? Потому что запрос разрешения требует двух 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, но большинство других сайтов такого разрешения не дадут.

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

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

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

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

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