Ориентация Exif на стороне клиента JS: поворот и отражение изображений JPEG


154

Фотографии с цифровых камер часто сохраняются в формате JPEG с тегом «ориентации» EXIF. Для правильного отображения изображения необходимо поворачивать / отражать в зависимости от установленной ориентации, но браузеры игнорируют эту информацию, отображающую изображение. Даже в крупных коммерческих веб-приложениях поддержка ориентации EXIF ​​может быть неочевидной 1 . Тот же источник также предоставляет хорошее резюме 8 различных ориентаций, которые может иметь JPEG:

Резюме EXIF ​​Ориентации

Образцы изображений доступны на 4 .

Вопрос в том, как повернуть / отразить изображение на стороне клиента, чтобы оно отображалось правильно и могло быть обработано при необходимости?

Для анализа EXIF-данных доступны библиотеки JS, в том числе атрибут ориентации 2 . Flickr отметил возможную проблему с производительностью при разборе больших изображений, требующую использования веб-работников 3 .

Консольные инструменты могут правильно переориентировать изображения 5 . Сценарий PHP, решающий проблему, доступен на 6

Ответы:


145

Проект github JavaScript-Load-Image предоставляет полное решение проблемы ориентации EXIF, правильно поворачивая / зеркально отображая изображения для всех 8 ориентаций exif. Смотрите онлайн-демонстрацию Javascript EXIF ​​ориентации

Изображение рисуется на холсте HTML5. Его правильный рендеринг реализован в js / load-image-direction.js с помощью операций canvas.

Надеюсь, это сэкономит кому-то еще время и научит поисковики об этом сокровище с открытым исходным кодом :)


1
Я использую эту библиотеку, но недавно она сломалась на iOS 8.3, где она мне нужна больше всего :(
Гордон Сан

2
Я действительно изо всех сил пытаюсь понять, насколько это полезно. Я пытаюсь научиться этому, чтобы я мог либо проанализировать страницу и повернуть изображения, когда они должны быть повернуты, либо определить ориентацию и повернуть файл (каким-либо образом) перед его фактической загрузкой. Демо не делает ни того, ни другого. Он просто берет файл из файлового ввода и отображает его правильно, когда это полезно в реальном мире? Когда я анализирую свою страницу и передаю URL-адреса из тегов изображения в библиотеку loadImage, нет данных exif, поэтому я не могу этого сделать. Для загрузки он возвращает объект холста, поэтому я не могу отправить его на сервер или что-либо еще.
незнакомец

3
@igneosaur: вы можете отправлять данные в кодировке base64 на сервер, canvas.toDataURL()декодировать и сохранять их на стороне сервера.
br4nnigan

1
вместо того, чтобы использовать проект load-image, вы можете использовать некоторую его кодовую базу для собственной выпечки - просто посмотрите в github.com/blueimp/JavaScript-Load-Image/blob/master/js/… .
Энди Лоренц

1
Есть ли способ сделать холст, который эта библиотека производит, отзывчивым, как обычный <img>тег?
Эрик Беркун-Древниг

103

Контекстное преобразование Медера работает отлично. Если вам нужно извлечь ориентацию, используйте только эту функцию - вам не нужны никакие библиотеки для чтения EXIF. Ниже приведена функция для переустановки ориентации в изображении base64. Вот скрипка для этого . Я также подготовил скрипку с демонстрацией извлечения ориентации .

function resetOrientation(srcBase64, srcOrientation, callback) {
  var img = new Image();    

  img.onload = function() {
    var width = img.width,
        height = img.height,
        canvas = document.createElement('canvas'),
        ctx = canvas.getContext("2d");

    // set proper canvas dimensions before transform & export
    if (4 < srcOrientation && srcOrientation < 9) {
      canvas.width = height;
      canvas.height = width;
    } else {
      canvas.width = width;
      canvas.height = height;
    }

    // transform context before drawing image
    switch (srcOrientation) {
      case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
      case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
      case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
      case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
      case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
      case 7: ctx.transform(0, -1, -1, 0, height, width); break;
      case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
      default: break;
    }

    // draw image
    ctx.drawImage(img, 0, 0);

    // export base64
    callback(canvas.toDataURL());
  };

  img.src = srcBase64;
};

7
Случай по умолчанию в ориентации switchне требуется, так как это преобразование ничего не делает. Кроме того, рассмотрите возможность использования srcOrientation > 4 && srcOrientation < 9вместо [5,6,7,8].indexOf(srcOrientation) > -1, потому что это быстрее и менее ресурсоемким (как ОЗУ и ЦП). Там нет необходимости иметь массив там. Это важно при пакетировании большого количества изображений, где каждый бит считается. В остальном довольно неплохой ответ. Upvoted!
Райан Касас

4
@RyanCasas Я не знал, насколько тяжелым может indexOfбыть то, что вы предложили. Я запустил простой цикл с 10M итерациями, и он был на 1400% быстрее. Приятно: D Спасибо большое!
WunderBart

1
Я использовал этот ответ в Android WebView, и оказалось, что есть некоторые устройства Android, которые не поддерживают WebGL в WebView (см. Bugs.chromium.org/p/chromium/issues/detail?id=555116 ). Вращение может занять очень много времени на таких устройствах в зависимости от размера изображения.
ndreisg

1
При использовании со ссылками getOrientationмне интересно, насколько это эффективно с точки зрения производительности. getOrientationзвонки fileReader.readAsArrayBuffer, а затем мы вызываем URL.createObjectURLи передаем результат в resetOrientation, который загружает этот URL-адрес как изображение. Означает ли это, что файл изображения будет «загружен» / прочитан браузером не один, а два раза, или я неправильно понял?
Оливер Джозеф Эш

1
Кроме того, что делать с браузерами, где ориентация уже будет соблюдаться? Я считаю, что это относится к iOS Safari и к браузерам, поддерживающим CSS, image-orientation: from-imageкогда он используется.
Оливер Джозеф Эш

39

Если

width = img.width;
height = img.height;
var ctx = canvas.getContext('2d');

Затем вы можете использовать эти преобразования, чтобы превратить изображение в ориентацию 1

Из ориентации:

  1. ctx.transform(1, 0, 0, 1, 0, 0);
  2. ctx.transform(-1, 0, 0, 1, width, 0);
  3. ctx.transform(-1, 0, 0, -1, width, height);
  4. ctx.transform(1, 0, 0, -1, 0, height);
  5. ctx.transform(0, 1, 1, 0, 0, 0);
  6. ctx.transform(0, 1, -1, 0, height, 0);
  7. ctx.transform(0, -1, -1, 0, height, width);
  8. ctx.transform(0, -1, 1, 0, 0, width);

Перед нанесением изображения на ctx


58
что здесь вообще происходит?
Мухаммед Умер

1
При этом вам просто нужно знать ориентацию (например, через EXIF), а затем вращать по мере необходимости. Половина того, что я ищу.
Бренден,

2
эти преобразования не сработали для меня - вместо этого я использовал код из проекта load-image по адресу github.com/blueimp/JavaScript-Load-Image/blob/master/js/…, чтобы получить то, что я считаю хорошо проверенным кодом который использует обе операции перевода, поворота в контексте холста плюс обмен шириной / высотой. Дали мне 100% результатов после многих часов экспериментов с другими попытками трансформации и т. Д.
Энди Лоренц

1
Почему это важно, когда вы рисуете изображение на ctx? Вы не можете вращать холст после рисования на нем изображения?
AlxVallejo

Вращение для ориентации 8 работает для меня следующим образом: ctx.transform (0, -1, 1, 0, 0, высота);
Джорджио Барчиези

24

Хорошо, в дополнение к ответу @ user3096626, я думаю, будет более полезно, если кто-то предоставит пример кода, следующий пример покажет вам, как исправить ориентацию изображения, полученную из URL (удаленные изображения):


Решение 1: использование JavaScript (рекомендуется)

  1. поскольку библиотека load-image не извлекает теги exif только из изображений url (file / blob), мы будем использовать библиотеки javascript exif-js и load-image , поэтому сначала добавьте эти библиотеки на свою страницу следующим образом:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/exif-js/2.1.0/exif.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-load-image/2.12.2/load-image.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-load-image/2.12.2/load-image-scale.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-load-image/2.12.2/load-image-orientation.min.js"></script>
    

    Обратите внимание, что версия 2.2 exif-js кажется имеет проблемы, поэтому мы использовали 2.1

  2. тогда в основном то, что мы будем делать, это

    а - загрузить изображение с помощью window.loadImage()

    b - читать теги exif, используя window.EXIF.getData()

    c - преобразовать изображение в холст и исправить ориентацию изображения, используя window.loadImage.scale()

    d - поместить холст в документ

Ну вот :)

window.loadImage("/your-image.jpg", function (img) {
  if (img.type === "error") {
    console.log("couldn't load image:", img);
  } else {
    window.EXIF.getData(img, function () {
        var orientation = EXIF.getTag(this, "Orientation");
        var canvas = window.loadImage.scale(img, {orientation: orientation || 0, canvas: true});
        document.getElementById("container").appendChild(canvas); 
        // or using jquery $("#container").append(canvas);

    });
  }
});

конечно, вы также можете получить изображение как base64 из объекта canvas и поместить его в атрибут img src, так что вы можете использовать jQuery;)

$("#my-image").attr("src",canvas.toDataURL());

вот полный код на: github: https://github.com/digital-flowers/loadimage-exif-example


Решение 2: использование HTML (взлом браузера)

есть очень быстрый и легкий способ взлома, большинство браузеров отображают изображение в правильной ориентации, если изображение открывается внутри новой вкладки напрямую без HTML (LOL, я не знаю почему), поэтому в основном вы можете отображать свое изображение с помощью iframe поместив атрибут iframe src в качестве URL изображения:

<iframe src="/my-image.jpg"></iframe>

Решение 3: использование CSS (только Firefox и Safari на IOS)

есть атрибут css3 для исправления ориентации изображения, но проблема, с которой он работает только в firefox и safari / ios, все же стоит упомянуть, потому что скоро он будет доступен для всех браузеров (информация о поддержке браузера из caniuse )

img {
   image-orientation: from-image;
}

Решение 1 не работает для меня. Это возвращает window.loadImage не определено
Перри

Это произошло, если вы не включили библиотеки, которые я упомянул в шаге 1
Фарид Алнамрути

Я включил библиотеки. Мне нравится простота вашего примера, но я продолжаю получать это сообщение об ошибке.
Перри

1
кажется, что exif-js был сломан, пожалуйста, проверьте мои правки, я также добавил полный код на github: github.com/digital-flowers/loadimage-exif-example
Fareed Alnamrouti

1
ок, извлеките из проекта github новый файл «upload.html», пожалуйста :)
Fareed Alnamrouti

10

Для тех, у кого есть файл из элемента управления вводом, не знают, какова его ориентация, немного ленивы и не хотят включать большую библиотеку ниже, это код, предоставленный @WunderBart, объединенный с ответом, на который он ссылается ( https://stackoverflow.com/a/32490603 ), который находит ориентацию.

function getDataUrl(file, callback2) {
        var callback = function (srcOrientation) {
            var reader2 = new FileReader();
            reader2.onload = function (e) {
                var srcBase64 = e.target.result;
                var img = new Image();

                img.onload = function () {
                    var width = img.width,
                        height = img.height,
                        canvas = document.createElement('canvas'),
                        ctx = canvas.getContext("2d");

                    // set proper canvas dimensions before transform & export
                    if (4 < srcOrientation && srcOrientation < 9) {
                        canvas.width = height;
                        canvas.height = width;
                    } else {
                        canvas.width = width;
                        canvas.height = height;
                    }

                    // transform context before drawing image
                    switch (srcOrientation) {
                        case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
                        case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
                        case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
                        case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
                        case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
                        case 7: ctx.transform(0, -1, -1, 0, height, width); break;
                        case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
                        default: break;
                    }

                    // draw image
                    ctx.drawImage(img, 0, 0);

                    // export base64
                    callback2(canvas.toDataURL());
                };

                img.src = srcBase64;
            }

            reader2.readAsDataURL(file);
        }

        var reader = new FileReader();
        reader.onload = function (e) {

            var view = new DataView(e.target.result);
            if (view.getUint16(0, false) != 0xFFD8) return callback(-2);
            var length = view.byteLength, offset = 2;
            while (offset < length) {
                var marker = view.getUint16(offset, false);
                offset += 2;
                if (marker == 0xFFE1) {
                    if (view.getUint32(offset += 2, false) != 0x45786966) return callback(-1);
                    var little = view.getUint16(offset += 6, false) == 0x4949;
                    offset += view.getUint32(offset + 4, little);
                    var tags = view.getUint16(offset, little);
                    offset += 2;
                    for (var i = 0; i < tags; i++)
                        if (view.getUint16(offset + (i * 12), little) == 0x0112)
                            return callback(view.getUint16(offset + (i * 12) + 8, little));
                }
                else if ((marker & 0xFF00) != 0xFF00) break;
                else offset += view.getUint16(offset, false);
            }
            return callback(-1);
        };
        reader.readAsArrayBuffer(file);
    }

который можно легко назвать таким

getDataUrl(input.files[0], function (imgBase64) {
      vm.user.BioPhoto = imgBase64;
});

2
Спасибо, после более чем 2-часового поиска в Google, я нашел правильное решение.
Mr.Trieu

2
почему кому-то лень предпочесть более легкое решение?
роса

4
Портируется на Typescript и оптимизируется от 4 до 20 миллисекунд. Узким местом было кодирование Base64 - использование image/jpegвместо значения по умолчанию image/pngи изменение размера выходного изображения до максимальной ширины (в моем случае 400 пикселей) имели огромное значение. (это хорошо работает для миниатюр, но если вам нужно отобразить полное изображение, я бы рекомендовал избегать base64 и просто вставлять элемент canvas прямо в страницу ...)
mindplay.dk

8

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

Собираем всю информацию из вундербарта, что-то вроде этого;

var handleTakePhoto = function () {
    let fileInput: HTMLInputElement = <HTMLInputElement>document.getElementById('photoInput');
    fileInput.addEventListener('change', (e: any) => handleInputUpdated(fileInput, e.target.files));
    fileInput.click();
}

var handleInputUpdated = function (fileInput: HTMLInputElement, fileList) {
    let file = null;

    if (fileList.length > 0 && fileList[0].type.match(/^image\//)) {
        isLoading(true);
        file = fileList[0];
        getOrientation(file, function (orientation) {
            if (orientation == 1) {
                imageBinary(URL.createObjectURL(file));
                isLoading(false);
            }
            else 
            {
                resetOrientation(URL.createObjectURL(file), orientation, function (resetBase64Image) {
                    imageBinary(resetBase64Image);
                    isLoading(false);
                });
            }
        });
    }

    fileInput.removeEventListener('change');
}


// from http://stackoverflow.com/a/32490603
export function getOrientation(file, callback) {
    var reader = new FileReader();

    reader.onload = function (event: any) {
        var view = new DataView(event.target.result);

        if (view.getUint16(0, false) != 0xFFD8) return callback(-2);

        var length = view.byteLength,
            offset = 2;

        while (offset < length) {
            var marker = view.getUint16(offset, false);
            offset += 2;

            if (marker == 0xFFE1) {
                if (view.getUint32(offset += 2, false) != 0x45786966) {
                    return callback(-1);
                }
                var little = view.getUint16(offset += 6, false) == 0x4949;
                offset += view.getUint32(offset + 4, little);
                var tags = view.getUint16(offset, little);
                offset += 2;

                for (var i = 0; i < tags; i++)
                    if (view.getUint16(offset + (i * 12), little) == 0x0112)
                        return callback(view.getUint16(offset + (i * 12) + 8, little));
            }
            else if ((marker & 0xFF00) != 0xFF00) break;
            else offset += view.getUint16(offset, false);
        }
        return callback(-1);
    };

    reader.readAsArrayBuffer(file.slice(0, 64 * 1024));
};

export function resetOrientation(srcBase64, srcOrientation, callback) {
    var img = new Image();

    img.onload = function () {
        var width = img.width,
            height = img.height,
            canvas = document.createElement('canvas'),
            ctx = canvas.getContext("2d");

        // set proper canvas dimensions before transform & export
        if (4 < srcOrientation && srcOrientation < 9) {
            canvas.width = height;
            canvas.height = width;
        } else {
            canvas.width = width;
            canvas.height = height;
        }

        // transform context before drawing image
        switch (srcOrientation) {
            case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
            case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
            case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
            case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
            case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
            case 7: ctx.transform(0, -1, -1, 0, height, width); break;
            case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
            default: break;
        }

        // draw image
        ctx.drawImage(img, 0, 0);

        // export base64
        callback(canvas.toDataURL());
    };

    img.src = srcBase64;
}

1
Отличный подход. Вам также следует рассмотреть возможность обработки -2и -1возврата из, getOrientationчтобы не пытаться вращать не-jpgs или изображения без данных поворота.
Стивен Ламберт

Я произвел больше оптимизаций, см. Мой комментарий выше - я только что добавил и вашу file.slice()оптимизацию, но реальное преимущество дает использование меньших изображений и image/jpegвыходного формата для создания меньших URL-адресов данных. (нет заметной задержки даже для 10-мегапиксельных изображений.)
mindplay.dk

Осторожнее с тем file.slice(), как вы предотвратите поддержку источников изображений base64.
Mark

Знак благодарности - хотите предоставить альтернативу?
Статлер

7

Один лайнер кто-нибудь?

Я не видел, чтобы кто-нибудь упоминал browser-image-compressionбиблиотеку . У него есть вспомогательная функция, идеально подходящая для этого.

Использование: const orientation = await imageCompression.getExifOrientation(file)

Такой полезный инструмент и во многих других отношениях.


Это получает ориентацию. Но как мне создать новый объект .jpg File на стороне клиента, используя эту информацию?
masterxilo

Вы не можете создавать Fileобъекты, насколько я знаю. Следующим лучшим вариантом будет отправить изображение и ориентацию на сервер и выполнить там манипулирование файлами. Или вы можете создать строку base64 , используя canvasи toDataURLметод.
Гилберт-в

1
Нет, вы можете просто создать объект File из Blob. Файл Файл (части массива, имя файла String, свойства BlobPropertyBag); developer.mozilla.org/de/docs/Web/API/File
masterxilo

2
Золотая наклейка! @masterxilo
Гилберт-в

2
Это отлично сработало для меня! Я боролся с ориентацией в течение последних 2 дней! Все опубликованные операторы Transform Switch по какой-то причине не будут обрабатывать портретные изображения с моего телефона, это будут черные полосы. Возможно, потому что я пытался сжать и вращать одновременно.
cjd82187

2

Я создал класс в модуле ES6, который решает именно это.

Это 103 строки, без зависимостей, и довольно хорошо структурированные и документированные, которые легко модифицировать / использовать повторно.

Обрабатывает все 8 возможных ориентаций и основан на обещаниях.

Вот, пожалуйста, надеюсь, это кому-то еще поможет: https://gist.github.com/vdavid/3f9b66b60f52204317a4cc0e77097913


1

Я использую смешанный раствор (php + css).

Контейнеры необходимы для:

  • div.imgCont2 контейнер необходимо вращать;
  • div.imgCont1контейнер, необходимый для zoomOut - width:150%;
  • div.imgCont Контейнер необходим для полос прокрутки, когда изображение является zoomOut.

,

<?php
    $image_url = 'your image url.jpg';
    $exif = @exif_read_data($image_url,0,true);
    $orientation = @$exif['IFD0']['Orientation'];
?>

<style>
.imgCont{
    width:100%;
    overflow:auto;
}
.imgCont2[data-orientation="8"]{
    transform:rotate(270deg);
    margin:15% 0;
}
.imgCont2[data-orientation="6"]{
    transform:rotate(90deg);
    margin:15% 0;
}
.imgCont2[data-orientation="3"]{
    transform:rotate(180deg);
}
img{
    width:100%;
}
</style>

<div class="imgCont">
  <div class="imgCont1">
    <div class="imgCont2" data-orientation="<?php echo($orientation) ?>">
      <img src="<?php echo($image_url) ?>">
    </div>
  </div>
</div>

Спасибо, это кажется лучшим решением для моих нужд. Нет необходимости во внешних библиотеках и нет длинных блоков ненужного кода. Просто определите ориентацию из exif с помощью php, отправьте его в представление и используйте его для запуска классов CSS, которые поворачиваются на соответствующее количество градусов. Красиво и чисто! Редактировать: это перевернет изображения в браузерах, которые фактически читают данные exif и вращаются соответственно. Ака, это решит проблему на рабочем столе, но создаст новую на ios safari.
cwal

0

В дополнение к ответу @fareed namrouti,

Это следует использовать, если изображение нужно просматривать из элемента ввода файла

<input type="file" name="file" id="file-input"><br/>
image after transform: <br/>
<div id="container"></div>

<script>
    document.getElementById('file-input').onchange = function (e) {
        var image = e.target.files[0];
        window.loadImage(image, function (img) {
            if (img.type === "error") {
                console.log("couldn't load image:", img);
            } else {
                window.EXIF.getData(image, function () {
                    console.log("load image done!");
                    var orientation = window.EXIF.getTag(this, "Orientation");
                    var canvas = window.loadImage.scale(img,
                        {orientation: orientation || 0, canvas: true, maxWidth: 200});
                    document.getElementById("container").appendChild(canvas);
                    // or using jquery $("#container").append(canvas);
                });
            }
        });
    };
</script>

0

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

<?php

header("Content-type: image/jpeg");
$img = 'IMG URL';

$exif = @exif_read_data($img,0,true);
$orientation = @$exif['IFD0']['Orientation'];
if($orientation == 7 || $orientation == 8) {
    $degrees = 90;
} elseif($orientation == 5 || $orientation == 6) {
    $degrees = 270;
} elseif($orientation == 3 || $orientation == 4) {
    $degrees = 180;
} else {
    $degrees = 0;
}
$rotate = imagerotate(imagecreatefromjpeg($img), $degrees, 0);
imagejpeg($rotate);
imagedestroy($rotate);

?>

ура

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.