如何通过浏览器中的 Javascript 压缩图像?

;

有没有一种方法可以直接在浏览器端压缩图像(主要是 jpeg、 png 和 gif) ,然后再上传?我非常肯定 JavaScript 可以做到这一点,但是我找不到实现它的方法。


下面是我想要实现的完整场景:

  • 用户去我的网站,并通过 input type="file"元素选择一个图像,
  • 该图像是通过 JavaScript 检索的,我们做了一些验证,如正确的文件格式,最大文件大小等,
  • 如果一切正常,图像的预览就会显示在页面上,
  • 用户可以进行一些基本操作,如旋转图像90 °/-90 ° ,按照预定义的比例裁剪图像等,或者用户可以上传另一张图像并返回到步骤1,
  • 当用户满意时,编辑后的图像将被压缩并在本地“保存”(不是保存到文件中,而是在浏览器内存/页面中) ,-
  • 用户在表单中填写姓名、年龄等数据,
  • 用户点击“完成”按钮,然后包含数据 + 压缩图像的表单被发送到服务器(没有 AJAX) ,

整个过程直到最后一步应该做客户端,并应兼容最新的 Chrome 和 Firefox,Safari 5 + 和 IE8 + 。如果可能的话,应该只使用 JavaScript (但我很确定这是不可能的)。

我现在还没有编写任何代码,但我已经想过了。通过 文件 API可以本地读取文件,图像预览和编辑可以使用 画布元素,但 我找不到图像压缩

根据 Html5please. comCaniuse.com,支持这些浏览器是相当困难的(感谢 IE) ,但可以做到使用多填充,如 FlashCanvas文件阅读器

实际上,我们的目标是减少文件大小,所以我认为图像压缩是一个解决方案。但是,我知道上传的图片将在我的网站上显示,每次在同一个地方,我知道这个显示区域的尺寸(例如200x400)。因此,我可以调整图像的大小,以适应这些尺寸,从而减少文件大小。我不知道这项技术的压缩比。

你觉得怎么样?你有什么建议要告诉我吗?你知道用 JavaScript 压缩图像浏览器端的方法吗?谢谢你的回复。

198816 次浏览

简而言之:

  • 使用带有.readAsArrayBuffer 的 HTML5FileReaderAPI 读取文件
  • 使用文件数据创建一个 Blob,并使用 CreateObjectURL (blob)获取它的 URL
  • 创建新的 Image 元素并将其 src 设置为文件 blob URL
  • 将图像发送到画布。画布大小设置为所需的输出大小
  • 通过虚拟画布.toDataURL (“ image/jpeg”,0.7)(设置您自己的输出格式和质量)从画布中获取缩小后的数据
  • 将新的隐藏输入附加到原始表单,并将 dataURI 图像基本上作为普通文本传输
  • 在后端,读取 dataURI,从 Base64解码并保存它

资料来源: 密码

编辑: 根据“ Me 先生”对这个答案的评论,看起来压缩现在可用于 JPG/WebP 格式(参见 https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL)。

据我所知,您不能使用画布压缩图像,相反,您可以调整它的大小。ToDataURL 不允许您选择要使用的压缩比。你可以看一下你想要的画面: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

事实上,通常只要调整图像的大小就足够了,但是如果你想要更进一步,你必须使用新引入的方法 file.readAsArrayBuffer 来获得一个包含图像数据的缓冲区。

然后,根据图像格式规范(http://en.wikipedia.org/wiki/JPEGhttp://en.wikipedia.org/wiki/Portable_Network_Graphics)使用 DataView 读取它的内容。

处理图像数据压缩会很困难,但尝试一下会更糟。另一方面,您可以尝试删除 PNG 头或 JPEG 的 exif 数据,使您的图像更小,这应该更容易做到这一点。

您必须在另一个缓冲区上创建另一个 DataWiew,并用经过过滤的图像内容填充它。然后,只需要使用 window.btoa 将图像内容编码到 DataURI。

让我知道,如果你实现了类似的东西,将有趣的通过代码。

@ 心理森林的回答很好。我想提供我自己的解决方案。这个 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;
}

这个代码可以用在任何你有一个数据 URL 并且想要一个缩放图像的数据 URL 的地方。

对于 JPG 图像压缩,你可以使用最好的压缩技术—— JIC (Javascript 图像压缩)这肯定会对你有所帮助—— > https://github.com/brunobar79/J-I-C

我从其他答案中看到了两个缺失的东西:

  • canvas.toBlob(如果有的话)比 canvas.toDataURL性能更好,也是异步的。
  • 文件-> 图像-> 画布-> 文件转换丢失 EXIF 数据; 特别是关于图像旋转的数据,这些数据通常由现代手机/平板电脑设置。

下面的脚本涉及这两点:

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

示例用法:

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

我在上面@daniel-allen-langdon 发布的 downscaleImage()函数中遇到了一个问题,因为图像加载是 异步的,所以 image.widthimage.height属性不能立即使用。

请参阅下面更新的 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;
}


}

你可以看看 图像转换,试试这里—— > 演示页面

enter image description here

我把功能改进成这样:

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

它的使用:

minifyImg(<--DATAURL_HERE-->,<--new width-->,<--type like image/jpeg-->,(data)=>{
console.log(data); // the new DATAURL
});

享受;)

我发现有比公认的答案更简单的解决方案。

  • 使用带 .readAsArrayBuffer的 HTML5FileReaderAPI 读取文件
  • 使用文件数据创建一个 Blob,并使用 window.URL.createObjectURL(blob)获取它的 URL
  • 创建新的 Image 元素并将其 src 设置为文件 blob URL
  • 将图像发送到画布。画布大小设置为所需的输出大小
  • 从画布 通过 canvas.toDataURL("image/jpeg",0.7)(设置自己的输出格式和质量)获取缩小后的数据
  • 将新的隐藏输入附加到原始表单 并将 dataURI 图像基本上作为普通文本传输
  • 在后端,读取 dataURI,从 Base64解码,然后 保存它

根据你的问题:

有没有压缩图像的方法(主要是 jpeg、 png 和 gif) 直接浏览器端,然后上传

我的解决办法是:

  1. URL.createObjectURL(inputFileElement.files[0])直接创建带有文件的 blob。

  2. 和接受的答案一样。

  3. 和接受的答案一样。值得一提的是,画布大小是必要的,并使用 img.widthimg.height来设置 canvas.widthcanvas.height。不是 img.clientWidth

  4. 通过 canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5)获得缩小后的图像。设置 'image/jpg'没有效果。也支持 image/png。使用 let compressedImageBlob = new File([blob])callbackfunction 尸体中创建一个新的 File对象。

  5. 添加新的隐藏输入或 通过 javascript 发送.Server 不必解码任何东西。

检查 https://javascript.info/binary的所有信息。我想出了解决方案后,阅读这一章。


密码:

    <!DOCTYPE html>
<html>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="fileToUpload" id="fileToUpload" multiple>
<input type="submit" value="Upload Image" name="submit">
</form>
</body>
</html>

这个代码看起来没有其他答案那么可怕。

更新:

必须把所有东西都放在 img.onload里面。否则,canvas将无法得到图像的宽度和高度正确的时间 canvas分配。

    function upload(){
var f = fileToUpload.files[0];
var fileName = f.name.split('.')[0];
var img = new Image();
img.src = URL.createObjectURL(f);
img.onload = function(){
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(function(blob){
console.info(blob.size);
var f2 = new File([blob], fileName + ".jpeg");
var xhr = new XMLHttpRequest();
var form = new FormData();
form.append("fileToUpload", f2);
xhr.open("POST", "upload.php");
xhr.send(form);
}, 'image/jpeg', 0.5);
}
}

设置 image/jpeg参数的 3.4MB .png文件压缩测试。

    |0.9| 777KB |
|0.8| 383KB |
|0.7| 301KB |
|0.6| 251KB |
|0.5| 219kB |

压缩机

Https://github.com/fengyuanchen/compressorjs

import axios from 'axios';
import Compressor from 'compressorjs';


document.getElementById('file').addEventListener('change', (e) => {
const file = e.target.files[0];


if (!file) {
return;
}


new Compressor(file, {
quality: 0.6,


// The compression process is asynchronous,
// which means you have to access the `result` in the `success` hook function.
success(result) {
const formData = new FormData();


// The third parameter is required for server
formData.append('file', result, result.name);


// Send the compressed image file to server with XMLHttpRequest.
axios.post('/path/to/upload', formData).then(() => {
console.log('Upload success');
});
},
error(err) {
console.log(err.message);
},
});
});

试试这个可定制的纯 JS 样本-压缩超过90% :

   <div id="root">
<p>Upload an image and see the result</p>
<input id="img-input" type="file" accept="image/*" style="display:block" />
</div>


<script>
const MAX_WIDTH = 320;
const MAX_HEIGHT = 180;
const MIME_TYPE = "image/jpeg";
const QUALITY = 0.7;


const input = document.getElementById("img-input");
input.onchange = function (ev) {
const file = ev.target.files[0]; // get the file
const blobURL = URL.createObjectURL(file);
const img = new Image();
img.src = blobURL;
img.onerror = function () {
URL.revokeObjectURL(this.src);
// Handle the failure properly
console.log("Cannot load image");
};
img.onload = function () {
URL.revokeObjectURL(this.src);
const [newWidth, newHeight] = calculateSize(img, MAX_WIDTH, MAX_HEIGHT);
const canvas = document.createElement("canvas");
canvas.width = newWidth;
canvas.height = newHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, newWidth, newHeight);
canvas.toBlob(
(blob) => {
// Handle the compressed image. es. upload or save in local state
displayInfo('Original file', file);
displayInfo('Compressed file', blob);
},
MIME_TYPE,
QUALITY
);
document.getElementById("root").append(canvas);
};
};


function calculateSize(img, maxWidth, maxHeight) {
let width = img.width;
let height = img.height;


// calculate the width and height, constraining the proportions
if (width > height) {
if (width > maxWidth) {
height = Math.round((height * maxWidth) / width);
width = maxWidth;
}
} else {
if (height > maxHeight) {
width = Math.round((width * maxHeight) / height);
height = maxHeight;
}
}
return [width, height];
}


// Utility functions for demo purpose


function displayInfo(label, file) {
const p = document.createElement('p');
p.innerText = `${label} - ${readableBytes(file.size)}`;
document.getElementById('root').append(p);
}


function readableBytes(bytes) {
const i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];


return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
}
</script>

我使用了以下软件包: Https://www.npmjs.com/package/browser-image-compression

npm install browser-image-compression
or
yarn add browser-image-compression

那就跟着医生走:

import imageCompression from 'browser-image-compression';
const options = {
maxSizeMB: 0.5, // pretty much self-explanatory
maxWidthOrHeight: 500, // apparently px
}


imageCompression(file, options)
.then(function(compressedFile) {
console.log(
"compressedFile instanceof Blob",
compressedFile instanceof Blob
); // true
console.log(
`compressedFile size ${compressedFile.size /
1024 /
1024} MB`
); // smaller than maxSizeMB


return uploader(compressedFile); // code to actual upload, in my case uploader() is a function to upload to Firebase storage.
})

如果你对 uploader()感兴趣的话,以下是它的代码:

import { initializeApp } from "firebase/app";


const firebaseConfig = {
// your config
};


initializeApp(firebaseConfig);
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
const storage = getStorage();
const sRef = ref(storage);


const uploader = async (file) => {
/* uploads to root */


// const imageRef = ref(sRef, file.name);
// console.log(imageRef);
// await uploadBytes(imageRef, file).then((snapshot) => {
//  console.log("Uploaded a blob or file!", snapshot);
// });


/* upload to folder 'techs/' */
const folderRef = ref(sRef, "techs/" + file.name);
await uploadBytes(folderRef, file);


// get URL
const url = await getDownloadURL(ref(storage, folderRef));


console.log("url: ", url);
return url;
};


可以使用 HTML <canvas>元素压缩图像:

function compressImage(imgToCompress, resizingFactor, quality) {
// resizing the image
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
  

const originalWidth = imgToCompress.width;
const originalHeight = imgToCompress.height;
  

const canvasWidth = originalWidth * resizingFactor;
const canvasHeight = originalHeight * resizingFactor;
  

canvas.width = canvasWidth;
canvas.height = canvasHeight;
  

context.drawImage(
imgToCompress,
0,
0,
originalWidth * resizingFactor,
originalHeight * resizingFactor
);
  

// reducing the quality of the image
canvas.toBlob(
(blob) => {
if (blob) {
// showing the compressed image
resizedImage.src = URL.createObjectURL(resizedImageBlob);
}
},
"image/jpeg",
quality
);
}

有关详细解释,请参阅这篇博客文章: https://img.ly/blog/how-to-compress-an-image-before-uploading-it-in-javascript/

对于现代浏览器,使用 createImageBitmap()代替 img.onload

async function compressImage(blobImg, percent) {
let bitmap = await createImageBitmap(blobImg);
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
ctx.drawImage(bitmap, 0, 0);
let dataUrl = canvas.toDataURL("image/jpeg", percent/100);
return dataUrl;
}


inputImg.addEventListener('change', async(e) => {
let img = e.target.files[0];
console.log('File Name: ', img.name)
console.log('Original Size: ', img.size.toLocaleString())
  

let imgCompressed = await compressImage(img, 75) // set to 75%
let compSize = atob(imgCompressed.split(",")[1]).length;
console.log('Compressed Size: ', compSize.toLocaleString())
//console.log(imgCompressed)
})
<input type="file" id="inputImg">