JavaScript 图像压缩和调整大小
上传和下载图片是现代 Web 应用中非常常见的功能,但在客户端和服务器之间交换文件很快就会成为一项高资源消耗的任务。我们还必须考虑到,大多数互联网流量来自移动设备,因此我们预计用户会上传用手机拍摄的照片。由于新型移动设备的摄像头分辨率不断提高,这些文件可能会非常大(> 10MB)。
在您的平台上共享图片意味着用户将照片上传到您的存储服务器,然后其他用户下载这些照片并以某种方式使用它们。与在数据库中存储新记录相比,这项任务需要更多的资源。我们可以预期在以下方面会有更高的成本:
- 上传带宽。
- 下载带宽。在典型的用例中,每上传一张图片都会有多次下载。
- 存储。照片和文件通常存储在磁盘或某些对象存储服务中。需要注意的是,一旦将照片保存到存储空间中,除非您应用了某些删除策略,否则必须在软件的整个生命周期内保持存储。因此,存储成本会随着时间的推移而增加,而带宽成本则取决于当前的使用情况。
由于新冠疫情的紧急状态,在2020年3月至6月期间,Nuvola已成为教师、学生和家长的主要中心。这种情况导致流量迅速增加,正如我们在之前的文章中讨论过的那样。此外,学校的需求也发生了变化,以适应远程学习。例如,学生需要将作业发送给老师,老师也需要将批改发回。到目前为止,此功能并不需要,因为这个过程是在教室里物理完成的。这个新功能显然意味着文件共享。在与客户交谈中,我们发现用户更喜欢在练习本上做作业,拍照并在平台上分享。这意味着大多数共享文件都是图像,因此,图像压缩的优势将非常巨大。
图片分享如何优化?
答案显而易见:图像压缩。但是,如果您的软件主要关注图像质量,那么这项技术可能并不适合您。一种常见的解决方案是服务器端压缩,以减少所需的下载带宽和存储空间。然而,这种方法会增加 CPU 周期,这意味着额外的成本,尽管这可能比下载带宽成本更低。
得益于现代浏览器 API,我们还可以在上传图片之前在客户端进行压缩,从而减少不必要的上传带宽。降低带宽也意味着更快的上传速度,因为压缩时间远小于通过网络上传大文件所需的时间。
Canvas、FileReader 和 Blob 等 HTML5 功能允许直接在浏览器中压缩图像,从而减少平台需要上传、存储和下载的字节数。
来自 MDN 的一些背景信息
Canvas API提供了一种通过 JavaScript 和 HTML canvas元素绘制图形的方法。它不仅可用于动画、游戏图形、数据可视化、照片处理和实时视频处理,还可用于其他用途。Canvas API 主要专注于 2D 图形。WebGL API 也使用该<canvas>
元素,用于绘制硬件加速的 2D 和 3D 图形。
FileReader对象允许 Web 应用异步读取用户计算机上存储的文件(或原始数据缓冲区)的内容,并使用 File 或 Blob 对象指定要读取的文件或数据。File 对象可以从用户使用输入元素选择文件时返回的 FileList 对象、拖放操作的 DataTransfer 对象或 HTMLCanvasElement 上的 mozGetAsFile() API 获取。
Blob对象表示一个 Blob,它是一个类似文件的对象,包含不可变的原始数据;它们可以作为文本或二进制数据读取,也可以转换为 ReadableStream 以便使用其方法处理数据。Blob 可以表示不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了 Blob 的功能并对其进行扩展以支持用户系统上的文件。
图像压缩步骤
<input>
使用type="file" 元素读取文件
const input = document.getElementById(‘input’);
input.onChange = function(ev) {
const file = ev.target.files\[0\];
// Use the file
};
- 使用文件数据创建 Blob 并使用createObjectURL获取其 URL
const blobURL = window.URL.createObjectURL(file)
- 创建一个辅助图像对象并使用 blob URL 作为源
const img = new Image()
img.src = blobURL
- 使用
onload
回调来处理图像
img.onload = function (ev) {
window.URL.revokeObjectURL(blobURL) // release memory
// Use the img
}
- 创建一个画布元素,设置宽度和高度以匹配图像的新尺寸。
const canvas = document.createElement(‘canvas’);
canvas.width = newWidth;
canvas.height = newHeight;
- 创建 2D 上下文对象并在画布上绘制图像
const ctx = canvas.getContext(‘2d’);
ctx.drawImage(img, 0, 0, newWidth, newHeight);
- 以所需的输出质量导出图像
canvas.toBlob(
function (blob) {
// Handle the compressed image
},
mimeType,
quality
)
mimeType
是结果图像的MIME 类型,例如image/jpeg、image/png。其值的范围quality
是 0 到 1,表示输出图像的质量。如果您未在toBlob()
方法中指定 MIME 和质量,则将设置默认质量,MIME 类型将为image/png 。并非所有浏览器都完全支持HTMLCanvasElement.toBlob ,请参阅下面的 polyfill 部分。
- (可选)在文档中显示压缩图像
document.body.append(canvas)
填充 canvas.toBlob
基于 toDataURL 的低性能 polyfill。
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" }));
},
});
}
}
来源:MDN
最终代码
尝试Codepen上的 JS 图像调整大小工具。
<div id="root">
<p>Upload an image and see the result</p>
<input id="img-input" type="file" accept="image/*" style="display:block" />
</div>
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]
}