Acceso a datos de rotación JPEG EXIF ​​en JavaScript en el lado del cliente

125

Me gustaría rotar las fotos según su rotación original, según lo establecido por la cámara en los datos de imagen JPEG EXIF. El truco es que todo esto debería suceder en el navegador, usando JavaScript y <canvas>.

¿Cómo podría JavaScript acceder a JPEG, un objeto API de archivo local, local <img>o remoto <img>, datos EXIF ​​para leer la información de rotación?

Las respuestas del lado del servidor no están bien; Estoy buscando una solución del lado del cliente.

Mikko Ohtamaa
fuente

Respuestas:

261

Si solo desea la etiqueta de orientación y nada más y no le gusta incluir otra gran biblioteca de JavaScript, escribí un pequeño código que extrae la etiqueta de orientación lo más rápido posible (utiliza DataView y readAsArrayBufferestán disponibles en IE10 +, pero puede escribir su propio lector de datos para navegadores antiguos):

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

ingrese la descripción de la imagen aquí

Para aquellos que usan Typecript, pueden usar el siguiente 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);
}
Ali
fuente
para 2,4,5,7 para obtener la imagen correcta necesita girar y voltear, ¿verdad?
Muhammad Umer
La orientación de mi imagen es 3 ... ¿Cómo configuro la orientación en 1?
Lucy
3
@Mick PNG o GIF no tienen ningún formato estándar para almacenar la orientación de la imagen stackoverflow.com/questions/9542359/…
Ali
2
Trabajando para mí, pero necesitaba cambiar la última línea a solo reader.readAsArrayBuffer (archivo); sin el segmento, ya que tengo la intención de usar el búfer para mi imagen base64, de lo contrario, solo verá el primer segmento de la imagen. Por cierto, esto no es necesario si solo necesita la información de orientación. Gracias
Philip Murphy
2
@DaraJava Eliminé la parte de corte porque a veces la etiqueta entró después del límite, pero ralentizará la operación si nunca se encuentra la etiqueta. De todos modos, a diferencia de la etiqueta de orientación, la etiqueta Flash no está en el directorio IFD0 y mi código solo busca esta parte. para obtener la etiqueta Flash debe buscar en el directorio SubIFD. Puede encontrar un buen tutorial sobre EXIF ​​aquí: media.mit.edu/pia/Research/deepview/exif.html
Ali
22

Puede usar la biblioteca exif-js en combinación con la API de archivos 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
fuente
Gracias. La biblioteca JS en la pregunta parece un poco desactualizada, pero probablemente funcionaría.
Mikko Ohtamaa
Vea también mi demo de un widget de carga de archivos que acabo de escribir. Utiliza la biblioteca EXIF.js mencionada anteriormente para leer el indicador de orientación EXIF ​​en los metatdatos del archivo de imagen. Según la información, aplica la rotación utilizando un elemento de lienzo ... sandbox.juurlink.org/html5imageuploader
Rob Juurlink
Intentar incluso incluir binaryajax.js en mi proyecto provoca un error de acceso denegado.
Obi Wan
¿De dónde viene el objeto EXIF? El script BinaryFile no parece contenerlo, y por lo que puedo decir, no es parte de jquery ni de ningún otro script que use regularmente ...
jrista
66
El sitio web de la biblioteca parece inactivo, y las únicas otras bibliotecas de ExifReader que he encontrado estaban limitadas en el soporte del navegador. ¿Hay alguna buena alternativa?
Praxis Ashelin
19

Firefox 26 admite image-orientation: from-image: las imágenes se muestran en vertical u horizontal, según los datos EXIF. (Ver sethfowler.org/blog/2013/09/13/new-in-firefox-26-css-image-orientation ).

También hay un error para implementar esto en Chrome .

Tenga en cuenta que esta propiedad solo es compatible con Firefox y es probable que sea obsoleta .

Sam Dutton
fuente
55
Gracias por el enlace al informe de error. Lo destaqué para que el equipo de Chrome sepa que más personas quieren esto.
DemiImp
Según este comentario bugs.chromium.org/p/chromium/issues/detail?id=158753#c104 por un miembro del proyecto Chromium: "El cambio está en Chrome 81. Eso se lanzará al público como la versión estable en 8 -10 semanas "
jeff forest
1
Implementado en Chrome a partir de 81 🎉 Se tomará un tiempo antes que la gente actualizar su navegador, aunque - mantener un ojo en caniuse
Robin Métral
4

Si lo quieres entre navegadores, lo mejor es hacerlo en el servidor. Podría tener una API que tome una URL de archivo y le devuelva los datos EXIF; PHP tiene un módulo para eso .

Esto se puede hacer usando Ajax para que sea transparente para el usuario. Si no le importa la compatibilidad entre navegadores y puede confiar en la funcionalidad del archivo HTML5 , busque en la biblioteca JsJPEGmeta que le permitirá obtener esos datos en JavaScript nativo.

Alex Turpin
fuente
21
@MikkoOhtamaa: Debe comprender que Stack Overflow responde preguntas para todos , solo la persona original que lo pregunta. La próxima persona que tenga el mismo objetivo que usted puede ser un desarrollador de PHP: ¿por qué querría negarles la información que Xeon06 incluye? Era inapropiado edición que fuera, sólo porque usted no quiere una solución PHP.
Jon Skeet
55
La pregunta dice "en Javascript", por lo que la parte era irrelevante. Hay muchas otras preguntas y respuestas similares para PHP que ya están en el sitio y es un ruido innecesario con respecto a esta pregunta.
Mikko Ohtamaa
2
Si las personas solicitan una solución Javascript, no quieren ver la solución PHP como la primera publicación.
Mikko Ohtamaa
1
@MikkoOhtamaa parece que no está de acuerdo con usted meta.stackexchange.com/questions/157338 /... Parece que tiene un sentido de propiedad ilícito en las respuestas a sus preguntas.
Alex Turpin
1
Edité la respuesta para tener la respuesta correcta al principio. Perdón por la pelusa.
Mikko Ohtamaa
3

Vea un módulo que he escrito (puede usarlo en el navegador) que convierte la orientación exif a la transformación CSS: https://github.com/Sobesednik/exif2css

También existe este programa de nodo para generar dispositivos JPEG con todas las orientaciones: https://github.com/Sobesednik/generate-exif-fixtures

zavr
fuente
1
Buen módulo! Sin embargo, ¿cómo obtiene la información EXIF ​​de JPEG en primer lugar?
Mikko Ohtamaa
@MikkoOhtamaa gracias y no, no, tienes que hacerlo con exif-js o exiftool del lado del servidor
zavr
Esto es útil. Pero me parece que solo funciona correctamente para fotos de retratos, no de paisajes.
Sridhar Sarnobat
3

Subo el código de expansión para mostrar la foto de la cámara de Android en html de forma normal en alguna etiqueta img con rotación derecha, especialmente para la etiqueta img cuyo ancho es más ancho que la altura. Sé que este código es feo, pero no necesita instalar ningún otro paquete. (Utilicé el código anterior para obtener un valor de rotación exif, gracias).

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

y luego usar 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
fuente
2

Mejorando / agregando más funcionalidad a la respuesta de Ali de antes, creé un método util en Typecript que se adaptaba a mis necesidades para este problema. Esta versión devuelve la rotación en grados que también podría necesitar para su proyecto.

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
fuente