Dostęp do danych rotacji JPEG EXIF ​​w JavaScript po stronie klienta

125

Chciałbym obracać zdjęcia na podstawie ich oryginalnego obrotu, ustawionego przez aparat w danych obrazu JPEG EXIF. Rzecz w tym, że wszystko to powinno się odbywać w przeglądarce, używając JavaScript i <canvas>.

W jaki sposób JavaScript może uzyskać dostęp do JPEG, obiektu API pliku lokalnego, lokalnego <img>lub zdalnego <img>, danych EXIF, aby odczytać informacje o rotacji?

Odpowiedzi po stronie serwera nie są poprawne; Szukam rozwiązania po stronie klienta .

Mikko Ohtamaa
źródło

Odpowiedzi:

261

Jeśli chcesz tylko znacznika orientacji i nic więcej i nie lubisz dołączać kolejnej ogromnej biblioteki javascript, napisałem mały kod, który wyodrębnia znacznik orientacji tak szybko, jak to możliwe (używa DataView i readAsArrayBufferktóre są dostępne w IE10 +, ale możesz pisać własny czytnik danych dla starszych przeglądarek):

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

wartości:

-2: not jpeg
-1: not defined

wprowadź opis obrazu tutaj

Dla tych, którzy używają Typescript, możesz użyć następującego kodu:

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
źródło
dla 2,4,5,7 aby uzyskać poprawny obraz trzeba obracać i odwracać, prawda?
Muhammad Umer
Orientacja mojego obrazu to 3. Jak ustawić orientację na 1?
Lucy
3
@Mick PNG lub GIF nie mają żadnego standardowego formatu do przechowywania orientacji obrazu stackoverflow.com/questions/9542359/ ...
Ali
2
Pracowałem dla mnie, ale musiałem zmienić ostatnią linię na po prostu reader.readAsArrayBuffer (plik); bez plasterka, ponieważ zamierzam użyć bufora dla mojego obrazu base64, w przeciwnym razie zobaczysz tylko pierwszy wycinek obrazu. Przy okazji, nie jest to wymagane, jeśli potrzebujesz tylko informacji o orientacji. Dzięki
Philip Murphy,
2
@DaraJava Usunąłem część wycinka, ponieważ czasami tag pojawiał się po limicie, ale spowolni to działanie, jeśli tag nigdy nie zostanie znaleziony. W każdym razie, w przeciwieństwie do tagu orientacji, tagu Flash nie ma w katalogu IFD0, a mój kod przeszukuje tylko tę część. aby pobrać tag Flash, musisz przeszukać katalog SubIFD. Dobry samouczek na temat EXIF ​​można znaleźć tutaj: media.mit.edu/pia/Research/deepview/exif.html
Ali
22

Możesz użyć biblioteki exif-js w połączeniu z interfejsem API plików 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
źródło
Dzięki. Biblioteka JS w pytaniu wygląda na nieco przestarzałą, ale prawdopodobnie zadziała.
Mikko Ohtamaa
Zobacz także moje demo widżetu do przesyłania plików, który właśnie napisałem. Używa wspomnianej powyżej biblioteki EXIF.js do odczytywania flagi orientacji EXIF ​​w metatanych pliku obrazu. Na podstawie tych informacji stosuje rotację za pomocą elementu canvas ... sandbox.juurlink.org/html5imageuploader
Rob Juurlink
Próba nawet włączenia binaryajax.js do mojego projektu powoduje błąd odmowy dostępu.
Obi Wan
Skąd pochodzi obiekt EXIF? Wydaje się, że skrypt BinaryFile go nie zawiera i, o ile wiem, nie jest częścią jquery ani żadnego innego skryptu, którego regularnie używam ...
jrista
6
Wydaje się, że witryna biblioteki nie działa, a jedyne inne biblioteki ExifReader, które znalazłem, miały ograniczoną obsługę przeglądarki. Czy jest jakaś dobra alternatywa?
Praxis Ashelin
19

Firefox 26 obsługuje image-orientation: from-image: obrazy są wyświetlane pionowo lub poziomo, w zależności od danych EXIF. (Zobacz sethfowler.org/blog/2013/09/13/new-in-firefox-26-css-image-orientation .)

Istnieje również błąd umożliwiający zaimplementowanie tego w Chrome .

Pamiętaj, że ta właściwość jest obsługiwana tylko przez przeglądarkę Firefox i prawdopodobnie zostanie wycofana .

Sam Dutton
źródło
5
Dzięki za link do raportu o błędzie. Oznaczam to gwiazdką, aby zespół Chrome wiedział, że więcej osób tego chce.
DemiImp
Zgodnie z tym komentarzem bugs.chromium.org/p/chromium/issues/detail?id=158753#c104 autorstwa członka projektu Chromium: „Zmiana dotyczy Chrome 81. Zostanie ona udostępniona publicznie jako wersja stabilna w 8 -10 tygodnia ”
jeff forest
1
Wdrożone w Chrome od 81 🎉 Jednak minie trochę czasu, zanim użytkownicy zaktualizują swoją przeglądarkę - miej
Robin Métral
4

Jeśli chcesz, aby działała w różnych przeglądarkach, najlepiej jest zrobić to na serwerze. Możesz mieć interfejs API, który pobiera adres URL pliku i zwraca dane EXIF; PHP ma do tego moduł .

Można to zrobić za pomocą Ajax, aby było to bezproblemowe dla użytkownika. Jeśli nie zależy Ci na kompatybilności z różnymi przeglądarkami i możesz polegać na funkcjonalności pliku HTML5 , zajrzyj do biblioteki JsJPEGmeta , która pozwoli ci uzyskać te dane w natywnym JavaScript.

Alex Turpin
źródło
21
@MikkoOhtamaa: Musisz zrozumieć, że przepełnienie stosu odpowiada na pytania wszystkich , tylko oryginalnej osoby, która je zadaje. Następna osoba, która ma ten sam cel co Ty, może być programistą PHP - dlaczego miałbyś chcieć odmawiać jej informacji zawartych w Xeon06? Edycja tego była niewłaściwa tylko dlatego , że nie chcesz mieć rozwiązania PHP.
Jon Skeet
5
Pytanie brzmi „w Javascript”, więc ta część była nieistotna. Na stronie jest już wiele innych podobnych pytań i odpowiedzi dla PHP i jest to niepotrzebny hałas w związku z tym pytaniem.
Mikko Ohtamaa
2
Jeśli ludzie proszą o rozwiązanie Javascript, nie chcą widzieć rozwiązania PHP jako pierwszego postu.
Mikko Ohtamaa
1
@MikkoOhtamaa, wygląda na to, że większość się z tobą nie zgadza meta.stackexchange.com/questions/157338/… Wydaje się, że masz niewłaściwe poczucie własności odpowiedzi na swoje pytania.
Alex Turpin,
1
Zredagowałem odpowiedź tak, aby na początku była poprawna. Przepraszam za fuzz.
Mikko Ohtamaa
3

Sprawdź moduł, który napisałem (możesz go używać w przeglądarce), który konwertuje orientację exif na transformację CSS: https://github.com/Sobesednik/exif2css

Istnieje również ten program węzłowy do generowania urządzeń JPEG ze wszystkimi orientacjami: https://github.com/Sobesednik/generate-exif-fixtures

zavr
źródło
1
Niezły moduł! Jednak w jaki sposób w pierwszej kolejności uzyskuje informacje EXIF ​​z JPEG?
Mikko Ohtamaa
@MikkoOhtamaa dzięki i nie, musisz to zrobić z exif-js lub exiftool po stronie serwera
zavr
To jest przydatne. Ale wydaje mi się, że działa poprawnie tylko w przypadku zdjęć portretowych, a nie krajobrazowych.
Sridhar Sarnobat
3

Przesyłam kod rozszerzenia, aby pokazać zdjęcie z aparatu Android w html jak zwykle na jakimś tagu img z prawą rotacją, szczególnie dla tagu img, którego szerokość jest większa niż wysokość. Wiem, że ten kod jest brzydki, ale nie musisz instalować żadnych innych pakietów. (Użyłem powyższego kodu, aby uzyskać wartość rotacji exif, dziękuję.)

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

a następnie użyj takich jak

$("#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
źródło
2

Poprawiając / dodając więcej funkcjonalności do odpowiedzi Ali z wcześniejszej, stworzyłem metodę util w skrypcie Typescript, która odpowiadała moim potrzebom w tym problemie. Ta wersja zwraca obrót w stopniach, który może być również potrzebny w projekcie.

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

Stosowanie:

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