Orientación Exif del lado del cliente JS: rotar y reflejar imágenes JPEG

154

Las fotos de la cámara digital a menudo se guardan como JPEG con una etiqueta de "orientación" EXIF. Para que se muestren correctamente, las imágenes deben girarse / reflejarse según la orientación establecida, pero los navegadores ignoran esta información que representa la imagen. Incluso en grandes aplicaciones web comerciales, el soporte para la orientación EXIF ​​puede ser irregular 1 . La misma fuente también proporciona un buen resumen de las 8 orientaciones diferentes que puede tener un JPEG:

Resumen de orientaciones EXIF

Las imágenes de muestra están disponibles en 4 .

La pregunta es cómo rotar / reflejar la imagen en el lado del cliente para que se muestre correctamente y pueda procesarse si es necesario.

Hay bibliotecas JS disponibles para analizar datos EXIF, incluido el atributo de orientación 2 . Flickr observó un posible problema de rendimiento al analizar imágenes grandes, que requieren el uso de trabajadores web 3 .

Las herramientas de la consola pueden reorientar correctamente las imágenes 5 . Un script PHP para resolver el problema está disponible en 6

flexible
fuente

Respuestas:

145

El proyecto github JavaScript-Load-Image proporciona una solución completa al problema de orientación EXIF, rotando / reflejando correctamente las imágenes para las 8 orientaciones exif. Vea la demostración en línea de la orientación javascript exif

La imagen se dibuja en un lienzo HTML5. Su representación correcta se implementa en js / load-image-oriente.js a través de operaciones de lienzo.

Espero que esto ahorre algo de tiempo a alguien más y enseñe a los motores de búsqueda sobre esta joya de código abierto :)

flexible
fuente
1
He estado usando esta biblioteca, pero recientemente se rompió en iOS 8.3, que es donde más la necesito para funcionar :(
Gordon Sun
2
Realmente estoy luchando por ver cómo esto es útil. Estoy jugando con él tratando de aprenderlo para poder analizar una página y rotar las imágenes cuando es necesario rotarlas, o detectar la orientación y rotar el archivo (de alguna manera) antes de subirlo. La demo tampoco. Simplemente toma un archivo de una entrada de archivo y lo muestra de la manera correcta, ¿cuándo es esto útil en el mundo real? Cuando analizo mi página y alimenta las URL de las etiquetas de imagen a la biblioteca loadImage, no hay datos exif, por lo que no puedo hacer eso. Para la carga, devuelve un objeto de lienzo, por lo que no puedo enviarlo al servidor ni nada.
igneosaurio
3
@igneosaur: puede enviar datos codificados en base64 al servidor con canvas.toDataURL()y decodificar y guardarlo en el lado del servidor.
br4nnigan
1
en lugar de usar el proyecto load-image, puede usar parte de su código base para hornear el suyo, solo mire en github.com/blueimp/JavaScript-Load-Image/blob/master/js/… .
Andy Lorenz
1
¿Hay alguna manera de hacer que el lienzo que produce esta biblioteca responda como una <img>etiqueta normal ?
Erik Berkun-Drevnig
103

La transformación de contexto de Mederr funciona perfectamente. Si necesita extraer la orientación, use esta función ; no necesita ninguna biblioteca de lectura EXIF. A continuación se muestra una función para restablecer la orientación en la imagen base64. Aquí hay un violín para ello . También he preparado un violín con demostración de extracción de orientación .

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;
};
WunderBart
fuente
77
El caso predeterminado en la orientación switchno es necesario, ya que esa transformación no hace nada. Además, considere usar en srcOrientation > 4 && srcOrientation < 9lugar de [5,6,7,8].indexOf(srcOrientation) > -1, porque es más rápido y requiere menos recursos (tanto RAM como CPU). No hay necesidad de tener una matriz allí. Esto es importante cuando se agrupan muchas imágenes, donde cada bit cuenta. De lo contrario, muy buena respuesta. ¡Votado!
Ryan Casas
44
@RyanCasas No sabía cuán pesado puede indexOfser comparado con lo que propusiste. Ejecuté un bucle simple con 10M de iteraciones y fue 1400% más rápido. Niza: D ¡Muchas gracias!
WunderBart
1
Usé esta respuesta en un Android WebView y resultó que hay algunos dispositivos Android que no admiten WebGL dentro de un WebView (ver bugs.chromium.org/p/chromium/issues/detail?id=555116 ) La rotación puede llevar mucho tiempo en dichos dispositivos, dependiendo del tamaño de la imagen.
ndreisg
1
Cuando se usa con el referenciado getOrientation, tengo curiosidad por saber si esto es eficiente en términos de rendimiento. getOrientationllamadas fileReader.readAsArrayBuffer, y luego llamamos URL.createObjectURLy pasamos el resultado a resetOrientation, que carga esta URL como una imagen. ¿Significa esto que el archivo de imagen será "cargado" / leído por el navegador no una sino dos veces, o no lo entiendo?
Oliver Joseph Ash
1
Además, ¿qué hacer con los navegadores donde ya se respetará la orientación? Creo que este es el caso de iOS Safari, y en los navegadores que admiten CSS image-orientation: from-imagecuando se usa.
Oliver Joseph Ash el
40

Si

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

Luego puede usar estas transformaciones para convertir la imagen en orientación 1

De orientación:

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

Antes de dibujar la imagen en ctx

Mederr
fuente
58
¿Qué está pasando aquí?
Muhammad Umer
1
Con esto solo necesita conocer la orientación (por ejemplo, a través de EXIF) y luego rotar según sea necesario. La mitad de lo que estoy buscando.
Brenden
2
estas transformaciones no funcionaron para mí; en su lugar, utilicé el código del proyecto de carga de imágenes en github.com/blueimp/JavaScript-Load-Image/blob/master/js/… para obtener lo que creo que es un código bien probado que usa ambas operaciones de traslación, rotación en el contexto del lienzo más un intercambio de ancho / alto. Me dio 100% de resultados después de muchas horas de experimentación con otros intentos de transformación, etc.
Andy Lorenz
1
¿Por qué importa cuando dibujas la imagen en ctx? ¿No puede girar el lienzo después de dibujar una imagen en él?
AlxVallejo
La rotación para la orientación 8 funciona de esta manera para mí: ctx.transform (0, -1, 1, 0, 0, height);
Giorgio Barchiesi
24

ok, además de la respuesta @ user3096626, creo que será más útil si alguien proporciona un ejemplo de código, el siguiente ejemplo le mostrará cómo corregir la orientación de la imagen que proviene de la URL (imágenes remotas):


Solución 1: usar javascript (recomendado)

  1. debido a que la biblioteca de imágenes de carga no extrae etiquetas exif solo de imágenes de URL (archivo / blob), usaremos las bibliotecas javascript de exif-js y de carga de imágenes , así que primero agregue estas bibliotecas a su página de la siguiente manera:

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

    Tenga en cuenta que la versión 2.2 de exif-js parece tener problemas, por lo que utilizamos 2.1

  2. entonces básicamente lo que haremos es

    a - carga la imagen usando window.loadImage()

    b - lee las etiquetas exif usando window.EXIF.getData()

    c - convierta la imagen a lienzo y arregle la orientación de la imagen usando window.loadImage.scale()

    d - coloca el lienzo en el documento

aqui tienes :)

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

    });
  }
});

por supuesto, también puede obtener la imagen como base64 del objeto de lienzo y colocarla en el atributo img src, por lo que puede usar jQuery;)

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

Aquí está el código completo en: github: https://github.com/digital-flowers/loadimage-exif-example


Solución 2: usando html (pirateo del navegador)

hay un hack muy rápido y fácil, la mayoría de los navegadores muestran la imagen en la orientación correcta si la imagen se abre dentro de una nueva pestaña directamente sin ningún html (LOL, no sé por qué), así que básicamente puedes mostrar tu imagen usando iframe poniendo el atributo iframe src como la url de la imagen directamente:

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

Solución 3: usando CSS (solo Firefox y Safari en iOS)

hay un atributo css3 para corregir la orientación de la imagen, pero el problema es que solo funciona en firefox y safari / ios, todavía vale la pena mencionarlo porque pronto estará disponible para todos los navegadores (información de soporte del navegador de caniuse )

img {
   image-orientation: from-image;
}
Alnamrouti Fareed
fuente
La solución 1 no funcionó para mí. Está regresando window.loadImage no está definida
Perry
Esto sucedió si no incluyó las bibliotecas que menciono en el paso 1
Fareed Alnamrouti
Incluí las bibliotecas. Me gusta la simplicidad de su ejemplo, pero sigo recibiendo ese mensaje de error.
Perry
1
parece que exif-js se rompió, compruebe mi edición, también he agregado el código completo en github: github.com/digital-flowers/loadimage-exif-example
Fareed Alnamrouti
1
ok revisa el proyecto github, hay un nuevo archivo "upload.html", de nada :)
Fareed Alnamrouti
10

Para aquellos que tienen un archivo de un control de entrada, no saben cuál es su orientación, son un poco vagos y no quieren incluir una gran biblioteca a continuación, el código proporcionado por @WunderBart se combina con la respuesta a la que se vincula ( https://stackoverflow.com/a/32490603 ) que encuentra la orientación.

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

que fácilmente se puede llamar así

getDataUrl(input.files[0], function (imgBase64) {
      vm.user.BioPhoto = imgBase64;
});
runxc1 Bret Ferrier
fuente
2
Gracias después de más de 2 horas de búsqueda en Google, encuentro la solución correcta.
Mr.Trieu
2
¿Por qué alguien sería perezoso para preferir una solución mucho más ligera?
rocío
44
Portado a Typecript y optimizado de aproximadamente 4 segundos a ~ 20 milisegundos. La codificación Base64 fue el cuello de botella: usar en image/jpeglugar del predeterminado image/pngy cambiar el tamaño de la imagen de salida a un ancho máximo (400 px en mi caso) hizo una gran diferencia. (esto funciona bien para las miniaturas, pero si tiene que mostrar la imagen completa, sugiero evitar base64 y simplemente inyectar el elemento de lienzo directamente en la página ...)
mindplay.dk
8

La respuesta de WunderBart fue la mejor para mí. Tenga en cuenta que puede acelerarlo mucho si sus imágenes son a menudo correctas, simplemente probando primero la orientación y omitiendo el resto del código si no se requiere rotación.

Poniendo toda la información de wunderbart juntos, algo como esto;

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;
}
Statler
fuente
1
Gran enfoque También debe considerar manejar -2y -1regresar desde getOrientationpara no intentar rotar imágenes que no sean jpgs o sin datos de rotación.
Steven Lambert
Hice más optimizaciones, vea mi comentario más arriba : acabo de agregar su file.slice()optimización también, pero el impulso real proviene del uso de imágenes y image/jpegformatos de salida más pequeños para crear URL de datos más pequeños. (no hay un retraso notable incluso para imágenes de 10 megapíxeles.)
mindplay.dk
Tenga cuidado file.slice(), ya que evitará la compatibilidad con las fuentes de imagen base64.
Mark
Gracias, Mark: ¿quieres proporcionar una edición para una alternativa?
statler
7

Un trazador de líneas alguien?

No he visto a nadie mencionar la browser-image-compressionbiblioteca . Tiene una función auxiliar perfecta para esto.

Uso: const orientation = await imageCompression.getExifOrientation(file)

Una herramienta tan útil en muchas otras formas también.

gilbert-v
fuente
Esto obtiene la orientación. Pero, ¿cómo produzco un nuevo objeto de archivo .jpg en el lado del cliente utilizando esta información?
masterxilo
No puedes crear Fileobjetos hasta donde yo sé. Lo mejor sería enviar la imagen y la orientación al servidor y manipular los archivos allí. O podría generar una cadena base64 usando canvasy el toDataURLmétodo.
gilbert-v
1
No, puedes crear un objeto File desde un Blob muy bien. Archivo Archivo (partes de matriz, nombre de archivo de cadena, propiedades de BlobPropertyBag); developer.mozilla.org/de/docs/Web/API/File
masterxilo
2
Etiqueta de oro! @masterxilo
gilbert-v
2
¡Esto funciono muy bien para mi! ¡Había estado luchando con las orientaciones durante los últimos 2 días! Todas las declaraciones de cambio de transformación que se publicaron no manejarían imágenes de retrato de mi teléfono por alguna razón, habría barras negras. Probablemente porque estaba tratando de comprimir y rotar al mismo tiempo.
cjd82187
2

Creé una clase envuelta en un módulo ES6 que resuelve exactamente esto.

Son 103 líneas, sin dependencias, y bastante bien estructuradas y documentadas, destinadas a ser fáciles de modificar / reutilizar.

Maneja las 8 orientaciones posibles y está basado en promesas.

Aquí tienes, espero que esto ayude a alguien: https://gist.github.com/vdavid/3f9b66b60f52204317a4cc0e77097913

Dávid Veszelovszki
fuente
1

Estoy usando una solución mixta (php + css).

Se necesitan contenedores para:

  • div.imgCont2 contenedor necesario para rotar;
  • div.imgCont1contenedor necesario para zoomOut - width:150%;
  • div.imgCont contenedor necesario para barras de desplazamiento, cuando la imagen es 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>
Sergey S
fuente
Gracias, esta parece ser la mejor solución, para mis necesidades de todos modos. No necesita bibliotecas externas ni bloques largos de código innecesario. Simplemente determine la orientación del exif con php, envíelo a la vista y úselo para activar clases CSS que roten el número apropiado de grados. ¡Bonito y limpio! Editar: esto rotará las imágenes en los navegadores que realmente leen los datos exif y rotan en consecuencia. AKA solucionará el problema en el escritorio, pero crea uno nuevo en ios safari.
cwal
0

Además de la respuesta de @fareed namrouti,

Esto debe usarse si la imagen tiene que ser explorada desde un elemento de entrada de archivo

<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>
Sidra de pera
fuente
0

He escrito un pequeño script php que rota la imagen. Asegúrese de almacenar la imagen a favor de volver a calcularla en cada solicitud.

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

?>

Salud

Thom
fuente