클라이언트 측에서 JavaScript의 JPEG EXIF ​​회전 데이터에 액세스


125

JPEG EXIF ​​이미지 데이터에서 카메라가 설정 한 원래 회전을 기준으로 사진을 회전하고 싶습니다. 트릭은이 모든 것이 브라우저에서 자바 스크립트와<canvas> .

JavaScript는 로컬 파일 API 객체 인 JPEG에 어떻게 액세스 할 수 있습니까? <img> 또는 원격 <img>EXIF 데이터에 액세스하여 회전 정보를 읽을 수 있습니까?

서버 측 답변은 좋지 않습니다. 클라이언트 측 솔루션을 찾고 있습니다.

답변:


261

방향 태그 만 원하고 다른 것은 원치 않고 다른 거대한 자바 스크립트 라이브러리를 포함하고 싶지 않다면 가능한 한 빨리 방향 태그를 추출하는 작은 코드를 작성했습니다 (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/...
알리

2
나를 위해 일했지만 마지막 줄을 reader.readAsArrayBuffer (file);로 변경해야했습니다. base64 이미지에 버퍼를 사용하려는 것처럼 슬라이스 없이도 그렇지 않으면 이미지의 첫 번째 슬라이스 만 표시됩니다. BTW, 오리엔테이션 정보 만 필요한 경우에는 필요하지 않습니다. 감사합니다
필립 머피

2
@DaraJava 가끔 태그가 제한 이후에 들어 왔기 때문에 슬라이스 부분을 제거했지만 태그를 찾을 수 없으면 작업 속도가 느려집니다. 어쨌든 방향 태그와 달리 Flash 태그는 IFD0 디렉토리에 없으며 내 코드는이 부분 만 검색합니다. Flash 태그를 얻으려면 SubIFD 디렉토리를 검색해야합니다. EXIF에 대한 좋은 튜토리얼은 여기에서 찾을 수 있습니다 : media.mit.edu/pia/Research/deepview/exif.html
Ali

22

HTML5 파일 API ( http://jsfiddle.net/xQnMd/1/) 와 함께 exif-js 라이브러리를 사용할 수 있습니다 .

$("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 2011 년

방금 작성한 파일 업로드 위젯의 데모도 참조하십시오. 위에서 언급 한 EXIF.js 라이브러리를 사용하여 이미지 파일의 메타 데이터에서 EXIF ​​방향 플래그를 읽습니다. 정보를 기반으로 캔버스 요소를 사용하여 회전을 적용합니다 ... sandbox.juurlink.org/html5imageuploader
Rob

내 프로젝트에 binaryajax.js를 포함하려고하면 액세스 거부 오류가 발생합니다.
Obi Wan

EXIF 개체의 출처는 어디입니까? BinaryFile 스크립트는 그것을 포함하지 않는 것 같습니다. 그리고 제가 알 수있는 한, jquery 나 제가 정기적으로 사용하는 다른 스크립트의 일부가 아닙니다 ...
jrista

6
라이브러리 웹 사이트가 다운 된 것 같고 내가 찾은 유일한 ExifReader 라이브러리는 브라우저 지원이 제한되었습니다. 좋은 대안이 있습니까?
Praxis Ashelin 2014

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 2014

Chromium 프로젝트 멤버 의이 댓글 bugs.chromium.org/p/chromium/issues/detail?id=158753#c104 에 따르면 : "변경 사항은 Chrome 81에 있습니다. 이는 8에서 Stable 버전으로 공개 될 것입니다. -10주의 시간 "
jeff forest

1
81로 시작하는 Chrome에서 구현 🎉 사람들이 브라우저를 업데이트하기까지는 시간이 걸립니다. caniuse
Robin Métral


4

크로스 브라우저를 원한다면 가장 좋은 방법은 서버에서하는 것입니다. 파일 URL을 가져와 EXIF ​​데이터를 반환하는 API를 가질 수 있습니다. PHP에는이를위한 모듈이 있습니다.

이것은 Ajax를 사용하여 수행 할 수 있으므로 사용자에게 원활하게 작동합니다. 브라우저 간 호환성에 관심이없고 HTML5 파일 기능 에 의존 할 수 있다면 네이티브 JavaScript에서 해당 데이터를 가져올 수 있는 라이브러리 JsJPEGmeta 를 살펴보십시오 .


21
@MikkoOhtamaa : Stack Overflow가 모든 사람의 질문에 답한다는 것을 이해해야 합니다. 당신과 같은 목표를 가진 다음 사람은 PHP 개발자 일 수 있습니다. 왜 Xeon06에 포함 된 정보를 거부하고 싶습니까? 그냥 있기 때문에, 편집 할 수있는 것을 부적절 당신이 하는 PHP 솔루션을 원하지 않는다.
Jon Skeet

5
질문은 "자바 스크립트"로되어 있으므로 해당 부분은 관련이 없습니다. 이미 사이트에 PHP에 대한 다른 많은 유사한 질문과 답변이 있으며이 질문에 대한 불필요한 소음입니다.
Mikko Ohtamaa 2012

2
사람들이 Javascript 솔루션을 요청하면 PHP 솔루션을 첫 번째 게시물로보고 싶지 않습니다.
Mikko Ohtamaa 2012

1
대부분의 당신과 함께 동의 것처럼 보인다 @MikkoOhtamaa meta.stackexchange.com/questions/157338/... 당신은 당신의 질문에 대한 소유권의 일부 부당 감각을 갖고있는 것 같다.
Alex Turpin 2012

1
나는 처음에 정답을 갖도록 답을 편집했습니다. 퍼즈 미안합니다.
Mikko Ohtamaa

3

exif 방향을 CSS 변환으로 변환하는 내가 작성한 모듈 (브라우저에서 사용할 수 있음)을 확인하십시오 . https://github.com/Sobesednik/exif2css

모든 방향으로 JPEG 픽스쳐를 생성하는이 노드 프로그램도 있습니다 : https://github.com/Sobesednik/generate-exif-fixtures


1
멋진 모듈! 그러나 처음에 JPEG에서 EXIF ​​정보를 어떻게 얻습니까?
Mikko Ohtamaa

@MikkoOhtamaa 감사와 아니 그것은, 당신은 EXIF-JS 또는 exiftool 서버 측 함께 할 필요가 없습니다
zavr

이것은 유용합니다. 그러나 가로 사진이 아닌 인물 사진에서만 올바르게 작동하는 것처럼 보입니다.
Sridhar Sarnobat

3

특히 너비가 높이보다 넓은 img 태그의 경우 오른쪽 회전이있는 일부 img 태그에서 정상적으로 html의 Android 카메라로 사진을 표시하는 확장 코드 업로드 합니다. 이 코드가 추악하다는 것을 알고 있지만 다른 패키지를 설치할 필요가 없습니다. (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에서 util 메서드를 만들었습니다. 이 버전은 프로젝트에도 필요할 수있는 회전 각도를 반환합니다.

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.