数码相机照片通常保存为带有 EXIF“方向”标签的 JPEG。为了正确显示,图像需要根据设置的方向进行旋转/镜像,但浏览器会忽略此信息来呈现图像。即使在大型商业网络应用程序中,对 EXIF 方向的支持也可能参差不齐1。同一来源还很好地总结了 JPEG 可以具有的 8 种不同方向:
https://i.stack.imgur.com/6cJTP.gif
4 上提供了示例图像。
问题是如何在客户端旋转/镜像图像,以便正确显示并在必要时进行进一步处理?
有可用于解析 EXIF 数据的 JS 库,包括方向属性 2。 Flickr 注意到在解析大图像时可能存在性能问题,需要使用网络工作者3。
控制台工具可以正确地重新定向图像 5。解决该问题的 PHP 脚本可在 6 获得
canvas.drawImage
时,因此您不再需要手动执行此操作。 twitter.com/zcorpan/status/1235709107933503489
github 项目 JavaScript-Load-Image 为 EXIF 方向问题提供了完整的解决方案,可以正确旋转/镜像所有 8 个 exif 方向的图像。查看 javascript exif orientation 的在线演示
图像被绘制到 HTML5 画布上。它的正确呈现是通过画布操作在 js/load-image-orientation.js 中实现的。
希望这可以节省其他人一些时间,并向搜索引擎介绍这个开源 gem :)
Mederr's context transform 完美运行。如果您只需要提取方向,请使用 this function - 您不需要任何 EXIF 读取库。下面是在 base64 图像中重新设置方向的功能。 Here's a fiddle for it。我还准备了一个fiddle with orientation extraction demo。
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;
};
switch
中的默认情况,因为该转换不做任何事情。此外,请考虑使用 srcOrientation > 4 && srcOrientation < 9
而不是 [5,6,7,8].indexOf(srcOrientation) > -1
,因为它速度更快且占用资源更少(RAM 和 CPU)。那里不需要数组。这在批量处理大量图像时很重要,其中每一位都很重要。否则,很好的答案。点赞!
indexOf
与您提议的相比有多大。我运行了一个具有 10M 次迭代的简单循环,它的速度提高了 1400%。很好 :D 非常感谢!
getOrientation
一起使用时,我很好奇这在性能方面是否有效。 getOrientation
调用 fileReader.readAsArrayBuffer
,然后我们调用 URL.createObjectURL
并将结果传递给 resetOrientation
,后者将该 URL 作为图像加载。这是否意味着图像文件将被浏览器“加载”/读取一次而不是两次,还是我误解了?
image-orientation: from-image
的浏览器就是这种情况。
如果
width = img.width;
height = img.height;
var ctx = canvas.getContext('2d');
然后您可以使用这些转换将图像转换为方向 1
从方向:
ctx.transform(1, 0, 0, 1, 0, 0); ctx.transform(-1, 0, 0, 1, width, 0); ctx.transform(-1, 0, 0, -1, 宽度, 高度); ctx.transform(1, 0, 0, -1, 0, 高度); ctx.transform(0, 1, 1, 0, 0, 0); ctx.transform(0, 1, -1, 0, height, 0); ctx.transform(0, -1, -1, 0, 高度, 宽度); ctx.transform(0, -1, 1, 0, 0, 宽度);
在 ctx 上绘制图像之前
好的,除了@user3096626 回答我认为如果有人提供代码示例会更有帮助,以下示例将向您展示如何修复来自 url(远程图像)的图像方向:
解决方案1:使用javascript(推荐)
因为 load-image 库不会仅从 url 图像(文件/blob)中提取 exif 标签,我们将同时使用 exif-js 和 load-image javascript 库,因此首先将这些库添加到您的页面,如下所示: 注意 exif-js 的 2.2 版本似乎有问题所以我们使用 2.1 然后基本上我们要做的是 a - 使用 window.loadImage() b -使用 window.EXIF.getData() 读取 exif 标签 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);
});
}
});
当然,您也可以从画布对象中获取 base64 格式的图像并将其放在 img src 属性中,因此您可以使用 jQuery ;)
$("#my-image").attr("src",canvas.toDataURL());
这是完整的代码:github:https://github.com/digital-flowers/loadimage-exif-example
解决方案 2:使用 html(浏览器 hack)
有一个非常快速和简单的 hack,如果图像直接在新标签中打开而没有任何 html,大多数浏览器都会以正确的方向显示图像(哈哈,我不知道为什么),所以基本上你可以使用 iframe 显示你的图像通过将 iframe src 属性直接作为图像 url:
<iframe src="/my-image.jpg"></iframe>
解决方案 3:使用 css(只有 ios 上的 firefox 和 safari)
有 css3 属性来修复图像方向,但它仅适用于 firefox 和 safari/ios 的问题仍然值得一提,因为它很快将适用于所有浏览器(来自 caniuse 的浏览器支持信息)
img {
image-orientation: from-image;
}
对于那些拥有来自输入控件的文件,不知道它的方向是什么,有点懒惰并且不想包含大型库的人来说,@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;
});
image/jpeg
而不是默认的 image/png
,并将输出图像的大小调整为最大宽度(在我的情况下为 400 像素)产生了巨大的差异。 (这适用于缩略图,但如果您必须显示完整图像,我建议避免使用 base64 并简单地将画布元素直接注入页面......)
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;
}
getOrientation
返回的 -2
和 -1
,这样您就不会尝试旋转非 jpg 或没有旋转数据的图像。
file.slice()
优化,但真正的提升来自使用更小的图像和 image/jpeg
输出格式来创建更小的数据 URL。 (即使是 10 兆像素的图像也没有明显的延迟。)
file.slice()
,因为您将阻止对 base64 图像源的支持。
一个班轮有人吗?
我还没有看到有人提到 browser-image-compression
library。它有一个完美的辅助功能。
用法:const orientation = await imageCompression.getExifOrientation(file)
在许多其他方面也是如此有用的工具。
File
对象。下一个最好的方法是将图像和方向发送到服务器,并在那里进行文件操作。或者,您可以使用 canvas
和 toDataURL
方法生成 base64 字符串。
我创建了一个封装在 ES6 模块中的类,它正好解决了这个问题。
它有 103 行,没有依赖关系,并且结构和文档都相当好,意味着易于修改/重用。
处理所有 8 种可能的方向,并且是基于 Promise 的。
给你,希望这仍然对某人有帮助:https://gist.github.com/vdavid/3f9b66b60f52204317a4cc0e77097913
Wunderbart 的 post 与 statler 的 improvements 一起为我工作。添加更多注释和语法清理,并传回方向值,我可以随意使用以下代码。只需调用下面的 readImageFile()
函数,即可返回转换后的图像和原始方向。
const JpegOrientation = [
"NOT_JPEG",
"NORMAL",
"FLIP-HORIZ",
"ROT180",
"FLIP-HORIZ-ROT180",
"FLIP-HORIZ-ROT270",
"ROT270",
"FLIP-HORIZ-ROT90",
"ROT90"
];
//Provided a image file, determines the orientation of the file based on the EXIF information.
//Calls the "callback" function with an index into the JpegOrientation array.
//If the image is not a JPEG, returns 0. If the orientation value cannot be read (corrupted file?) return -1.
function getOrientation(file, callback) {
const reader = new FileReader();
reader.onload = (e) => {
const view = new DataView(e.target.result);
if (view.getUint16(0, false) !== 0xFFD8) {
return callback(0); //NOT A JPEG FILE
}
const length = view.byteLength;
let offset = 2;
while (offset < length) {
if (view.getUint16(offset+2, false) <= 8) //unknown?
return callback(-1);
const marker = view.getUint16(offset, false);
offset += 2;
if (marker === 0xFFE1) {
if (view.getUint32(offset += 2, false) !== 0x45786966)
return callback(-1); //unknown?
const little = view.getUint16(offset += 6, false) === 0x4949;
offset += view.getUint32(offset + 4, little);
const 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)); //found orientation code
}
}
}
else if ((marker & 0xFF00) !== 0xFF00) {
break;
}
else {
offset += view.getUint16(offset, false);
}
}
return callback(-1); //unknown?
};
reader.readAsArrayBuffer(file);
}
//Takes a jpeg image file as base64 and transforms it back to original, providing the
//transformed image in callback. If the image is not a jpeg or is already in normal orientation,
//just calls the callback directly with the source.
//Set type to the desired output type if transformed, default is image/jpeg for speed.
function resetOrientation(srcBase64, srcOrientation, callback, type = "image/jpeg") {
if (srcOrientation <= 1) { //no transform needed
callback(srcBase64);
return;
}
const img = new Image();
img.onload = () => {
const width = img.width;
const height = img.height;
const canvas = document.createElement('canvas');
const 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 1: normal, no transform needed
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(type), srcOrientation);
};
img.src = srcBase64;
};
//Read an image file, providing the returned data to callback. If the image is jpeg
//and is transformed according to EXIF info, transform it first.
//The callback function receives the image data and the orientation value (index into JpegOrientation)
export function readImageFile(file, callback) {
getOrientation(file, (orientation) => {
console.log("Read file \"" + file.name + "\" with orientation: " + JpegOrientation[orientation]);
const reader = new FileReader();
reader.onload = () => { //when reading complete
const img = reader.result;
resetOrientation(img, orientation, callback);
};
reader.readAsDataURL(file); //start read
});
}
我正在使用混合解决方案(php + css)。
容器需要:
div.imgCont2 容器需要旋转;
div.imgCont1 容器需要缩放 - 宽度:150%;
当图像为 zoomOut 时,滚动条需要 div.imgCont 容器。
.
<?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>
除了@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>
我写了一个小 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);
?>
干杯
就我而言,我需要 exif-auto-rotate 库。
我有来自后端的 base64 格式图像,在使用它之前我必须将其旋转到正确的方向。它会在更改旋转后返回 base64,这对我来说也是一个优点。
这是该库的 npm 链接:https://www.npmjs.com/package/exif-auto-rotate
这个答案是为 Angular 的人准备的,这里有一个包可以帮助你找到方向。
在下面的示例中,我使用 changeEvent 作为输入标签,但如果您有 dataUrl,则可以使用它
如果您没有 dataUrl ,则可以轻松转换 File -> dataUrl 或 blob -> dataUrl。我已附加文件 -> 转换器功能,可帮助我将所选图像文件转换为 dataUrl
https://www.npmjs.com/package/ngx-image-compress 这是包裹npm i ngx-image-compress
import { DOC_ORIENTATION, NgxImageCompressService } from 'ngx-image-compress';
@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss'],
})
export class Test {
constructor(private imageCompress: NgxImageCompressService) {}
async onFileSelected(event: any) {
const file: File = event.target.files[0]
const dataUrl : string = await this.fileToDataURL(file)
const orientation : DOC_ORIENTATION = await this.imageCompress.getOrientation(file)
}
fileToDataURL(blob: Blob): Promise<string> {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = _e => resolve(reader.result as string);
reader.onerror = _e => reject(reader.error);
reader.onabort = _e => reject(new Error("Read aborted"));
reader.readAsDataURL(blob);
});
}
}
不定期副业成功案例分享
canvas.toDataURL()
将 base64 编码数据发送到服务器并解码并保存在服务器端。