Експертна орієнтація на стороні клієнта JS: обертання та дзеркальні зображення JPEG


154

Фотографії цифрових фотоапаратів часто зберігаються у форматі JPEG із тегом EXIF ​​"орієнтація". Щоб правильно відобразити зображення, потрібно обертати / дзеркально відображати залежно від того, яка орієнтація встановлена, але браузери ігнорують цю інформацію, яка відображає зображення. Навіть у великих комерційних веб-додатках підтримка орієнтації EXIF ​​може бути помітною 1 . Це ж джерело також дає короткий підсумок 8 різних орієнтацій, які може мати JPEG:

Короткий зміст орієнтирів EXIF

Зразки зображень доступні на 4 .

Питання полягає в тому, як обертати / дзеркально відображати зображення на стороні клієнта, щоб воно відображалось правильно та було можливо додатково оброблятися?

Існують бібліотеки JS для аналізу даних EXIF, включаючи атрибут 2 орієнтації . Flickr відзначив можливу проблему з продуктивністю під час аналізу великих зображень, що вимагає використання веб-працівників 3 .

Консольні інструменти можуть правильно переорієнтувати зображення 5 . PHP-скрипт для вирішення проблеми доступний на 6

Відповіді:


145

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

Зображення малюється на полотні HTML5. Його правильне візуалізація реалізована в js / load-image-орієнтація.js за допомогою операцій canvas.

Сподіваюся, що це економить когось ще деякий час, і вчить пошукові системи про цей дорогоцінний камінь з відкритим кодом :)


1
Я використовую цю бібліотеку, але останнім часом вона зламалася на iOS 8.3, і саме там мені потрібно, щоб вона працювала найбільше :(
Gordon Sun

2
Мені дуже важко бачити, як це корисно. Я граю з ним, намагаючись дізнатися його, щоб я міг розібрати сторінку та обертати зображення, коли їх потрібно обертати, або виявити орієнтацію та повернути файл (якось) перед тим, як фактично його завантажувати. Демонстрація не робить жодного. Він просто бере файл з файлового вводу і відображає його правильним чином, коли це корисно в реальному світі? Коли я розбираю свою сторінку і передаю URL-адреси з тегів зображень у бібліотеку loadImage, немає даних exif, тому не можу цього зробити. Під час завантаження він повертає об’єкт полотна, тому я не можу надіслати його на сервер чи що-небудь.
Ігнеозавр

3
@igneosaur: ви можете надіслати кодовані дані base64 на сервер canvas.toDataURL()і декодувати і зберегти його на стороні сервера.
br4nnigan

1
замість того, щоб використовувати проект завантаження зображень, ви можете використовувати деякі його кодові бази для створення власного - просто подивіться на 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, оскільки це швидше і менш ресурсомістке (як оперативна пам'ять, так і процесор). Там не потрібно мати масив. Це важливо під час створення великої кількості зображень, де кожен біт рахується. Інакше досить гарна відповідь. Оголошено!
Ryan Casas

4
@RyanCasas Я не знав, як важко можна indexOfпорівняти з тим, що ти запропонував. Я запустив просту петлю з 10M ітераціями, і це було на 1400% швидше. Приємно: D Дякую купу!
WunderBart

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

1
При використанні з посиланням getOrientationмені цікаво, чи ефективно це з точки зору продуктивності. getOrientationдзвінки fileReader.readAsArrayBuffer, а потім ми дзвонимо URL.createObjectURLі передаємо результат resetOrientation, який завантажує цю URL-адресу як зображення. Чи означає це, що файл зображення буде завантажений / прочитаний браузером не один раз, а двічі, або я неправильно розумію?
Олівер Джозеф Еш

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

40

Якщо

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
ці перетворення для мене не спрацювали - замість цього я використав код проекту завантаження зображень на сайті 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. оскільки бібліотека завантажувальних зображень не витягує теги exif лише з зображень URL-адреси (файл / blob), ми будемо використовувати як бібліотеки JavaScript- файлів exif-js, так і завантажувальних зображень , тому спочатку додайте ці бібліотеки на вашу сторінку в такий спосіб:

    <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. то в основному те, що ми будемо робити, - це

    a - завантажте зображення за допомогою window.loadImage()

    б - читати теги 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, все одно варто згадати, оскільки незабаром вона стане доступною для всіх браузерів (інформація про підтримку браузера від каніуси )

img {
   image-orientation: from-image;
}

Рішення 1 для мене не спрацювало. Повертається window.loadImage не визначено
Перрі

Це сталося, якщо ви не включили бібліотеки, про які я згадую в кроці 1
Fareed Alnamrouti

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

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 та просто вводити елемент полотна прямо на сторінку ...)
mindplay.dk

8

Відповідь WunderBart була найкращою для мене. Зауважте, що ви можете значно пришвидшити, якщо ваші зображення часто є правильним шляхом, просто попередньо випробувавши орієнтацію та обійшовши решту коду, якщо обертання не потрібно.

Збираючи всю інформацію з 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.
Марк

Знак подяки - дбайливо запропонувати редагування альтернативи?
statler

7

Один лайнер хтось?

Я не бачив, щоб хтось згадував про browser-image-compressionбібліотеку . Для цього ідеально підійде допоміжна функція.

Використання: const orientation = await imageCompression.getExifOrientation(file)

Такий корисний інструмент теж у багатьох інших способах.


Це отримує орієнтацію. Але як я можу створити новий .jpg файл-об’єкт на стороні клієнта, використовуючи цю інформацію?
masterxilo

Ви не можете створювати Fileоб'єкти, наскільки я знаю. Наступним найкращим моментом буде надіслати зображення та орієнтацію на сервер і зробити там маніпуляції файлами. Або ви можете створити рядок base64 за допомогою canvasта toDataURLметоду.
gilbert-v

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

2
Золота наклейка! @masterxilo
-v

2
Це чудово працювало для мене! Я боровся з орієнтаціями останні 2 дні! Усі опубліковані заяви перемикання перетворені чомусь не оброблятимуть портретні зображення з мого телефону, там будуть чорні смуги. Можливо, тому, що я намагався одночасно стискати і обертати.
cjd82187

2

Я створив клас, загорнутий у модуль ES6, який вирішує саме це.

Це 103 рядки, ніяких залежностей, і досить добре структуровані та задокументовані, покликані легко змінювати / використовувати повторно.

Обробляє всі 8 можливих орієнтацій і базується на Обіцянні.

Ось, сподіваюся, це все-таки допомагає комусь: https://gist.github.com/vdavid/3f9b66b60f52204317a4cc0e77097913


1

Я використовую змішаний розчин (php + css).

Контейнери потрібні для:

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

.

<?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, та обертання відповідно. AKA це виправить проблему на робочому столі, але створить новий на 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.