JS客户端Exif方向:旋转和镜像JPEG图像


154

数码相机的照片通常使用EXIF“方向”标签保存为JPEG。为了正确显示,需要根据设置的方向旋转/镜像图像,但是浏览器会忽略此信息来渲染图像。即使在大型商业Web应用程序中,对EXIF方向的支持也可能参差不齐1。相同的来源还提供 了JPEG可以具有的8个不同方向的很好的总结:

EXIF方向摘要

示例图像位于4处

问题是如何在客户端旋转/镜像图像,以使其正确显示并在必要时可以进行进一步处理?

有JS库可用于解析EXIF数据,包括方向属性2。Flickr指出,在解析大图像时可能需要性能,这需要使用Webworkers 3

控制台工具可以正确调整图像方向5。解决问题的PHP脚本可在6获得

Answers:


145

github项目JavaScript-Load-Image提供了针对EXIF方向问题的完整解决方案,可以针对所有8个exif方向正确旋转/镜像。请参阅javascript exif方向在线演示

该图像被绘制到HTML5画布上。通过画布操作在js / load-image-orientation.js中实现了其正确的呈现。

希望这可以节省其他人的时间,并向搜索引擎介绍这种开源gem :)


1
我一直在使用此库,但最近它在iOS 8.3上坏了,这是我最需要它运行的地方:(
Gordon Sun

2
我真的很想知道这有什么帮助。我在尝试学习它,所以我可以解析页面并在需要旋转图像时旋转图像,或者在实际上传之前检测方向并旋转文件(以某种方式)。该演示都没有。它只是从文件输入中提取一个文件并以正确的方式显示它,这在现实世界中什么时候有用?当我解析页面并将图像标签中的URL馈入loadImage库时,没有exif数据,因此无法做到这一点。对于上传,它返回一个canvas对象,因此我无法将其发送到服务器或其他任何对象。
igneosaur's

3
@igneosaur:您可以将base64编码的数据发送到服务器,canvas.toDataURL()并在服务器端进行解码和保存。
br4nnigan

1
除了使用load-image项目,您还可以使用其某些代码库来烘焙自己的代码-只需查看github.com/blueimp/JavaScript-Load-Image/blob/master/js/…即可
安迪·洛伦兹

1
有什么方法可以使该库生成的画布像常规<img>标签一样具有响应性?
Erik Berkun-Drevnig '16

103

Mederr的上下文转换效果很好。如果您只需要提取方向,请使用此功能 -您不需要任何EXIF读取库。以下是用于在base64图像中重新设置方向的功能。 这是一个小玩意儿。我还准备了一个带有方向提取演示小提琴

function resetOrientation(srcBase64, srcOrientation, callback) {
  var img = new Image();    

  img.onload = function() {
    var width = img.width,
        height = img.height,
        canvas = document.createElement('canvas'),
        ctx = canvas.getContext("2d");

    // set proper canvas dimensions before transform & export
    if (4 < srcOrientation && srcOrientation < 9) {
      canvas.width = height;
      canvas.height = width;
    } else {
      canvas.width = width;
      canvas.height = height;
    }

    // transform context before drawing image
    switch (srcOrientation) {
      case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
      case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
      case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
      case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
      case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
      case 7: ctx.transform(0, -1, -1, 0, height, width); break;
      case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
      default: break;
    }

    // draw image
    ctx.drawImage(img, 0, 0);

    // export base64
    callback(canvas.toDataURL());
  };

  img.src = srcBase64;
};

7
switch不需要默认方向的情况,因为该转换不会执行任何操作。另外,考虑使用srcOrientation > 4 && srcOrientation < 9代替[5,6,7,8].indexOf(srcOrientation) > -1,因为它速度更快,资源占用更少(包括RAM和CPU)。那里不需要数组。当批处理大量每个位都很重要的图像时,这一点很重要。否则,答案很好。已投票!
瑞安·卡萨斯

4
@RyanCasas我不知道与indexOf您的建议相比有多沉重。我运行了一个具有1000万次迭代的简单循环,速度提高了1400%。尼斯:D谢谢一堆!
WunderBart

1
我在Android WebView中使用了这个答案,结果发现,有一些Android设备在WebView中不支持WebGL(请参见bugs.chromium.org/p/chromium/issues/detail?id=555116)。此类设备在旋转时可能会花费很长时间,具体取决于图像的大小。
ndreisg

1
与引用一起使用时getOrientation,我很好奇这在性能方面是否有效。getOrientation调用fileReader.readAsArrayBuffer,然后我们调用URL.createObjectURL并将结果传递到中resetOrientation,该结果将该URL加载为图像。这是否意味着图像文件将不被浏览器“加载” /读取一次,而是两次,还是我误会了?
奥利弗·约瑟夫·阿什

1
另外,对于已经尊重方向的浏览器该怎么办?我认为iOS Safari和image-orientation: from-image使用CSS的浏览器都支持这种情况。
奥利弗·约瑟夫·阿什

40

如果

width = img.width;
height = img.height;
var ctx = canvas.getContext('2d');

然后,您可以使用这些转换将图像转到方向1

从方向:

  1. ctx.transform(1, 0, 0, 1, 0, 0);
  2. ctx.transform(-1, 0, 0, 1, width, 0);
  3. ctx.transform(-1, 0, 0, -1, width, height);
  4. ctx.transform(1, 0, 0, -1, 0, height);
  5. ctx.transform(0, 1, 1, 0, 0, 0);
  6. ctx.transform(0, 1, -1, 0, height, 0);
  7. ctx.transform(0, -1, -1, 0, height, width);
  8. ctx.transform(0, -1, 1, 0, 0, width);

在ctx上绘制图像之前


58
这里到底发生了什么?
穆罕默德·乌默尔

1
这样,您只需要知道方向(例如通过EXIF),然后根据需要旋转即可。我要找的一半。
布伦登

2
这些转换对我不起作用-相反,我使用了github.com/blueimp/JavaScript-Load-Image/blob/master/js/上的load-image项目中的代码,以获得我认为是经过验证的代码它在画布上下文上使用了平移,旋转操作以及宽度/高度交换。经过数小时的尝试以及其他尝试进行变形等的尝试,给了我100%的结果
Andy Lorenz

1
在ctx上绘制图像时为什么有关系?在画布上绘制图像后无法旋转画布吗?
AlxVallejo

方向8的旋转对我来说是这样的:ctx.transform(0,-1,1,0,0,height);
Giorgio Barchiesi

24

好的,除了@ user3096626回答,我认为如果有人提供了代码示例,它将更有用,下面的示例将向您展示如何解决来自url(远程图像)的图像方向:


解决方案1:使用javascript(推荐)

  1. 由于加载图像库不会仅从url图像(文件/ blob)中提取exif标签,因此我们将同时使用exif-js加载图像 javascript库,因此请按照以下步骤将这些库添加到您的页面中:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/exif-js/2.1.0/exif.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-load-image/2.12.2/load-image.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-load-image/2.12.2/load-image-scale.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-load-image/2.12.2/load-image-orientation.min.js"></script>

    请注意,exif-js的2.2版似乎存在问题,因此我们使用2.1

  2. 那么基本上我们将要做的是

    a-使用加载图片 window.loadImage()

    b-使用以下命令读取exif标签 window.EXIF.getData()

    c-将图像转换为画布并使用固定图像方向 window.loadImage.scale()

    d-将画布放入文档中

干得好 :)

window.loadImage("/your-image.jpg", function (img) {
  if (img.type === "error") {
    console.log("couldn't load image:", img);
  } else {
    window.EXIF.getData(img, function () {
        var orientation = EXIF.getTag(this, "Orientation");
        var canvas = window.loadImage.scale(img, {orientation: orientation || 0, canvas: true});
        document.getElementById("container").appendChild(canvas); 
        // or using jquery $("#container").append(canvas);

    });
  }
});

当然,您也可以从canvas对象获取图像为base64并将其放置在img src属性中,因此使用jQuery即可;)

$("#my-image").attr("src",canvas.toDataURL());

这是完整的代码:github:https : //github.com/digital-flowers/loadimage-exif-example


解决方案2:使用html(浏览器黑客)

有一个非常快速简便的技巧,如果直接在新标签页中打开图像而没有任何html,则大多数浏览器会以正确的方向显示图像(大声笑,我不知道为什么),因此基本上您可以使用iframe显示图像通过直接将iframe src属性作为图片网址:

<iframe src="/my-image.jpg"></iframe>

解决方案3:使用CSS(仅iOS上的Firefox和Safari)

有css3属性可以解决图像方向问题,但是它仅在firefox和safari / ios上有效,仍然值得一提,因为它将很快对所有浏览器可用(caniuse的浏览器支持信息)

img {
   image-orientation: from-image;
}

解决方案1不适用于我。它正在返回window.loadImage未定义
Perry

如果您没有包括我在第1步中提到的库,就会发生这种情况
Fareed Alnamrouti

我包括了图书馆。我喜欢您的示例的简单性,但我不断收到该错误消息。
佩里

1
似乎exif-js坏了,请检查我的编辑我还在github上添加了完整代码:github.com/digital-flowers/loadimage-exif-example
Fareed Alnamrouti

1
好的,检查github项目是否有一个新文件“ upload.html”,欢迎您:)
Fareed Alnamrouti

10

对于那些具有输入控件文件的用户,不知道其方向是什么,有点懒惰,并且不想在下面包含一个大型库,@ WunderBart提供的代码与他链接到的答案融合在一起(https://stackoverflow.com/a/32490603)找到方向。

function getDataUrl(file, callback2) {
        var callback = function (srcOrientation) {
            var reader2 = new FileReader();
            reader2.onload = function (e) {
                var srcBase64 = e.target.result;
                var img = new Image();

                img.onload = function () {
                    var width = img.width,
                        height = img.height,
                        canvas = document.createElement('canvas'),
                        ctx = canvas.getContext("2d");

                    // set proper canvas dimensions before transform & export
                    if (4 < srcOrientation && srcOrientation < 9) {
                        canvas.width = height;
                        canvas.height = width;
                    } else {
                        canvas.width = width;
                        canvas.height = height;
                    }

                    // transform context before drawing image
                    switch (srcOrientation) {
                        case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
                        case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
                        case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
                        case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
                        case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
                        case 7: ctx.transform(0, -1, -1, 0, height, width); break;
                        case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
                        default: break;
                    }

                    // draw image
                    ctx.drawImage(img, 0, 0);

                    // export base64
                    callback2(canvas.toDataURL());
                };

                img.src = srcBase64;
            }

            reader2.readAsDataURL(file);
        }

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

可以很容易地这样称呼

getDataUrl(input.files[0], function (imgBase64) {
      vm.user.BioPhoto = imgBase64;
});

2
经过2个多小时的Google搜索,谢谢,我找到了正确的解决方案。
Trieu先生

2
为什么有人会懒于选择更轻便的解决方案?
露水

4
移植到Typescript并从大约4秒到〜20毫秒进行了优化。Base64编码是瓶颈-使用image/jpeg而不是默认值image/png,并将输出图像的大小调整为最大宽度(在我的情况下为400px)产生了很大的差异。(这对于缩略图来说效果很好,但是如果您必须显示完整图像,我建议您避免使用base64并直接将canvas元素直接注入页面中...)
mindplay.dk

8

WunderBart的答案对我来说是最好的。请注意,如果图像通常是正确的方向,则可以大大提高速度,只需先测试方向,然后在不需要旋转的情况下绕过其余代码即可。

将来自wunderbart的所有信息放在一起,就像这样;

var handleTakePhoto = function () {
    let fileInput: HTMLInputElement = <HTMLInputElement>document.getElementById('photoInput');
    fileInput.addEventListener('change', (e: any) => handleInputUpdated(fileInput, e.target.files));
    fileInput.click();
}

var handleInputUpdated = function (fileInput: HTMLInputElement, fileList) {
    let file = null;

    if (fileList.length > 0 && fileList[0].type.match(/^image\//)) {
        isLoading(true);
        file = fileList[0];
        getOrientation(file, function (orientation) {
            if (orientation == 1) {
                imageBinary(URL.createObjectURL(file));
                isLoading(false);
            }
            else 
            {
                resetOrientation(URL.createObjectURL(file), orientation, function (resetBase64Image) {
                    imageBinary(resetBase64Image);
                    isLoading(false);
                });
            }
        });
    }

    fileInput.removeEventListener('change');
}


// from http://stackoverflow.com/a/32490603
export function getOrientation(file, callback) {
    var reader = new FileReader();

    reader.onload = function (event: any) {
        var view = new DataView(event.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.slice(0, 64 * 1024));
};

export function resetOrientation(srcBase64, srcOrientation, callback) {
    var img = new Image();

    img.onload = function () {
        var width = img.width,
            height = img.height,
            canvas = document.createElement('canvas'),
            ctx = canvas.getContext("2d");

        // set proper canvas dimensions before transform & export
        if (4 < srcOrientation && srcOrientation < 9) {
            canvas.width = height;
            canvas.height = width;
        } else {
            canvas.width = width;
            canvas.height = height;
        }

        // transform context before drawing image
        switch (srcOrientation) {
            case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
            case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
            case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
            case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
            case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
            case 7: ctx.transform(0, -1, -1, 0, height, width); break;
            case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
            default: break;
        }

        // draw image
        ctx.drawImage(img, 0, 0);

        // export base64
        callback(canvas.toDataURL());
    };

    img.src = srcBase64;
}

1
很棒的方法。您还应该考虑处理-2-1返回,getOrientation以便不要尝试旋转没有旋转数据的非jpg图像或图像。
史蒂文·兰伯特

我进行了更多优化,请参见上面的评论 -我也刚刚添加了file.slice()优化功能,但真正的推动力来自使用较小的图像和image/jpeg输出格式来创建较小的数据URL。(即使是10兆像素的图像,也没有明显的延迟。)
mindplay.dk

小心file.slice(),因为您将阻止对base64图像源的支持。
标记

谢谢标记-希望提供替代品的编辑吗?
statler

7

一个班轮有人吗?

我还没有看到有人提到browser-image-compression图书馆。它有一个完美的辅助功能。

用法: const orientation = await imageCompression.getExifOrientation(file)

这样的工具也可以通过许多其他方式使用。


这得到了方向。但是,如何使用此信息在客户端生成新的.jpg File对象?
masterxilo

File据我所知,您无法创建对象。接下来的最好的事情是将图像和方向发送到服务器,并在那里进行文件操作。或者,您可以使用canvastoDataURL方法生成base64字符串。
gilbert-v

1
不,您可以从Blob创建File对象。文件文件(数组部分,字符串文件名,BlobPropertyBag属性);developer.mozilla.org/de/docs/Web/API/File
masterxilo

2
金贴!@masterxilo
gilbert-v

2
这对我来说很棒!在过去的两天里,我一直在努力适应方向!由于某种原因,所有发布的转换开关语句将无法处理手机中的人像图像,其中会出现黑条。可能是因为我试图同时压缩和旋转。
cjd82187 '19


1

我正在使用混合解决方案(php + css)。

需要容器用于:

  • div.imgCont2 需要旋转的容器;
  • div.imgCont1zoomOut-所需的容器width:150%
  • div.imgCont 图像为zoomOut时,滚动条所需的容器。

<?php
    $image_url = 'your image url.jpg';
    $exif = @exif_read_data($image_url,0,true);
    $orientation = @$exif['IFD0']['Orientation'];
?>

<style>
.imgCont{
    width:100%;
    overflow:auto;
}
.imgCont2[data-orientation="8"]{
    transform:rotate(270deg);
    margin:15% 0;
}
.imgCont2[data-orientation="6"]{
    transform:rotate(90deg);
    margin:15% 0;
}
.imgCont2[data-orientation="3"]{
    transform:rotate(180deg);
}
img{
    width:100%;
}
</style>

<div class="imgCont">
  <div class="imgCont1">
    <div class="imgCont2" data-orientation="<?php echo($orientation) ?>">
      <img src="<?php echo($image_url) ?>">
    </div>
  </div>
</div>

谢谢,无论如何,这似乎是最好的解决方案。无需外部库,也不需要冗长的不必要代码块。只需使用php确定exif的方向,将其发送到视图,然后使用它触发可旋转适当度数的CSS类。干净整洁!编辑:这将在实际读取exif数据的浏览器中过度旋转图像并进行相应旋转。又名它将解决此问题在桌面上,但在ios野生动物园上创建一个新的。
cwal

0

除了@fareed namrouti的答案,

如果必须从文件输入元素浏览图像,则应使用此选项

<input type="file" name="file" id="file-input"><br/>
image after transform: <br/>
<div id="container"></div>

<script>
    document.getElementById('file-input').onchange = function (e) {
        var image = e.target.files[0];
        window.loadImage(image, function (img) {
            if (img.type === "error") {
                console.log("couldn't load image:", img);
            } else {
                window.EXIF.getData(image, function () {
                    console.log("load image done!");
                    var orientation = window.EXIF.getTag(this, "Orientation");
                    var canvas = window.loadImage.scale(img,
                        {orientation: orientation || 0, canvas: true, maxWidth: 200});
                    document.getElementById("container").appendChild(canvas);
                    // or using jquery $("#container").append(canvas);
                });
            }
        });
    };
</script>

0

我已经写了一个小php脚本来旋转图像。确保存储该图像,以便仅在每次请求时重新计算它。

<?php

header("Content-type: image/jpeg");
$img = 'IMG URL';

$exif = @exif_read_data($img,0,true);
$orientation = @$exif['IFD0']['Orientation'];
if($orientation == 7 || $orientation == 8) {
    $degrees = 90;
} elseif($orientation == 5 || $orientation == 6) {
    $degrees = 270;
} elseif($orientation == 3 || $orientation == 4) {
    $degrees = 180;
} else {
    $degrees = 0;
}
$rotate = imagerotate(imagecreatefromjpeg($img), $degrees, 0);
imagejpeg($rotate);
imagedestroy($rotate);

?>

干杯

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.