Accès aux données de rotation JPEG EXIF ​​en JavaScript côté client

125

Je voudrais faire pivoter les photos en fonction de leur rotation d'origine, comme défini par l'appareil photo dans les données d'image JPEG EXIF. L'astuce est que tout cela devrait se produire dans le navigateur, en utilisant JavaScript et <canvas>.

Comment JavaScript pourrait-il accéder au JPEG, un objet API de fichier local, local <img>ou distant <img>, aux données EXIF ​​pour lire les informations de rotation?

Les réponses côté serveur ne sont pas OK; Je recherche une solution côté client .

Mikko Ohtamaa
la source

Réponses:

261

Si vous ne voulez que la balise d'orientation et rien d'autre et que vous n'aimez pas inclure une autre énorme bibliothèque javascript, j'ai écrit un petit code qui extrait la balise d'orientation le plus rapidement possible (il utilise DataView et readAsArrayBufferqui sont disponibles dans IE10 +, mais vous pouvez écrire votre propre lecteur de données pour les anciens navigateurs):

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

valeurs:

-2: not jpeg
-1: not defined

entrez la description de l'image ici

Pour ceux qui utilisent Typescript, vous pouvez utiliser le code suivant:

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
la source
pour 2,4,5,7 pour obtenir une image correcte, vous devez faire pivoter et retourner, non?
Muhammad Umer
L'orientation de mon image est 3 .. Comment définir l'orientation sur 1 ??
Lucy
3
@Mick PNG ou GIF n'ont pas de format standard pour stocker l'orientation de l'image stackoverflow.com/questions/9542359/…
Ali
2
Travailler pour moi, mais j'avais besoin de changer la dernière ligne en juste reader.readAsArrayBuffer (file); sans la tranche car j'ai l'intention d'utiliser le tampon pour mon image base64, sinon, vous ne verrez que la première tranche de l'image. BTW, ce n'est pas nécessaire si vous avez juste besoin des informations d'orientation. Merci
Philip Murphy
2
@DaraJava J'ai supprimé la partie tranche parce que parfois la balise arrivait après la limite, mais cela ralentira l'opération si la balise n'est jamais trouvée. Quoi qu'il en soit, contrairement à la balise d'orientation, la balise Flash n'est pas dans le répertoire IFD0 et mon code ne recherche que cette partie. pour obtenir la balise Flash, vous devez rechercher le répertoire SubIFD. Vous pouvez trouver un bon tutoriel sur EXIF ​​ici: media.mit.edu/pia/Research/deepview/exif.html
Ali
22

Vous pouvez utiliser les exif-js bibliothèque en combinaison avec l'API de fichier 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
la source
Merci. La bibliothèque JS dans la question semble un peu obsolète, mais fonctionnerait probablement.
Mikko Ohtamaa
Voir aussi ma démo d'un widget de téléchargement de fichier que je viens d'écrire. Il utilise la bibliothèque EXIF.js mentionnée ci-dessus pour lire l'indicateur d'orientation EXIF ​​dans les métadonnées du fichier image. Sur la base des informations, il applique la rotation à l'aide d'un élément canvas ... sandbox.juurlink.org/html5imageuploader
Rob Juurlink
Tenter d'inclure même binaryajax.js dans mon projet provoque une erreur d'accès refusé.
Obi Wan
D'où vient l'objet EXIF? Le script BinaryFile ne semble pas le contenir, et pour autant que je sache, il ne fait pas partie de jquery ou de tout autre script que j'utilise régulièrement ...
jrista
6
Le site Web de la bibliothèque semble en panne et les seules autres bibliothèques ExifReader que j'ai trouvées étaient limitées dans la prise en charge du navigateur. Existe-t-il une bonne alternative?
Praxis Ashelin
19

Firefox 26 prend en charge image-orientation: from-image: les images sont affichées en portrait ou en paysage, selon les données EXIF. (Voir sethfowler.org/blog/2013/09/13/new-in-firefox-26-css-image-orientation .)

Il existe également un bug pour implémenter cela dans Chrome .

Attention, cette propriété n'est prise en charge que par Firefox et est susceptible d'être obsolète .

Sam Dutton
la source
5
Merci pour le lien vers le rapport de bogue. Je l'ai mis en vedette pour que l'équipe Chrome sache que plus de gens le souhaitent.
DemiImp
Selon ce commentaire bugs.chromium.org/p/chromium/issues/detail?id=158753#c104 par un membre du projet Chromium: "Le changement est dans Chrome 81. Cela sera déployé au public en tant que version stable dans 8 -10 semaine "
jeff forest le
1
Implémenté sur Chrome à partir de 81 🎉 Cela prendra un certain temps avant que les gens mettent à jour leur navigateur - gardez un œil sur caniuse
Robin Métral
4

Si vous le souhaitez sur plusieurs navigateurs, le mieux est de le faire sur le serveur. Vous pourriez avoir une API qui prend une URL de fichier et vous renvoie les données EXIF; PHP a un module pour cela .

Cela pourrait être fait en utilisant Ajax afin que ce soit transparent pour l'utilisateur. Si vous ne vous souciez pas de la compatibilité entre navigateurs et que vous pouvez compter sur la fonctionnalité de fichier HTML5 , regardez dans la bibliothèque JsJPEGmeta qui vous permettra d'obtenir ces données en JavaScript natif.

Alex Turpin
la source
21
@MikkoOhtamaa: Vous devez comprendre que Stack Overflow répond aux questions de tout le monde , juste la personne d'origine qui le pose. La prochaine personne qui a le même objectif que vous est peut-être un développeur PHP - pourquoi voudriez-vous leur refuser les informations que Xeon06 a incluses? Il était inapproprié de modifier cela, simplement parce que vous ne voulez pas d'une solution PHP.
Jon Skeet
5
La question dit "en Javascript" donc la partie n'était pas pertinente. Il existe déjà de nombreuses autres questions et réponses similaires pour PHP sur le site et il y a du bruit inutile concernant cette question.
Mikko Ohtamaa
2
Si les gens demandent une solution Javascript, ils ne veulent pas voir la solution PHP comme premier message.
Mikko Ohtamaa
1
@MikkoOhtamaa, il semblerait que la plupart ne soient pas d'accord avec vous meta.stackexchange.com/questions/157338/... Vous semblez avoir un sentiment de propriété erroné sur les réponses à vos questions.
Alex Turpin
1
J'ai modifié la réponse pour avoir la bonne réponse au début. Désolé pour le fuzz.
Mikko Ohtamaa
3

Découvrez un module que j'ai écrit (vous pouvez l'utiliser dans le navigateur) qui convertit l'orientation exif en transformation CSS: https://github.com/Sobesednik/exif2css

Il existe également ce programme de nœuds pour générer des fixtures JPEG avec toutes les orientations: https://github.com/Sobesednik/generate-exif-fixtures

zavr
la source
1
Beau module! Cependant, comment obtient-il des informations EXIF ​​à partir de JPEG en premier lieu?
Mikko Ohtamaa
@MikkoOhtamaa merci et non, vous devez le faire avec exif-js ou exiftool côté serveur
zavr
C'est utile. Mais il me semble que cela ne fonctionne correctement que pour les photos de portrait, pas celles de paysage.
Sridhar Sarnobat
3

Je télécharge le code d'extension pour montrer la photo par appareil photo Android sur html comme d'habitude sur une balise img avec rotation droite, en particulier pour la balise img dont la largeur est plus large que la hauteur. Je sais que ce code est moche mais vous n'avez pas besoin d'installer d'autres packages. (J'ai utilisé le code ci-dessus pour obtenir la valeur de rotation exif, merci.)

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

puis utilisez comme

$("#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
la source
2

Améliorer / ajouter plus de fonctionnalités à la réponse d'Ali précédemment, j'ai créé une méthode util dans Typescript qui répondait à mes besoins pour ce problème. Cette version renvoie la rotation en degrés dont vous pourriez également avoir besoin pour votre projet.

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

Usage:

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