Доступ к данным вращения JPEG EXIF ​​в JavaScript на стороне клиента


125

Я хочу повернуть фотографии на основе их исходного поворота, установленного камерой в данных изображения JPEG EXIF. Хитрость в том, что все это должно происходить в браузере с использованием JavaScript и <canvas>.

Как JavaScript может получить доступ к JPEG, локальному объекту файлового API, локальному <img>или удаленному <img>, к данным EXIF ​​для чтения информации о ротации?

Ответы на стороне сервера не подходят; Ищу клиентское решение.

Ответы:


261

Если вам нужен только тег ориентации и ничего больше и вы не хотите включать еще одну огромную библиотеку javascript, я написал небольшой код, который извлекает тег ориентации как можно быстрее (он использует DataView и readAsArrayBufferкоторые доступны в IE10 +, но вы можете написать ваш собственный считыватель данных для старых браузеров):

function getOrientation(file, callback) {
    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) 
        {
            if (view.getUint16(offset+2, false) <= 8) return callback(-1);
            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);
}

// usage:
var input = document.getElementById('input');
input.onchange = function(e) {
    getOrientation(input.files[0], function(orientation) {
        alert('orientation: ' + orientation);
    });
}
<input id='input' type='file' />

ценности:

-2: not jpeg
-1: not defined

введите описание изображения здесь

Для тех, кто использует Typescript, вы можете использовать следующий код:

export const getOrientation = (file: File, callback: Function) => {
  var reader = new FileReader();

  reader.onload = (event: ProgressEvent) => {

    if (! event.target) {
      return;
    }

    const file = event.target as FileReader;
    const view = new DataView(file.result as ArrayBuffer);

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

    const length = view.byteLength
    let offset = 2;

    while (offset < length)
    {
        if (view.getUint16(offset+2, false) <= 8) return callback(-1);
        let marker = view.getUint16(offset, false);
        offset += 2;

        if (marker == 0xFFE1) {
          if (view.getUint32(offset += 2, false) != 0x45786966) {
            return callback(-1);
          }

          let little = view.getUint16(offset += 6, false) == 0x4949;
          offset += view.getUint32(offset + 4, little);
          let tags = view.getUint16(offset, little);
          offset += 2;
          for (let 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);
}

для 2,4,5,7, чтобы получить правильное изображение, вам нужно вращать и переворачивать, верно?
Мухаммад Умер,

Ориентация моего изображения 3 .. Как мне установить ориентацию на 1 ??
Люси

3
@Mick PNG или GIF не имеет стандартного формата для хранения ориентации изображения stackoverflow.com/questions/9542359/…
Али

2
Работает для меня, но мне нужно было заменить последнюю строку на reader.readAsArrayBuffer (file); без среза, поскольку я собираюсь использовать буфер для своего изображения base64, иначе вы увидите только первый фрагмент изображения. Кстати, это не требуется, если вам просто нужна информация об ориентации. Спасибо
Филип Мерфи

2
@DaraJava Я удалил часть среза, потому что иногда тег появлялся после ограничения, но это замедляло работу, если тег никогда не был найден. В любом случае, в отличие от тега ориентации, тег Flash не находится в каталоге IFD0, и мой код выполняет поиск только в этой части. чтобы получить тег Flash, вы должны выполнить поиск в каталоге SubIFD. Вы можете найти хорошее руководство по EXIF ​​здесь: media.mit.edu/pia/Research/deepview/exif.html
Али

22

Вы можете использовать библиотеку exif-js в сочетании с API файлов HTML5: http://jsfiddle.net/xQnMd/1/ .

$("input").change(function() {
    var file = this.files[0];  // file
        fr   = new FileReader; // to read file contents

    fr.onloadend = function() {
        // get EXIF data
        var exif = EXIF.readFromBinaryFile(new BinaryFile(this.result));

        // alert a value
        alert(exif.Make);
    };

    fr.readAsBinaryString(file); // read the file
});

Спасибо. JS lib в вопросе выглядит немного устаревшим, но, вероятно, будет работать.
Микко Охтамаа

См. Также мою демонстрацию только что написанного виджета загрузки файлов. Он использует упомянутую выше библиотеку EXIF.js для чтения флага ориентации EXIF ​​в метаданных файла изображения. Основываясь на информации, он применяет вращение с помощью элемента холста ... sandbox.juurlink.org/html5imageuploader
Роб Джурлинк

Попытка даже включить binaryajax.js в мой проект вызывает ошибку отказа в доступе.
Оби Ван

Откуда взялся объект EXIF? Скрипт BinaryFile, кажется, не содержит его, и, насколько я могу судить, он не является частью jquery или любого другого скрипта, который я регулярно использую ...
jrista

6
Веб-сайт библиотеки не работает, и единственные другие библиотеки ExifReader, которые я нашел, были ограничены в поддержке браузером. Есть ли хорошая альтернатива?
Praxis Ashelin

19

Firefox 26 поддерживает image-orientation: from-image: изображения отображаются в книжной или альбомной ориентации, в зависимости от данных EXIF. (См. Sethfowler.org/blog/2013/09/13/new-in-firefox-26-css-image-orientation .)

Также есть ошибка для реализации этого в Chrome .

Помните, что это свойство поддерживается только Firefox и, скорее всего, станет устаревшим .


5
Спасибо за ссылку на отчет об ошибке. Я пометил его звездочкой, чтобы команда Chrome знала, что этого хочет больше людей.
DemiImp

В соответствии с этим комментарием bugs.chromium.org/p/chromium/issues/detail?id=158753#c104 участника проекта Chromium: «Изменения внесены в Chrome 81. Это будет выпущено для широкой публики как стабильная версия в версии 8. -10 недель »
Джефф Форест,

1
Реализовано в Chrome, начиная с версии 81 🎉 Тем не менее, потребуется некоторое время, прежде чем люди обновят свой браузер - следите за caniuse
Робин Метраль

11

https://github.com/blueimp/JavaScript-Load-Image - это современная библиотека javascript, которая может не только извлекать флаг ориентации exif, но и правильно отображать / поворачивать изображения JPEG на стороне клиента.

Я только что решил ту же проблему с этой библиотекой: JS Client-Side Exif Orientation: Rotate and Mirror JPEG Images


4

Если вы хотите кроссбраузерность, лучше всего сделать это на сервере. У вас может быть API, который принимает URL-адрес файла и возвращает вам данные EXIF; В PHP есть модуль для этого .

Это можно сделать с помощью Ajax, чтобы пользователю было удобно. Если вас не волнует кроссбраузерная совместимость и вы можете полагаться на функциональность файлов HTML5 , загляните в библиотеку JsJPEGmeta , которая позволит вам получить эти данные в собственном JavaScript.


21
@MikkoOhtamaa: Вы должны понимать, что Stack Overflow отвечает на вопросы всех , только того, кто их задает. Следующий человек, имеющий ту же цель, что и вы, может быть разработчиком PHP - зачем вам отказывать ему в информации, содержащейся в Xeon06? Редактировать это было неуместно только потому, что вам не нужно решение PHP.
Джон Скит,

5
В вопросе написано «в Javascript», поэтому часть не имеет значения. На сайте уже есть много других подобных вопросов и ответов для PHP, и это лишний шум по этому вопросу.
Микко Охтамаа

2
Если люди просят решение Javascript, они не хотят видеть решение PHP в качестве первого поста.
Микко Охтамаа

1
@MikkoOhtamaa, похоже, большинство с вами не согласны meta.stackexchange.com/questions/157338/… Похоже, у вас есть какое-то неправомерное чувство собственности на ответы на ваши вопросы.
Alex Turpin

1
Я отредактировал ответ, чтобы в начале был правильный ответ. Простите за пух.
Микко Охтамаа

3

Посмотрите на модуль, который я написал (вы можете использовать его в браузере), который преобразует ориентацию exif в преобразование CSS: https://github.com/Sobesednik/exif2css

Также существует эта программа узла для создания фикстур JPEG со всеми ориентациями: https://github.com/Sobesednik/generate-exif-fixtures


1
Хороший модуль! Однако, как вообще получить информацию EXIF ​​из JPEG?
Микко Охтамаа

@MikkoOhtamaa, спасибо, а нет, это не так, вы должны делать это с помощью exif-js или exiftool на стороне сервера
zavr

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

3

Я загружаю код расширения, чтобы показать фотографию с камеры Android в html как обычно на каком-то теге img с правильным вращением, особенно для тега img, ширина которого шире, чем высота. Я знаю, что этот код уродлив, но вам не нужно устанавливать другие пакеты. (Я использовал приведенный выше код для получения значения поворота exif, спасибо.)

function getOrientation(file, callback) {
  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);
}

var isChanged = false;
function rotate(elem, orientation) {
    if (isIPhone()) return;

    var degree = 0;
    switch (orientation) {
        case 1:
            degree = 0;
            break;
        case 2:
            degree = 0;
            break;
        case 3:
            degree = 180;
            break;
        case 4:
            degree = 180;
            break;
        case 5:
            degree = 90;
            break;
        case 6:
            degree = 90;
            break;
        case 7:
            degree = 270;
            break;
        case 8:
            degree = 270;
            break;
    }
    $(elem).css('transform', 'rotate('+ degree +'deg)')
    if(degree == 90 || degree == 270) {
        if (!isChanged) {
            changeWidthAndHeight(elem)
            isChanged = true
        }
    } else if ($(elem).css('height') > $(elem).css('width')) {
        if (!isChanged) {
            changeWidthAndHeightWithOutMargin(elem)
            isChanged = true
        } else if(degree == 180 || degree == 0) {
            changeWidthAndHeightWithOutMargin(elem)
            if (!isChanged)
                isChanged = true
            else
                isChanged = false
        }
    }
}


function changeWidthAndHeight(elem){
    var e = $(elem)
    var width = e.css('width')
    var height = e.css('height')
    e.css('width', height)
    e.css('height', width)
    e.css('margin-top', ((getPxInt(height) - getPxInt(width))/2).toString() + 'px')
    e.css('margin-left', ((getPxInt(width) - getPxInt(height))/2).toString() + 'px')
}

function changeWidthAndHeightWithOutMargin(elem){
    var e = $(elem)
    var width = e.css('width')
    var height = e.css('height')
    e.css('width', height)
    e.css('height', width)
    e.css('margin-top', '0')
    e.css('margin-left', '0')
}

function getPxInt(pxValue) {
    return parseInt(pxValue.trim("px"))
}

function isIPhone(){
    return (
        (navigator.platform.indexOf("iPhone") != -1) ||
        (navigator.platform.indexOf("iPod") != -1)
    );
}

а затем используйте, например,

$("#banner-img").change(function () {
    var reader = new FileReader();
    getOrientation(this.files[0], function(orientation) {
        rotate($('#banner-img-preview'), orientation, 1)
    });

    reader.onload = function (e) {
        $('#banner-img-preview').attr('src', e.target.result)
        $('#banner-img-preview').css('display', 'inherit')

    };

    // read the image file as a data URL.
    reader.readAsDataURL(this.files[0]);

});

2

Улучшение / добавление дополнительных функций к ответу Али из ранее, я создал метод util в Typescript, который соответствует моим потребностям для этой проблемы. Эта версия возвращает вращение в градусах, которое также может понадобиться для вашего проекта.

ImageUtils.ts

/**
 * Based on StackOverflow answer: https://stackoverflow.com/a/32490603
 *
 * @param imageFile The image file to inspect
 * @param onRotationFound callback when the rotation is discovered. Will return 0 if if it fails, otherwise 0, 90, 180, or 270
 */
export function getOrientation(imageFile: File, onRotationFound: (rotationInDegrees: number) => void) {
  const reader = new FileReader();
  reader.onload = (event: ProgressEvent) => {
    if (!event.target) {
      return;
    }

    const innerFile = event.target as FileReader;
    const view = new DataView(innerFile.result as ArrayBuffer);

    if (view.getUint16(0, false) !== 0xffd8) {
      return onRotationFound(convertRotationToDegrees(-2));
    }

    const length = view.byteLength;
    let offset = 2;

    while (offset < length) {
      if (view.getUint16(offset + 2, false) <= 8) {
        return onRotationFound(convertRotationToDegrees(-1));
      }
      const marker = view.getUint16(offset, false);
      offset += 2;

      if (marker === 0xffe1) {
        if (view.getUint32((offset += 2), false) !== 0x45786966) {
          return onRotationFound(convertRotationToDegrees(-1));
        }

        const little = view.getUint16((offset += 6), false) === 0x4949;
        offset += view.getUint32(offset + 4, little);
        const tags = view.getUint16(offset, little);
        offset += 2;
        for (let i = 0; i < tags; i++) {
          if (view.getUint16(offset + i * 12, little) === 0x0112) {
            return onRotationFound(convertRotationToDegrees(view.getUint16(offset + i * 12 + 8, little)));
          }
        }
        // tslint:disable-next-line:no-bitwise
      } else if ((marker & 0xff00) !== 0xff00) {
        break;
      } else {
        offset += view.getUint16(offset, false);
      }
    }
    return onRotationFound(convertRotationToDegrees(-1));
  };
  reader.readAsArrayBuffer(imageFile);
}

/**
 * Based off snippet here: https://github.com/mosch/react-avatar-editor/issues/123#issuecomment-354896008
 * @param rotation converts the int into a degrees rotation.
 */
function convertRotationToDegrees(rotation: number): number {
  let rotationInDegrees = 0;
  switch (rotation) {
    case 8:
      rotationInDegrees = 270;
      break;
    case 6:
      rotationInDegrees = 90;
      break;
    case 3:
      rotationInDegrees = 180;
      break;
    default:
      rotationInDegrees = 0;
  }
  return rotationInDegrees;
}

Использование:

import { getOrientation } from './ImageUtils';
...
onDrop = (pics: any) => {
  getOrientation(pics[0], rotationInDegrees => {
    this.setState({ image: pics[0], rotate: rotationInDegrees });
  });
};
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.