Acessando dados de rotação JPEG EXIF ​​em JavaScript no lado do cliente

125

Gostaria de girar as fotos com base na rotação original, conforme definido pela câmera nos dados de imagem JPEG EXIF. O truque é que tudo isso deve acontecer no navegador, usando JavaScript e <canvas>.

Como o JavaScript pode acessar o JPEG, um objeto de API de arquivo local, local <img>ou remoto <img>, dados EXIF ​​para ler as informações de rotação?

As respostas do servidor não estão OK; Estou procurando uma solução do lado do cliente .

Mikko Ohtamaa
fonte

Respostas:

261

Se você deseja apenas a tag de orientação e nada mais e não gosta de incluir outra enorme biblioteca javascript, escrevi um pequeno código que extrai a tag de orientação o mais rápido possível (ele usa o DataView e readAsArrayBufferestá disponível no IE10 +, mas você pode escrever seu próprio leitor de dados para navegadores mais antigos):

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' />

valores:

-2: not jpeg
-1: not defined

insira a descrição da imagem aqui

Para aqueles que usam o Typecript, você pode usar o seguinte código:

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);
}
Todos
fonte
para 2,4,5,7 para obter a imagem correta, você precisa girar e virar, certo?
Muhammad Umer 27/03
A orientação da minha imagem é 3. Como posso definir a orientação para 1 ??
17446 Lucy
3
O @Mick PNG ou GIF não tem formato padrão para armazenar a orientação da imagem stackoverflow.com/questions/9542359/… #
Ali Ali
2
Trabalhando para mim, mas eu precisava alterar a última linha para apenas reader.readAsArrayBuffer (file); sem a fatia, como pretendo usar o buffer da minha imagem base64, caso contrário, você verá a primeira fatia da imagem. BTW, isso não é necessário se você precisar apenas das informações de orientação. Obrigado
Philip Murphy
2
@DaraJava Eu removi a parte da fatia porque às vezes a tag entrava após o limite, mas retardará a operação se a tag nunca for encontrada. De qualquer forma, diferentemente da tag de orientação, a tag do Flash não está no diretório IFD0 e meu código pesquisa apenas nesta parte. Para obter a tag Flash, você deve procurar no diretório SubIFD. Você pode encontrar um bom tutorial sobre EXIF ​​aqui: media.mit.edu/pia/Research/deepview/exif.html
Ali
22

Você pode usar a biblioteca exif-js em combinação com a API de arquivos 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
});
pimvdb
fonte
Obrigado. A biblioteca JS na pergunta parece um pouco desatualizada, mas provavelmente funcionaria.
Mikko Ohtamaa 21/10
Veja também minha demonstração de um widget de upload de arquivo que acabei de escrever. Ele usa a biblioteca EXIF.js mencionada acima para ler o sinalizador de orientação EXIF ​​nos metadados do arquivo de imagem. Com base nas informações, aplica-se a rotação usando um elemento de tela ... sandbox.juurlink.org/html5imageuploader
Rob Juurlink
Tentar incluir até o binaryajax.js no meu projeto causa um erro de acesso negado.
Obi Wan
De onde vem o objeto EXIF? Se o script BinaryFile não parece contê-lo, e, tanto quanto eu posso dizer, ele não faz parte do jQuery ou qualquer outro script que eu uso regularmente ...
jrista
6
O site da biblioteca parece inoperante e as únicas outras bibliotecas ExifReader que encontrei foram limitadas no suporte ao navegador. Existe alguma boa alternativa?
Praxis Ashelin
19

O Firefox 26 suporta image-orientation: from-image: as imagens são exibidas em retrato ou paisagem, dependendo dos dados EXIF. (Consulte sethfowler.org/blog/2013/09/13/new-in-firefox-26-css-image-orientation .)

Há também um erro para implementar isso no Chrome .

Lembre-se de que essa propriedade é suportada apenas pelo Firefox e provavelmente será preterida .

Sam Dutton
fonte
5
Obrigado pelo link para o relatório de erros. Eu o destaquei para que a equipe do Chrome saiba que mais pessoas querem isso.
DemiImp
De acordo com este comentário bugs.chromium.org/p/chromium/issues/detail?id=158753#c104 por um membro do projeto Chromium: "A alteração está no Chrome 81. Isso será apresentado ao público como a versão estável em 8 -10 semanas
jeff forest
1
Implementado no Chrome a partir de 81 🎉 Vai demorar um pouco até que as pessoas atualizem seu navegador - fique de olho no canal
Robin Métral
4

Se você quiser usar o navegador, sua melhor aposta é fazê-lo no servidor. Você pode ter uma API que utiliza uma URL de arquivo e retorna os dados EXIF; PHP tem um módulo para isso .

Isso pode ser feito usando o Ajax, para que seja fácil para o usuário. Se você não se importa com a compatibilidade entre navegadores e pode confiar na funcionalidade do arquivo HTML5 , consulte a biblioteca JsJPEGmeta que permitirá obter esses dados em JavaScript nativo.

Alex Turpin
fonte
21
@MikkoOhtamaa: Você precisa entender que o Stack Overflow responde perguntas para todos , apenas para a pessoa original. A próxima pessoa que tem o mesmo objetivo que você pode ser um desenvolvedor de PHP - por que você gostaria de negar a eles as informações que o Xeon06 incluiu? Não foi adequado editar isso, apenas porque você não deseja uma solução PHP.
quer
5
A pergunta diz "em Javascript", portanto a peça era irrelevante. Já existem muitas outras perguntas e respostas semelhantes para o PHP no site e há ruído desnecessário em relação a essa pergunta.
Mikko Ohtamaa 03/12/12
2
Se as pessoas pedem uma solução Javascript, elas não querem ver a solução PHP como o primeiro post.
Mikko Ohtamaa 3/12/12
1
@MikkoOhtamaa, parece que a maioria discorda de você meta.stackexchange.com/questions/157338/… Você parece ter algum senso de propriedade errado nas respostas às suas perguntas.
Alex Turpin #
1
Eu editei a resposta para ter a resposta correta no início. Desculpe pela penugem.
Mikko Ohtamaa
3

Confira um módulo que eu escrevi (você pode usá-lo no navegador) que converte a orientação exif para a transformação CSS: https://github.com/Sobesednik/exif2css

Há também este programa de nó para gerar equipamentos JPEG com todas as orientações: https://github.com/Sobesednik/generate-exif-fixtures

zavr
fonte
1
Nice module! No entanto, como obtém informações EXIF ​​do JPEG em primeiro lugar?
Mikko Ohtamaa
@MikkoOhtamaa graças e nah isso não acontecer, você tem que fazê-lo com EXIF-js ou server-side exiftool
zavr
Isso é útil. Mas parece-me que só funciona corretamente para fotos de retrato, não para fotos de paisagem.
Sridhar Sarnobat 04/04
3

Eu carrego o código de expansão para mostrar a foto pela câmera do Android em html como normal em alguma tag img com rotação correta, especialmente para tag img cuja largura é maior que a altura. Eu sei que esse código é feio, mas você não precisa instalar nenhum outro pacote. (Eu usei o código acima para obter o valor de rotação exif, obrigado.)

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)
    );
}

e depois use como

$("#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]);

});
Wonhyuk Cho
fonte
2

Melhorando / Adicionando mais funcionalidades à resposta de Ali, criei um método util no Typescript que atendia às minhas necessidades para esse problema. Esta versão retorna a rotação em graus que você também pode precisar para o seu projeto.

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;
}

Uso:

import { getOrientation } from './ImageUtils';
...
onDrop = (pics: any) => {
  getOrientation(pics[0], rotationInDegrees => {
    this.setState({ image: pics[0], rotate: rotationInDegrees });
  });
};
Kevin Grant
fonte