Mengakses data rotasi JPEG EXIF ​​dalam JavaScript di sisi klien

125

Saya ingin memutar foto berdasarkan rotasi aslinya, seperti yang diatur oleh kamera dalam data gambar JPEG EXIF. Triknya adalah semua ini harus terjadi di browser, menggunakan JavaScript dan <canvas>.

Bagaimana JavaScript dapat mengakses JPEG, objek API file lokal, lokal <img>atau jarak jauh <img>, data EXIF ​​untuk membaca informasi rotasi?

Jawaban dari sisi server tidak OK; Saya mencari solusi sisi klien .

Mikko Ohtamaa
sumber

Jawaban:

261

Jika Anda hanya ingin tag orientasi dan tidak ada yang lain dan tidak suka menyertakan pustaka javascript besar lainnya, saya menulis kode kecil yang mengekstrak tag orientasi secepat mungkin (Menggunakan DataView dan readAsArrayBuffertersedia di IE10 +, tetapi Anda dapat menulis pembaca data Anda sendiri untuk browser lama):

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

nilai:

-2: not jpeg
-1: not defined

masukkan deskripsi gambar di sini

Bagi mereka yang menggunakan Ketikan, Anda dapat menggunakan kode berikut:

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
sumber
untuk 2,4,5,7 untuk mendapatkan gambar yang benar, Anda perlu memutar dan membalik, bukan?
Muhammad Umer
Orientasi gambar saya adalah 3..Bagaimana cara mengatur orientasi ke 1 ??
Lucy
3
@Mick PNG atau GIF tidak memiliki format standar untuk menyimpan orientasi gambar stackoverflow.com/questions/9542359/…
Ali
2
Bekerja untuk saya, tetapi saya perlu mengubah baris terakhir menjadi hanya reader.readAsArrayBuffer (file); tanpa potongan karena saya bermaksud menggunakan buffer untuk gambar base64 saya, jika tidak, Anda hanya akan melihat potongan pertama gambar. BTW, ini tidak diperlukan jika Anda hanya membutuhkan informasi orientasi. Terima kasih
Philip Murphy
2
@DaraJava Saya menghapus bagian slice karena terkadang tag masuk setelah batas, tetapi akan memperlambat operasi jika tag tidak pernah ditemukan. Bagaimanapun, tidak seperti tag orientasi, tag Flash tidak ada di direktori IFD0 dan kode saya hanya mencari bagian ini. untuk mendapatkan tag Flash Anda harus mencari direktori SubIFD. Anda dapat menemukan tutorial yang bagus tentang EXIF ​​di sini: media.mit.edu/pia/Research/deepview/exif.html
Ali
22

Anda dapat menggunakan pustaka exif-js yang dikombinasikan dengan API File 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.dll
sumber
Terima kasih. JS lib dalam pertanyaan tersebut terlihat agak ketinggalan jaman, tetapi mungkin akan berfungsi.
Mikko Ohtamaa
Lihat juga demo saya tentang widget unggah file yang baru saja saya tulis. Ini menggunakan pustaka EXIF.js yang disebutkan di atas untuk membaca bendera orientasi EXIF ​​di metatdata file gambar. Berdasarkan informasi, itu menerapkan rotasi menggunakan elemen kanvas ... sandbox.juurlink.org/html5imageuploader
Rob Juurlink
Mencoba untuk bahkan memasukkan binaryajax.js dalam proyek saya menyebabkan kesalahan akses ditolak.
Obi Wan
Dari mana asal objek EXIF? Skrip BinaryFile sepertinya tidak memuatnya, dan sejauh yang saya tahu, ini bukan bagian dari jquery atau skrip lain yang biasa saya gunakan ...
jrista
6
Situs web perpustakaan tampak tidak aktif, dan satu-satunya perpustakaan ExifReader lain yang saya temukan terbatas dalam dukungan browser. Apakah ada alternatif yang bagus?
Praxis Ashelin
19

Firefox 26 mendukung image-orientation: from-image: gambar ditampilkan potret atau lanskap, tergantung pada data EXIF. (Lihat sethfowler.org/blog/2013/09/13/new-in-firefox-26-css-image-orientation .)

Ada juga bug untuk menerapkan ini di Chrome .

Berhati-hatilah karena properti ini hanya didukung oleh Firefox dan kemungkinan besar tidak akan digunakan lagi .

Sam Dutton
sumber
5
Terima kasih untuk link ke laporan bug. Saya mengilaukan bintang agar tim Chrome tahu lebih banyak orang menginginkan ini.
DemiImp
Menurut komentar ini bugs.chromium.org/p/chromium/issues/detail?id=158753#c104 oleh anggota proyek Chromium: "Perubahan ada di Chrome 81. Itu akan diluncurkan ke publik sebagai versi Stabil di 8 -10 waktu minggu "
jeff forest
1
Diterapkan di Chrome mulai dari 81 🎉 Akan butuh waktu lama sebelum orang-orang memperbarui browser mereka - perhatikan caniuse
Robin Métral
4

Jika Anda menginginkannya lintas browser, taruhan terbaik Anda adalah melakukannya di server. Anda bisa memiliki API yang mengambil URL file dan mengembalikan Anda data EXIF; PHP memiliki modul untuk itu .

Ini bisa dilakukan menggunakan Ajax sehingga akan mulus bagi pengguna. Jika Anda tidak peduli tentang kompatibilitas lintas-browser, dan dapat mengandalkan fungsionalitas file HTML5 , lihat perpustakaan JsJPEGmeta yang memungkinkan Anda mendapatkan data itu dalam JavaScript asli.

Alex Turpin
sumber
21
@MikkoOhtamaa: Anda perlu memahami bahwa Stack Overflow menjawab pertanyaan untuk semua orang , hanya orang asli yang menanyakannya. Orang berikutnya yang memiliki tujuan yang sama seperti Anda mungkin adalah pengembang PHP - mengapa Anda ingin menolak informasi yang disertakan Xeon06? Tidak pantas untuk mengeditnya, hanya karena Anda tidak menginginkan solusi PHP.
Jon Skeet
5
Pertanyaannya mengatakan "dalam Javascript" jadi bagian itu tidak relevan. Ada banyak pertanyaan dan jawaban serupa lainnya untuk PHP yang sudah ada di situs dan tidak perlu ribut-ribut terkait pertanyaan ini.
Mikko Ohtamaa
2
Jika orang-orang meminta solusi Javascript, mereka tidak ingin melihat solusi PHP sebagai postingan pertama.
Mikko Ohtamaa
1
@MikkoOhtamaa sepertinya sebagian besar tidak setuju dengan Anda meta.stackexchange.com/questions/157338/… Anda tampaknya memiliki rasa kepemilikan yang salah atas jawaban atas pertanyaan Anda.
Alex Turpin
1
Saya mengedit jawaban untuk mendapatkan jawaban yang benar di awal. Maaf untuk fuzznya.
Mikko Ohtamaa
3

Lihat modul yang saya tulis (Anda dapat menggunakannya di browser) yang mengubah orientasi exif menjadi transformasi CSS: https://github.com/Sobesednik/exif2css

Ada juga program node ini untuk menghasilkan perlengkapan JPEG dengan semua orientasi: https://github.com/Sobesednik/generate-exif-fixtures

zavr
sumber
1
Modul yang bagus! Namun, bagaimana cara mendapatkan informasi EXIF ​​dari JPEG?
Mikko Ohtamaa
@MikkoOhtamaa terima kasih dan nah tidak, Anda harus melakukannya dengan exif-js atau exiftool server-side
zavr
Ini berguna. Tapi menurut saya ini hanya berfungsi dengan benar untuk foto potret, bukan foto lanskap.
Sridhar Sarnobat
3

Kode ekspansi saya unggah untuk menampilkan foto dengan kamera android pada html seperti biasa pada beberapa tag img dengan rotasi yang tepat, terutama untuk tag img yang lebarnya lebih lebar dari tinggi. Saya tahu kode ini jelek tetapi Anda tidak perlu menginstal paket lain. (Saya menggunakan kode di atas untuk mendapatkan nilai rotasi exif, Terima kasih.)

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

dan kemudian gunakan seperti

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

Meningkatkan / Menambahkan lebih banyak fungsionalitas ke jawaban Ali dari sebelumnya, saya membuat metode util di Typecript yang sesuai dengan kebutuhan saya untuk masalah ini. Versi ini mengembalikan rotasi dalam derajat yang mungkin juga Anda perlukan untuk proyek Anda.

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

Pemakaian:

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