Truy cập dữ liệu xoay JPEG EXIF ​​bằng JavaScript ở phía máy khách


125

Tôi muốn xoay ảnh dựa trên góc quay ban đầu của chúng, như được đặt bởi máy ảnh trong dữ liệu hình ảnh JPEG EXIF. Mẹo ở đây là tất cả những điều này sẽ xảy ra trong trình duyệt, sử dụng JavaScript và <canvas>.

Làm cách nào JavaScript có thể truy cập JPEG, một đối tượng API tệp cục bộ, cục bộ <img>hoặc từ xa <img>, dữ liệu EXIF ​​để đọc thông tin xoay vòng?

Câu trả lời phía máy chủ không ổn; Tôi đang tìm kiếm một giải pháp phía khách hàng .

Câu trả lời:


261

Nếu bạn chỉ muốn thẻ định hướng và không có gì khác và không muốn bao gồm một thư viện javascript lớn khác, tôi đã viết một đoạn mã trích xuất thẻ định hướng càng nhanh càng tốt (Nó sử dụng DataView và readAsArrayBuffercó sẵn trong IE10 +, nhưng bạn có thể viết trình đọc dữ liệu của riêng bạn cho các trình duyệt cũ hơn):

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

các giá trị:

-2: not jpeg
-1: not defined

nhập mô tả hình ảnh ở đây

Đối với những người sử dụng Bản mô tả, bạn có thể sử dụng mã sau đây:

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

cho 2,4,5,7 để có được hình ảnh chính xác, bạn cần phải xoay và lật, phải không?
Muhammad Umer

Hướng của hình ảnh của tôi là 3. Làm thế nào để tôi đặt hướng thành 1 ??
Lucy

3
@Mick PNG hoặc GIF không có bất kỳ định dạng tiêu chuẩn nào để lưu trữ stackoverflow.com/questions/9542359/
Ali

2
Làm việc cho tôi, nhưng tôi cần thay đổi dòng cuối cùng thành reader.readAsArrayBuffer (file); không có lát cắt như tôi dự định sử dụng bộ đệm cho hình ảnh base64 của mình, nếu không, bạn sẽ chỉ nhìn thấy lát đầu tiên của hình ảnh. BTW, điều này là không bắt buộc nếu bạn chỉ cần thông tin định hướng. Cảm ơn
Philip Murphy

2
@DaraJava Tôi đã xóa phần lát cắt vì đôi khi thẻ xuất hiện sau giới hạn, nhưng nó sẽ làm chậm hoạt động nếu không tìm thấy thẻ. Dù sao, không giống như thẻ định hướng, thẻ Flash không có trong thư mục IFD0 và mã của tôi chỉ tìm kiếm phần này. để lấy thẻ Flash, bạn phải tìm kiếm thư mục SubIFD. Bạn có thể tìm thấy một hướng dẫn tốt về EXIF ​​tại đây: media.mit.edu/pia/Research/deepview/exif.html
Ali

22

Bạn có thể sử dụng thư viện exif-js kết hợp với API tệp 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
});

Cảm ơn. Các lib lib trong câu hỏi có vẻ hơi lỗi thời, nhưng có lẽ sẽ hoạt động.
Mikko Ohtamaa

Xem thêm bản demo của tôi về một widget tải lên tập tin tôi vừa viết. Nó sử dụng thư viện EXIF.js được đề cập ở trên để đọc cờ định hướng EXIF ​​trong siêu dữ liệu của tệp hình ảnh. Dựa trên thông tin, nó áp dụng xoay vòng bằng cách sử dụng phần tử canvas ... sandbox.juurlink.org/html5imageuploader
Rob Juurlink

Việc cố gắng thậm chí bao gồm binaryajax.js trong dự án của tôi gây ra lỗi từ chối truy cập.
Obi Wan

Đối tượng EXIF ​​đến từ đâu? Kịch bản BinaryFile dường như không chứa nó, và theo như tôi có thể nói, nó không phải là một phần của jquery hay bất kỳ kịch bản nào khác mà tôi thường sử dụng ...
jrista

6
Trang web thư viện dường như không hoạt động và các thư viện ExifReader khác mà tôi tìm thấy bị giới hạn trong hỗ trợ trình duyệt. Có sự thay thế tốt nào không?
Praxis Ashelin

19

Hỗ trợ Firefox 26 image-orientation: from-image: hình ảnh được hiển thị dọc hoặc ngang, tùy thuộc vào dữ liệu EXIF. (Xem sethfowler.org/blog/2013/09/13/new-in-firefox-26-css-image-orientation .)

Ngoài ra còn có một lỗi để thực hiện điều này trong Chrome .

Coi chừng tài sản này chỉ được Firefox hỗ trợ và có khả năng không được dùng nữa .


5
Cảm ơn các liên kết đến báo cáo lỗi. Tôi đã gắn dấu sao để nhóm Chrome biết nhiều người muốn điều này hơn.
DemiImp

Theo nhận xét này bug.chromium.org/p/chromium/issues/detail?id=158753#c104 bởi một thành viên dự án Chromium: "Thay đổi này nằm trong Chrome 81. Điều đó sẽ được đưa ra công khai dưới dạng Phiên bản ổn định trong 8 -10 tuần "
rừng jeff

1
Được triển khai trên Chrome bắt đầu với 81 Sẽ mất một lúc trước khi mọi người cập nhật trình duyệt của họ - hãy để mắt tới caniuse
Robin Métral


4

Nếu bạn muốn nó đa trình duyệt, cách tốt nhất của bạn là làm điều đó trên máy chủ. Bạn có thể có một API lấy URL tệp và trả về cho bạn dữ liệu EXIF; PHP có một mô-đun cho điều đó .

Điều này có thể được thực hiện bằng cách sử dụng Ajax để nó sẽ liền mạch với người dùng. Nếu bạn không quan tâm đến khả năng tương thích giữa nhiều trình duyệt và có thể dựa vào chức năng tệp HTML5 , hãy xem thư viện JsJPEGmeta sẽ cho phép bạn lấy dữ liệu đó bằng JavaScript gốc.


21
@MikkoOhtamaa: Bạn cần hiểu rằng Stack Overflow trả lời các câu hỏi cho mọi người , chỉ là người ban đầu hỏi nó. Người tiếp theo có cùng mục tiêu với bạn có thể là nhà phát triển PHP - tại sao bạn muốn từ chối họ thông tin mà Xeon06 đưa vào? Việc chỉnh sửa nó là không phù hợp, chỉ vì bạn không muốn có giải pháp PHP.
Jon Skeet

5
Câu hỏi cho biết "trong Javascript" vì vậy phần này không liên quan. Có rất nhiều câu hỏi và câu trả lời tương tự khác cho PHP đã có trên trang web và đó là tiếng ồn không cần thiết liên quan đến câu hỏi này.
Mikko Ohtamaa

2
Nếu mọi người yêu cầu giải pháp Javascript, họ không muốn xem giải pháp PHP là bài đăng đầu tiên.
Mikko Ohtamaa

1
@MikkoOhtamaa có vẻ như hầu hết không đồng ý với bạn meta.stackexchange.com/questions/157338/ trộm Bạn dường như có ý thức sai về quyền sở hữu đối với câu trả lời cho câu hỏi của bạn.
Alex Turpin

1
Tôi đã chỉnh sửa câu trả lời để có câu trả lời đúng ngay từ đầu. Xin lỗi cho fuzz.
Mikko Ohtamaa

3

Kiểm tra một mô-đun tôi đã viết (bạn có thể sử dụng nó trong trình duyệt) để chuyển đổi định hướng exif thành chuyển đổi CSS: https://github.com/Sobesednik/exif2css

Ngoài ra còn có chương trình nút này để tạo đồ đạc JPEG với tất cả các định hướng: https://github.com/Sobesednik/generate-exif-fixutions


1
Mô-đun đẹp! Tuy nhiên, làm thế nào để nó lấy thông tin EXIF ​​từ JPEG ngay từ đầu?
Mikko Ohtamaa

@MikkoOhtamaa cảm ơn và không, bạn phải làm điều đó với exif-js hoặc
exiftool

Điều này rất hữu ích. Nhưng có vẻ như tôi chỉ hoạt động chính xác cho ảnh chân dung, không phải ảnh phong cảnh.
Sridhar Sarnobat

3

Tôi tải lên mã mở rộng để hiển thị ảnh bằng máy ảnh Android trên html như bình thường trên một số thẻ img với giới hạn đúng, đặc biệt đối với thẻ img có chiều rộng rộng hơn chiều cao. Tôi biết mã này là xấu nhưng bạn không cần phải cài đặt bất kỳ gói nào khác. (Tôi đã sử dụng mã ở trên để có được giá trị xoay exif, Cảm ơn bạn.)

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

và sau đó sử dụng như

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

Cải thiện / Thêm nhiều chức năng hơn cho câu trả lời của Ali từ trước đó, tôi đã tạo ra một phương thức sử dụng trong Bản mô tả phù hợp với nhu cầu của tôi cho vấn đề này. Phiên bản này trả về xoay vòng theo độ mà bạn cũng có thể cần cho dự án của mình.

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

Sử dụng:

import { getOrientation } from './ImageUtils';
...
onDrop = (pics: any) => {
  getOrientation(pics[0], rotationInDegrees => {
    this.setState({ image: pics[0], rotate: rotationInDegrees });
  });
};
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.