如何在浏览器中通过 Javascript 压缩图像?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/14672746/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to compress an image via Javascript in the browser?
提问by pomeh
TL;DR;
TL; 博士;
Is there a way to compress an image (mostly jpeg, png and gif) directly browser-side, before uploading it ? I'm pretty sure JavaScript can do this, but I can't find a way to achieve it.
有没有办法在上传之前直接在浏览器端压缩图像(主要是 jpeg、png 和 gif)?我很确定 JavaScript 可以做到这一点,但我找不到实现它的方法。
Here's the full scenario I would like to implement:
这是我想要实现的完整场景:
- the user goes to my website, and choose an image via an
input type="file"element, - this image is retrieved via JavaScript, we do some verification such as correct file format, maximum file size etc,
- if every thing is OK, a preview of the image is displayed on the page,
- the user can do some basic operations such as rotate the image by 90°/-90°, crop it following a pre-defined ratio, etc, or the user can upload another image and return to step 1,
- when the user is satisfied, the edited image is then compressed and "saved" locally (not saved to a file, but in the browser memory/page),-
- the user fill a form with data like name, age etc,
- the user click on the "Finish" button, then the form containing datas + compressed image is sent to the server (without AJAX),
- 用户访问我的网站,并通过
input type="file"元素选择图像, - 此图像是通过 JavaScript 检索的,我们会进行一些验证,例如正确的文件格式、最大文件大小等,
- 如果一切正常,页面上会显示图像预览,
- 用户可以进行一些基本操作,例如将图像旋转 90°/-90°,按照预先定义的比例进行裁剪等,或者用户可以上传另一张图像并返回到步骤 1,
- 当用户满意时,编辑后的图像会被压缩并在本地“保存”(不是保存到文件中,而是保存在浏览器内存/页面中),-
- 用户填写表格,填写姓名、年龄等数据,
- 用户单击“完成”按钮,然后将包含数据+压缩图像的表单发送到服务器(不带 AJAX),
The full process up to the last step should be done client side, and should be compatible on latest Chrome and Firefox, Safari 5+ and IE 8+. If possible, only JavaScript should be used (but I'm pretty sure this is not possible).
到最后一步的完整过程应该在客户端完成,并且应该兼容最新的 Chrome 和 Firefox、Safari 5+ 和IE 8+。如果可能,只应使用 JavaScript(但我很确定这是不可能的)。
I've not code anything right now, but I've thought about it already. File reading locally is possible via File API, image previewing and editing could be done using Canvaselement, but I can't find a way to do the image compression part.
我现在没有写任何代码,但我已经考虑过了。可以通过File API在本地读取文件,可以使用Canvas元素完成图像预览和编辑,但我找不到进行图像压缩部分的方法。
According to html5please.comand caniuse.com, supporting those browser is quite hard (thanks to IE), but could be done using polyfill such as FlashCanvasand FileReader.
根据html5please.com和caniuse.com,支持这些浏览器非常困难(感谢 IE),但可以使用诸如FlashCanvas和FileReader 之类的polyfill来完成。
Actually, the goal is to reduce file size, so I see image compression as a solution. But, I know that uploaded images are going to be displayed on my website, every time at the same place, and I know the dimension of this display area (eg. 200x400). So, I could resize the image to fit those dimensions, thus reducing file size. I have no idea what would be the compression ratio for this technique.
实际上,目标是减小文件大小,因此我将图像压缩视为一种解决方案。但是,我知道上传的图像将显示在我的网站上,每次都在同一个地方,并且我知道这个显示区域的尺寸(例如 200x400)。因此,我可以调整图像大小以适应这些尺寸,从而减小文件大小。我不知道这种技术的压缩比是多少。
What do you think ? Do you have any advice to tell me ? Do you know any way to compress an image browser-side in JavaScript ? Thanks for your replies.
你怎么认为 ?你有什么建议要告诉我吗?您知道在 JavaScript 中压缩图像浏览器端的任何方法吗?感谢您的回复。
回答by psychowood
In short:
简而言之:
- Read the files using the HTML5 FileReader API with .readAsArrayBuffer
- Create a Blob with the file data and get its url with window.URL.createObjectURL(blob)
- Create new Image element and set it's src to the file blob url
- Send the image to the canvas. The canvas size is set to desired output size
- Get the scaled-down data back from canvas via canvas.toDataURL("image/jpeg",0.7) (set your own output format and quality)
- Attach new hidden inputs to the original form and transfer the dataURI images basically as normal text
- On backend, read the dataURI, decode from Base64, and save it
- 使用带有 .readAsArrayBuffer 的 HTML5 FileReader API 读取文件
- 使用文件数据创建一个 Blob 并使用window.URL.createObjectURL(blob)获取其 url
- 创建新的 Image 元素并将其 src 设置为文件 blob url
- 将图像发送到画布。画布大小设置为所需的输出大小
- 通过 canvas.toDataURL("image/jpeg",0.7) 从画布取回缩小的数据(设置您自己的输出格式和质量)
- 将新的隐藏输入附加到原始表单并将 dataURI 图像基本上作为普通文本传输
- 在后端,读取 dataURI,从 Base64 解码并保存
Source: code.
来源:代码。
回答by Simon Lindholm
I see two things missing from the other answers:
我看到其他答案中缺少两件事:
canvas.toBlob(when available) is more performant thancanvas.toDataURL, and also async.- the file -> image -> canvas -> file conversion loses EXIF data; in particular, data about image rotation commonly set by modern phones/tablets.
canvas.toBlob(如果可用)比 性能更高canvas.toDataURL,并且也是异步的。- 文件 -> 图像 -> 画布 -> 文件转换丢失 EXIF 数据;特别是现代手机/平板电脑通常设置的有关图像旋转的数据。
The following script deals with both points:
以下脚本处理这两点:
// From https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob, needed for Safari:
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], {type: type || 'image/png'}));
}
});
}
window.URL = window.URL || window.webkitURL;
// Modified from https://stackoverflow.com/a/32490603, cc by-sa 3.0
// -2 = not jpeg, -1 = no data, 1..8 = orientations
function getExifOrientation(file, callback) {
// Suggestion from http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/:
if (file.slice) {
file = file.slice(0, 131072);
} else if (file.webkitSlice) {
file = file.webkitSlice(0, 131072);
}
var reader = new FileReader();
reader.onload = function(e) {
var view = new DataView(e.target.result);
if (view.getUint16(0, false) != 0xFFD8) {
callback(-2);
return;
}
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) {
callback(-1);
return;
}
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) {
callback(view.getUint16(offset + (i * 12) + 8, little));
return;
}
}
else if ((marker & 0xFF00) != 0xFF00) break;
else offset += view.getUint16(offset, false);
}
callback(-1);
};
reader.readAsArrayBuffer(file);
}
// Derived from https://stackoverflow.com/a/40867559, cc by-sa
function imgToCanvasWithOrientation(img, rawWidth, rawHeight, orientation) {
var canvas = document.createElement('canvas');
if (orientation > 4) {
canvas.width = rawHeight;
canvas.height = rawWidth;
} else {
canvas.width = rawWidth;
canvas.height = rawHeight;
}
if (orientation > 1) {
console.log("EXIF orientation = " + orientation + ", rotating picture");
}
var ctx = canvas.getContext('2d');
switch (orientation) {
case 2: ctx.transform(-1, 0, 0, 1, rawWidth, 0); break;
case 3: ctx.transform(-1, 0, 0, -1, rawWidth, rawHeight); break;
case 4: ctx.transform(1, 0, 0, -1, 0, rawHeight); break;
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
case 6: ctx.transform(0, 1, -1, 0, rawHeight, 0); break;
case 7: ctx.transform(0, -1, -1, 0, rawHeight, rawWidth); break;
case 8: ctx.transform(0, -1, 1, 0, 0, rawWidth); break;
}
ctx.drawImage(img, 0, 0, rawWidth, rawHeight);
return canvas;
}
function reduceFileSize(file, acceptFileSize, maxWidth, maxHeight, quality, callback) {
if (file.size <= acceptFileSize) {
callback(file);
return;
}
var img = new Image();
img.onerror = function() {
URL.revokeObjectURL(this.src);
callback(file);
};
img.onload = function() {
URL.revokeObjectURL(this.src);
getExifOrientation(file, function(orientation) {
var w = img.width, h = img.height;
var scale = (orientation > 4 ?
Math.min(maxHeight / w, maxWidth / h, 1) :
Math.min(maxWidth / w, maxHeight / h, 1));
h = Math.round(h * scale);
w = Math.round(w * scale);
var canvas = imgToCanvasWithOrientation(img, w, h, orientation);
canvas.toBlob(function(blob) {
console.log("Resized image to " + w + "x" + h + ", " + (blob.size >> 10) + "kB");
callback(blob);
}, 'image/jpeg', quality);
});
};
img.src = URL.createObjectURL(file);
}
Example usage:
用法示例:
inputfile.onchange = function() {
// If file size > 500kB, resize such that width <= 1000, quality = 0.9
reduceFileSize(this.files[0], 500*1024, 1000, Infinity, 0.9, blob => {
let body = new FormData();
body.set('file', blob, blob.name || "file.jpg");
fetch('/upload-image', {method: 'POST', body}).then(...);
});
};
回答by Vivian River
@PsychoWoods' answer is good. I would like to offer my own solution. This Javascript function takes an image data URL and a width, scales it to the new width, and returns a new data URL.
@PsychoWoods 的回答很好。我想提供我自己的解决方案。这个 Javascript 函数接受一个图像数据 URL 和一个宽度,将其缩放到新的宽度,并返回一个新的数据 URL。
// Take an image URL, downscale it to the given width, and return a new image URL.
function downscaleImage(dataUrl, newWidth, imageType, imageArguments) {
"use strict";
var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
// Provide default values
imageType = imageType || "image/jpeg";
imageArguments = imageArguments || 0.7;
// Create a temporary image so that we can compute the height of the downscaled image.
image = new Image();
image.src = dataUrl;
oldWidth = image.width;
oldHeight = image.height;
newHeight = Math.floor(oldHeight / oldWidth * newWidth)
// Create a temporary canvas to draw the downscaled image on.
canvas = document.createElement("canvas");
canvas.width = newWidth;
canvas.height = newHeight;
// Draw the downscaled image on the canvas and return the new data URL.
ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, newWidth, newHeight);
newDataUrl = canvas.toDataURL(imageType, imageArguments);
return newDataUrl;
}
This code can be used anywhere you have a data URL and want a data URL for a downscaled image.
此代码可用于您拥有数据 URL 并需要缩小图像的数据 URL 的任何地方。
回答by u8409280
You can take a look at image-conversion,Try it here --> demo page
你可以看看image-conversion,在这里试试 -->演示页面
回答by Russell Briggs
I had an issue with the downscaleImage()function posted above by @daniel-allen-langdon in that the image.widthand image.heightproperties are not available immediately because the image load is asynchronous.
我对downscaleImage()@daniel-allen-langdon 上面发布的函数有一个问题,因为图像加载是异步的,image.width并且image.height属性不能立即可用。
Please see updated TypeScript example below that takes this into account, uses asyncfunctions, and resizes the image based on the longest dimension rather than just the width
请参阅下面更新的 TypeScript 示例,该示例将这一点考虑在内,使用async函数并根据最长尺寸而不只是宽度调整图像大小
function getImage(dataUrl: string): Promise<HTMLImageElement>
{
return new Promise((resolve, reject) => {
const image = new Image();
image.src = dataUrl;
image.onload = () => {
resolve(image);
};
image.onerror = (el: any, err: ErrorEvent) => {
reject(err.error);
};
});
}
export async function downscaleImage(
dataUrl: string,
imageType: string, // e.g. 'image/jpeg'
resolution: number, // max width/height in pixels
quality: number // e.g. 0.9 = 90% quality
): Promise<string> {
// Create a temporary image so that we can compute the height of the image.
const image = await getImage(dataUrl);
const oldWidth = image.naturalWidth;
const oldHeight = image.naturalHeight;
console.log('dims', oldWidth, oldHeight);
const longestDimension = oldWidth > oldHeight ? 'width' : 'height';
const currentRes = longestDimension == 'width' ? oldWidth : oldHeight;
console.log('longest dim', longestDimension, currentRes);
if (currentRes > resolution) {
console.log('need to resize...');
// Calculate new dimensions
const newSize = longestDimension == 'width'
? Math.floor(oldHeight / oldWidth * resolution)
: Math.floor(oldWidth / oldHeight * resolution);
const newWidth = longestDimension == 'width' ? resolution : newSize;
const newHeight = longestDimension == 'height' ? resolution : newSize;
console.log('new width / height', newWidth, newHeight);
// Create a temporary canvas to draw the downscaled image on.
const canvas = document.createElement('canvas');
canvas.width = newWidth;
canvas.height = newHeight;
// Draw the downscaled image on the canvas and return the new data URL.
const ctx = canvas.getContext('2d')!;
ctx.drawImage(image, 0, 0, newWidth, newHeight);
const newDataUrl = canvas.toDataURL(imageType, quality);
return newDataUrl;
}
else {
return dataUrl;
}
}
回答by nfroidure
Edit: As per the Mr Me comment on this answer, it looks like compression is now available for JPG/WebP formats ( see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL).
编辑:根据 Mr Me 对此答案的评论,现在看来压缩可用于 JPG/WebP 格式(请参阅https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL) .
As far as I know, you cannot compress images using canvas, instead, you can resize it. Using canvas.toDataURL will not let you choose the compression ratio to use. You can take a look at canimage that does exactly what you want : https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js
据我所知,您不能使用画布压缩图像,而是可以调整其大小。使用 canvas.toDataURL 不会让您选择要使用的压缩率。您可以查看完全符合您要求的 canimage:https: //github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js
In fact, it's often sufficient to just resize the image to decrease it's size but if you want to go further, you'll have to use newly introduced method file.readAsArrayBuffer to get a buffer containing the image data.
事实上,通常只调整图像大小以减小其大小就足够了,但是如果您想更进一步,则必须使用新引入的方法 file.readAsArrayBuffer 来获取包含图像数据的缓冲区。
Then, just use a DataView to read it's content according to the image format specification (http://en.wikipedia.org/wiki/JPEGor http://en.wikipedia.org/wiki/Portable_Network_Graphics).
然后,只需使用 DataView 根据图像格式规范(http://en.wikipedia.org/wiki/JPEG或http://en.wikipedia.org/wiki/Portable_Network_Graphics)读取其内容。
It'll be hard to deal with image data compression, but it is worse a try. On the other hand, you can try to delete the PNG headers or the JPEG exif data to make your image smaller, it should be easier to do so.
处理图像数据压缩会很困难,但更糟糕的是尝试。另一方面,您可以尝试删除 PNG 标头或 JPEG exif 数据以使您的图像更小,这样做应该更容易。
You'll have to create another DataWiew on another buffer and fill it with the filtered image content. Then, you'll just have to encode you're image content to DataURI using window.btoa.
您必须在另一个缓冲区上创建另一个 DataWiew,并用过滤后的图像内容填充它。然后,您只需使用 window.btoa 将您的图像内容编码为 DataURI。
Let me know if you implement something similar, will be interesting to go through the code.
让我知道你是否实现了类似的东西,通过代码会很有趣。
回答by lifeisbeautiful
For JPG Image compression you can use the best compression technique called JIC (Javascript Image Compression)This will definitely help you -->https://github.com/brunobar79/J-I-C
对于 JPG 图像压缩,您可以使用称为 JIC(Javascript 图像压缩)的最佳压缩技术,这肯定会对您有所帮助 --> https://github.com/brunobar79/JIC
回答by abdurhman Nughmshi
i improved the function a head to be this :
我改进了功能 a head 是这样的:
var minifyImg = function(dataUrl,newWidth,imageType="image/jpeg",resolve,imageArguments=0.7){
var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
(new Promise(function(resolve){
image = new Image(); image.src = dataUrl;
log(image);
resolve('Done : ');
})).then((d)=>{
oldWidth = image.width; oldHeight = image.height;
log([oldWidth,oldHeight]);
newHeight = Math.floor(oldHeight / oldWidth * newWidth);
log(d+' '+newHeight);
canvas = document.createElement("canvas");
canvas.width = newWidth; canvas.height = newHeight;
log(canvas);
ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, newWidth, newHeight);
//log(ctx);
newDataUrl = canvas.toDataURL(imageType, imageArguments);
resolve(newDataUrl);
});
};
the use of it :
它的使用:
minifyImg(<--DATAURL_HERE-->,<--new width-->,<--type like image/jpeg-->,(data)=>{
console.log(data); // the new DATAURL
});
enjoy ;)
请享用 ;)


