การเข้าถึงข้อมูลการหมุน JPEG EXIF ​​ใน JavaScript ทางฝั่งไคลเอ็นต์


126

ฉันต้องการหมุนรูปภาพตามการหมุนเดิมตามที่กล้องกำหนดในข้อมูลภาพ JPEG EXIF เคล็ดลับคือทั้งหมดนี้ควรเกิดขึ้นในเบราว์เซอร์โดยใช้ JavaScript และ<canvas>.

JavaScript สามารถเข้าถึง JPEG ซึ่งเป็นอ็อบเจ็กต์ API ไฟล์ในเครื่องข้อมูลภายใน<img>หรือระยะไกล<img>EXIF เพื่ออ่านข้อมูลการหมุนได้อย่างไร

คำตอบฝั่งเซิร์ฟเวอร์ไม่ตกลง ฉันกำลังมองหาโซลูชันฝั่งไคลเอ็นต์

คำตอบ:


262

หากคุณต้องการเพียงแท็กการวางแนวและไม่มีอะไรอื่นและไม่ต้องการรวมไลบรารี javascript ขนาดใหญ่อื่นฉันเขียนโค้ดเล็ก ๆ น้อย ๆ ที่แยกแท็กการวางแนวให้เร็วที่สุด (ใช้ DataView และreadAsArrayBufferมีอยู่ใน IE10 + แต่คุณสามารถเขียนได้ โปรแกรมอ่านข้อมูลของคุณเองสำหรับเบราว์เซอร์รุ่นเก่า):

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

ค่า:

-2: not jpeg
-1: not defined

ใส่คำอธิบายภาพที่นี่

สำหรับผู้ที่ใช้ typescript คุณสามารถใช้รหัสต่อไปนี้:

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

สำหรับ 2,4,5,7 เพื่อให้ได้ภาพที่ถูกต้องคุณต้องหมุนและพลิกใช่ไหม?
Muhammad Umer

การวางแนวภาพของฉันคือ 3 .. ฉันจะตั้งค่าการวางแนวเป็น 1 ได้อย่างไร ??
Lucy

3
@Mick PNG หรือ GIF ไม่มีรูปแบบมาตรฐานในการจัดเก็บการวางแนวภาพstackoverflow.com/questions/9542359/…
Ali

2
ทำงานให้ฉัน แต่ฉันต้องการเปลี่ยนบรรทัดสุดท้ายเป็นเพียง reader.readAsArrayBuffer (ไฟล์); โดยไม่มีชิ้นส่วนเนื่องจากฉันตั้งใจจะใช้บัฟเฟอร์สำหรับภาพ base64 ของฉันมิฉะนั้นคุณจะเห็นชิ้นส่วนแรกของภาพ BTW ไม่จำเป็นหากคุณต้องการข้อมูลการวางแนว ขอบคุณ
Philip Murphy

2
@DaraJava ฉันลบส่วน slice ออกเพราะบางครั้งแท็กเข้ามาหลังขีด จำกัด แต่จะทำให้การดำเนินการช้าลงหากไม่พบแท็ก อย่างไรก็ตามไม่เหมือนกับแท็กการวางแนวแท็ก Flash ไม่ได้อยู่ในไดเร็กทอรี IFD0 และโค้ดของฉันค้นหาเฉพาะส่วนนี้ เพื่อรับแท็ก Flash คุณต้องค้นหาไดเรกทอรี SubIFD คุณสามารถดูบทแนะนำดีๆเกี่ยวกับ EXIF ​​ได้ที่นี่: media.mit.edu/pia/Research/deepview/exif.html
Ali

22

คุณสามารถใช้EXIF-jsห้องสมุดร่วมกับ HTML5 ไฟล์ API: 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
});

ขอบคุณ JS lib ในคำถามดูล้าสมัยไปหน่อย แต่ก็น่าจะใช้ได้
Mikko Ohtamaa

ดูการสาธิตวิดเจ็ตการอัปโหลดไฟล์ที่ฉันเพิ่งเขียน ใช้ไลบรารี EXIF.js ที่กล่าวถึงข้างต้นเพื่ออ่านค่าสถานะการวางแนว EXIF ​​ในข้อมูลเมตาของไฟล์รูปภาพ จากข้อมูลจะใช้การหมุนโดยใช้องค์ประกอบผ้าใบ ... sandbox.juurlink.org/html5imageuploader
Rob Juurlink

การพยายามรวม binaryajax.js ในโปรเจ็กต์ของฉันทำให้เกิดข้อผิดพลาดการเข้าถึงถูกปฏิเสธ
โอบีวัน

วัตถุ EXIF ​​มาจากไหน? ดูเหมือนว่าสคริปต์ BinaryFile จะไม่มีมันและเท่าที่ฉันบอกได้มันไม่ได้เป็นส่วนหนึ่งของ jquery หรือสคริปต์อื่น ๆ ที่ฉันใช้เป็นประจำ ...
jrista

6
ดูเหมือนว่าเว็บไซต์ไลบรารีจะหยุดทำงานและไลบรารี ExifReader อื่น ๆ ที่ฉันพบมีข้อ จำกัด ในการรองรับเบราว์เซอร์ มีทางเลือกอื่นที่ดีหรือไม่?
Praxis Ashelin

19

Firefox 26 รองรับimage-orientation: from-image: รูปภาพจะแสดงในแนวตั้งหรือแนวนอนขึ้นอยู่กับข้อมูล EXIF (ดูsethfowler.org/blog/2013/09/13/new-in-firefox-26-css-image-orientation )

นอกจากนี้ยังมีข้อผิดพลาดในการดำเนินการนี้ใน Chrome

ระวังว่าคุณสมบัตินี้ได้รับการสนับสนุนโดยเฉพาะ Firefox และเป็นแนวโน้มที่จะถูกเลิกใช้


5
ขอบคุณสำหรับลิงก์ไปยังรายงานข้อบกพร่อง ฉันติดดาวเพื่อให้ทีม Chrome รู้ว่ามีคนต้องการสิ่งนี้มากขึ้น
DemiImp

ตามความคิดเห็นนี้bugs.chromium.org/p/chromium/issues/detail?id=158753#c104โดยสมาชิกโครงการ Chromium: "การเปลี่ยนแปลงอยู่ใน Chrome 81 ที่จะเปิดตัวสู่สาธารณะในรูปแบบเสถียรใน 8 -10 สัปดาห์ "
ฟฟอเรสต์

1
ติดตั้งบน Chrome ตั้งแต่วันที่ 81 🎉จะใช้เวลาสักครู่ก่อนที่ผู้คนจะอัปเดตเบราว์เซอร์ของตน - จับตาดูcaniuse
Robin Métral

11

https://github.com/blueimp/JavaScript-Load-Imageเป็นไลบรารีจาวาสคริปต์สมัยใหม่ที่ไม่เพียง แต่สามารถแยกแฟล็กการวางแนว exif เท่านั้น แต่ยังสามารถสะท้อน / หมุนภาพ JPEG ที่ฝั่งไคลเอ็นต์ได้อย่างถูกต้อง

ฉันเพิ่งแก้ไขปัญหาเดียวกันกับไลบรารีนี้: JS Client-Side Exif Orientation: Rotate and Mirror JPEG Images


4

หากคุณต้องการใช้งานข้ามเบราว์เซอร์ทางออกที่ดีที่สุดของคุณคือทำบนเซิร์ฟเวอร์ คุณสามารถมี API ที่ใช้ URL ของไฟล์และส่งคืนข้อมูล EXIF ​​ให้คุณ PHP มีโมดูลสำหรับสิ่งนั้น

ซึ่งสามารถทำได้โดยใช้Ajaxเพื่อให้ผู้ใช้งานราบรื่น หากคุณไม่สนใจเกี่ยวกับความเข้ากันได้ของข้ามเบราว์เซอร์และสามารถพึ่งพาการทำงานของไฟล์HTML5 ได้ให้ดูที่ไลบรารีJsJPEGmetaที่จะช่วยให้คุณได้รับข้อมูลนั้นใน JavaScript ดั้งเดิม


21
@MikkoOhtamaa: คุณต้องเข้าใจว่า Stack Overflow ตอบคำถามสำหรับทุกคนเพียงแค่คนเดิมถามเท่านั้น บุคคลต่อไปที่มีวัตถุประสงค์เดียวกันกับคุณอาจเป็นนักพัฒนา PHP - ทำไมคุณถึงต้องการปฏิเสธข้อมูลที่ Xeon06 รวมอยู่ด้วย? การแก้ไขนั้นไม่เหมาะสมเพียงเพราะคุณไม่ต้องการโซลูชัน PHP
Jon Skeet

5
คำถามระบุว่า "ใน Javascript" ดังนั้นส่วนที่ไม่เกี่ยวข้อง มีคำถามและคำตอบอื่น ๆ ที่คล้ายกันสำหรับ PHP อยู่แล้วบนไซต์และเป็นเสียงรบกวนที่ไม่จำเป็นสำหรับคำถามนี้
Mikko Ohtamaa

2
หากมีคนถามหาโซลูชัน Javascript พวกเขาไม่ต้องการเห็นโซลูชัน PHP เป็นโพสต์แรก
Mikko Ohtamaa

1
@MikkoOhtamaa ดูเหมือนจะชอบมากที่สุดเห็นด้วยกับคุณmeta.stackexchange.com/questions/157338/... คุณดูเหมือนจะมีความรู้สึกบางอย่างผิดพลาดในการเป็นเจ้าของในการตอบคำถามของคุณ
Alex Turpin

1
ฉันแก้ไขคำตอบให้มีคำตอบที่ถูกต้องในตอนต้น ขออภัยในความไม่แน่นอน
Mikko Ohtamaa

3

ตรวจสอบโมดูลที่ฉันเขียน (คุณสามารถใช้ในเบราว์เซอร์) ซึ่งแปลงการวางแนว exif เป็นการแปลง CSS: https://github.com/Sobesednik/exif2css

นอกจากนี้ยังมีโปรแกรมโหนดนี้เพื่อสร้างการติดตั้ง JPEG ที่มีทิศทางทั้งหมด: https://github.com/Sobesednik/generate-exif-fixtures


1
โมดูลดี! อย่างไรก็ตามจะดึงข้อมูล EXIF ​​ออกจาก JPEG ได้อย่างไรในตอนแรก?
Mikko Ohtamaa

@MikkoOhtamaa ขอบคุณและไม่เป็นเช่นนั้นคุณต้องทำกับ exif-js หรือ exiftool ฝั่งเซิร์ฟเวอร์
zavr

สิ่งนี้มีประโยชน์ แต่สำหรับฉันแล้วดูเหมือนว่ามันใช้ได้กับภาพถ่ายแนวตั้งเท่านั้นไม่ใช่แนวนอน
Sridhar Sarnobat

3

ฉันอัปโหลดโค้ดส่วนขยายเพื่อแสดงภาพถ่ายด้วยกล้อง Android บน html ตามปกติในแท็ก img บางแท็กที่มีการหมุนขวาโดยเฉพาะแท็ก img ที่มีความกว้างมากกว่าความสูง ฉันรู้ว่ารหัสนี้น่าเกลียด แต่คุณไม่จำเป็นต้องติดตั้งแพ็คเกจอื่น ๆ (ฉันใช้รหัสด้านบนเพื่อรับค่าการหมุน exif ขอบคุณ)

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

แล้วใช้เช่น

$("#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]);

});

2

การปรับปรุง / เพิ่มฟังก์ชันการทำงานเพิ่มเติมสำหรับคำตอบของ Ali จากก่อนหน้านี้ฉันได้สร้างวิธีการใช้งานใน typescript ที่ตรงกับความต้องการของฉันสำหรับปัญหานี้ เวอร์ชันนี้ส่งคืนการหมุนในองศาที่คุณอาจต้องการสำหรับโครงการของคุณ

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

การใช้งาน:

import { getOrientation } from './ImageUtils';
...
onDrop = (pics: any) => {
  getOrientation(pics[0], rotationInDegrees => {
    this.setState({ image: pics[0], rotate: rotationInDegrees });
  });
};
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.