js压缩图片 到固定像素以内,500k为例 您所在的位置:网站首页 美股gild js压缩图片 到固定像素以内,500k为例

js压缩图片 到固定像素以内,500k为例

2024-04-13 04:45| 来源: 网络整理| 查看: 265

本文旨在探究js压缩图片的两种方式:改变图片长宽,改变图片质量,和结合了以上两者的最终方案。

首先,阅读本文需要知道canvas的两个方法

drawImage(image, x, y, width, height) 这个方法多了2个参数:width 和 height,这两个参数用来控制 当向canvas画入时应该缩放的大小

canvas.toDataURL(type, encoderOptions); type 可选 图片格式,默认为 image/png encoderOptions 可选 在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。

这两个方法具体的说明可以在MDN上查看,关于图片压缩,也有很多现成的博客可以直接用。但是那些博客都有个问题,并没有关心之后图片的压缩质量。 我试着用一个现成的例子去跑了一下,一个1.7M的图片压缩到了23k,堪称像素级毁灭性破坏。

改变图片长宽实现压缩目的

假如一张大图可能包含着很多文字等关键信息,必须上传之后使用方能清晰辨认。所以要压缩之后质量尽可能接近500k的。500k像素以内,就是若一张图宽度为1024,则高度不能超过500。因为图片有其他的信息,也是要占大小的。即不得大于1024*500。

1024*576在不控制图片质量的情况下并不是576kb

所以,根据需求,上传图片不能超过500k的情况下尽可能保留图片的清晰度。当然如果可以的情况下用上面提到的canvas.toDataURL设置压缩程度为0.9,0.8试试看,图片质量可以接受,大小会有大幅度的缩小。

设为0.9之后的图片大幅缩小了

如果不压缩,靠调整图片长宽去控制上传大小呢? 原理很简单,就是靠不断地缩小限定的最大宽高,直到最终长宽的积小于规定的大小。 这种方法有可能最后得出的图片的大小会略大于规定大小,原因上文也提到过了,如果想使用这种方法,可自行再调整一下。

// 实际的压缩在imgResize里 export default function getUploadImgInfo(e) { /** * 获取最终图片上传的所需入参:{ blob:图片文件, fileExtension:图片扩展名, fileType:图片类型, imgBase64:图片前端展示的base64 } * { blob, fileExtension, fileType, imgBase64} */ let fileInfo = getFileInfo(e); if (!fileInfo) return Promise.reject(); let { fileExtension, fileType, file } = fileInfo; return imgResize({ file, fileType }).then(res => { return convertBlob({ imgBase64: res, fileType, fileExtension }); }); } // 对图片进行压缩 function imgResize({ file, maxSizeNum = 500, fileType }) { let fileReader = new FileReader(); return new Promise((resolve, reject) => { let maxSize = 1 * 1024 * 1024 * (maxSizeNum / 1024); // 最大默认500k 即若规定最大宽度为1024,则高度不能超过500 let maxLength = 1024; if (file.size > maxSize) { fileReader.onload = function(e) { let IMG = new Image(); IMG.onload = function() { let reMaxLength = maxLength; let originW = this.naturalWidth; let originH = this.naturalHeight; let canvas = document.createElement('canvas'); let ctx = canvas.getContext('2d'); let level = 1.0; // level是图片质量 let { resizeW, resizeH } = getImgResize( originW, originH, maxLength ); let count = 0; while (resizeH * resizeW >= maxSize) { reMaxLength = reMaxLength - 100; let obj = getImgResize(originW, originH, reMaxLength); resizeH = obj.resizeH; resizeW = obj.resizeW; console.log('rr:', resizeH, resizeW, count++); } if (window.navigator.userAgent.indexOf('iPhone') > 0) { canvas.width = resizeH; canvas.height = resizeW; ctx.rotate((90 * Math.PI) / 180); ctx.drawImage(IMG, 0, -resizeH, resizeW, resizeH); } else { canvas.width = resizeW; canvas.height = resizeH; ctx.drawImage(IMG, 0, 0, resizeW, resizeH); } let base64 = canvas.toDataURL('image/' + fileType, level); resolve(base64); }; IMG.src = e.target.result; }; } else { fileReader.onload = function() { resolve(fileReader.result); }; } fileReader.readAsDataURL(file); }); } function getImgResize(originW, originH, maxLength = 1024) { // 获得图片调整大小之后的长宽;maxLength为长宽最大的长度 let resizeH = 0; let resizeW = 0; if (originW > maxLength || originH > maxLength) { let multiple = Math.max(originW / maxLength, originH / maxLength); resizeW = originW / multiple; resizeH = originH / multiple; } else { resizeW = originW; resizeH = originH; } return { resizeH, resizeW }; } function getFileInfo(e) { if (e.target.value == '') { return false; } let files = e.target.files || e.dataTransfer.files; if (!files.length) return; let file = files[0]; return { fileExtension: file.name.match(/[\w]*$/) && file.name.match(/[\w]*$/)[0], fileType: file.type.split('/')[1], file: files[0] }; } function convertBlob({ imgBase64, fileType, fileExtension }) { // 获得blob,用于最终上传的图片入参 if (!imgBase64) { this.$toast('请选择图片'); return; } let base64 = window.atob(imgBase64.split(',')[1]); let buffer = new ArrayBuffer(base64.length); let ubuffer = new Uint8Array(buffer); for (let i = 0; i < base64.length; i++) { ubuffer[i] = base64.charCodeAt(i); } let blob; try { blob = new Blob([buffer], { type: 'image/' + fileType }); } catch (e) { window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; if (e.name === 'TypeError' && window.BlobBuilder) { let blobBuilder = new BlobBuilder(); blobBuilder.append(buffer); blob = blobBuilder.getBlob('image/' + fileType); } } return Promise.resolve({ blob, fileExtension, fileType, imgBase64 }); } 改变图片清晰度实现压缩目的

上面的方法有个问题,就是改变了图片的原始长宽。如果一个图的长宽足够大,压缩图片质量,糊一点但是内容看得清也是ok的嘛。所以,跟上面同理,我们可以不断调整图片的质量设定直到大小合适,那么,如何在图片上传之前知道图片的大小呢? 首先,需要知道的一点是,压缩之后拿到的base64字符串会转成blob对象,然后传给服务端。 可以查阅文档,blob对象有个属性是size

返回一个File对象所指代的文件的大小,单位为字节。

这个size就是上传之后实际的文件大小。 参照上面的思路,可以每次改变canvas.toDataURL('image/' + fileType, level);level的值,去调整压缩图片质量,然后用blob对象的size去验证是否满足500k以内的需求。 关于 canvas.toDataURL的level到底是怎么计算的,MDN文档里也没说,写了个循环一次减少0.1的level压缩了几个图片

726625字节

1738594字节 1615259 byte

用加减乘除算了一下,没找到规律,数学不好放弃了(这个东西好像也不是能观察出来的,看结果跟初始大小没啥关系)。 这里要注意的是,有可能遇到超大图片,0.1的level可能不足以压缩到500k,所以小于0.1的时候,改变level递减的差值继续压缩下去

if (level { console.log('level', level); fileRes = await imgResize({ file, fileType, level, maxSize }); finalRes = await convertBlob({ imgBase64: fileRes, fileType, fileExtension }); }; await task(); while (finalRes.blob.size > maxSize) { // 如果文件大小不合适,则一直压缩到500k以内 if (level { fileSize = file.size; if (file.size > maxSize) { fileReader.onload = function(e) { let IMG = new Image(); IMG.onload = function() { let originW = this.naturalWidth; let originH = this.naturalHeight; let resizeH = originH; let resizeW = originW; let canvas = document.createElement('canvas'); let ctx = canvas.getContext('2d'); if (window.navigator.userAgent.indexOf('iPhone') > 0) { canvas.width = resizeH; canvas.height = resizeW; ctx.rotate((90 * Math.PI) / 180); ctx.drawImage(IMG, 0, -resizeH, resizeW, resizeH); } else { canvas.width = resizeW; canvas.height = resizeH; ctx.drawImage(IMG, 0, 0, resizeW, resizeH); } let base64 = canvas.toDataURL('image/' + fileType, level); resolve(base64); }; IMG.src = e.target.result; }; } else { fileReader.onload = function() { resolve(fileReader.result); }; } fileReader.readAsDataURL(file); }); } function convertBlob({ imgBase64, fileType, fileExtension }) { // 获得blob,用于最终上传的图片入参 if (!imgBase64) { this.$toast('请选择图片'); return; } let base64 = window.atob(imgBase64.split(',')[1]); let buffer = new ArrayBuffer(base64.length); let ubuffer = new Uint8Array(buffer); for (let i = 0; i < base64.length; i++) { ubuffer[i] = base64.charCodeAt(i); } let blob; try { blob = new Blob([buffer], { type: 'image/' + fileType }); } catch (e) { window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; if (e.name === 'TypeError' && window.BlobBuilder) { let blobBuilder = new BlobBuilder(); blobBuilder.append(buffer); blob = blobBuilder.getBlob('image/' + fileType); } } console.log( 'count:', count++, '占比:', blob.size / fileSize, 'final:', blob.size, blob.size / 1024, 'begin', fileSize, fileSize / 1024 ); return Promise.resolve({ blob, fileExtension, fileType, imgBase64 }); } let count = 1; 综合方案

其实单纯的压缩质量遇到稍大的图片,会导致页面高频计算,然后页面基本就用不了了- -。有尝试过用iphone的一个屏幕截图(10M左右),压的时候稍过一会,整个手机都在发烫,只能杀进程。

所以,若对长度没有特殊的限制,可以做一个缩放,去加快压缩的进度,提高能压缩的图片大小上限。

综合方案压缩10.8M图片

页面到了ios上还是不行- -,可以看到最后图片level为0.001,最长边为764。 问题还是循环次数还是过多,计算频率太高。从图中可看出,对于大图来说,初始设定的level和图片尺寸过于宽松,可以优化一下初始level和尺寸。

if (finalRes.blob.size > maxSize) { // 设定一个合适的初始level level = orginLevel(finalRes.blob.size, maxSize); if (level { console.log('test|| ', 'level:', level, 'maxLength:', maxLength); fileRes = await imgResize({ file, fileType, level, maxSize, maxLength }); finalRes = await convertBlob({ imgBase64: fileRes, fileType, fileExtension }); }; await task(); if (finalRes.blob.size > maxSize) { // 设定一个合适的level,避免压缩的时候太多 level = orginLevel(finalRes.blob.size, maxSize); if (level maxSize) { // 如果文件大小不合适,则一直压缩到500k以内 if (maxLength > minLength) { // 控制图片最大边的长度 maxLength = maxLength - 10; } if (level maxLength || originH > maxLength) { let multiple = Math.max(originW / maxLength, originH / maxLength); resizeW = originW / multiple; resizeH = originH / multiple; } else { resizeW = originW; resizeH = originH; } return { resizeH, resizeW }; } export function getImgBase64(e) { // 把文件转成base64--预览目的 let fileInfo = getFileInfo(e); let { fileType, file } = fileInfo; return imgResize({ file, maxSize: 500 * 1024, fileType, level: 0.6, maxLength: 500 }); } function getFileInfo(e) { if (e.target.value == '') { return false; } let files = e.target.files || e.dataTransfer.files; if (!files.length) return; let file = files[0]; return { fileExtension: file.name.match(/[\w]*$/) && file.name.match(/[\w]*$/)[0], fileType: file.type.split('/')[1], file: files[0] }; } // 对图片进行压缩 function imgResize({ file, maxSize = 500 * 1024, fileType, level = 1.0, maxLength = 1024 }) { let fileReader = new FileReader(); return new Promise((resolve, reject) => { fileSize = file.size; if (file.size > maxSize) { fileReader.onload = function(e) { let IMG = new Image(); IMG.onload = function() { let originW = this.naturalWidth; let originH = this.naturalHeight; let canvas = document.createElement('canvas'); let ctx = canvas.getContext('2d'); let { resizeW, resizeH } = getImgResize( originW, originH, maxLength ); if (window.navigator.userAgent.indexOf('iPhone') > 0) { canvas.width = resizeH; canvas.height = resizeW; ctx.rotate((90 * Math.PI) / 180); ctx.drawImage(IMG, 0, -resizeH, resizeW, resizeH); } else { canvas.width = resizeW; canvas.height = resizeH; ctx.drawImage(IMG, 0, 0, resizeW, resizeH); } let base64 = canvas.toDataURL('image/' + fileType, level); resolve(base64); }; IMG.src = e.target.result; }; } else { fileReader.onload = function() { resolve(fileReader.result); }; } fileReader.readAsDataURL(file); }); } function convertBlob({ imgBase64, fileType, fileExtension }) { // 获得blob,用于最终上传的图片入参 if (!imgBase64) { this.$toast('请选择图片'); return; } let base64 = window.atob(imgBase64.split(',')[1]); let buffer = new ArrayBuffer(base64.length); let ubuffer = new Uint8Array(buffer); for (let i = 0; i < base64.length; i++) { ubuffer[i] = base64.charCodeAt(i); } let blob; try { blob = new Blob([buffer], { type: 'image/' + fileType }); } catch (e) { window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; if (e.name === 'TypeError' && window.BlobBuilder) { let blobBuilder = new BlobBuilder(); blobBuilder.append(buffer); blob = blobBuilder.getBlob('image/' + fileType); } } if (finalSize === blob.size) { return Promise.reject( new Error('压缩该文件两次的结果相同,压缩无效!') ); } else { finalSize = blob.size; } console.log( 'test|| ', 'count:', count++, '占比:', blob.size / fileSize, 'final:', blob.size, blob.size / 1024, 'begin', fileSize, fileSize / 1024 ); return Promise.resolve({ blob, fileExtension, fileType, imgBase64 }); } 优化方案

解决的隐患:上面这个方案会出现我需要一个500k的照片,压到了520k之后,再压了一次。有时候这最后的一次会特别夸张,直接将图片弄到了几十k。 参考了:https://github.com/WangYuLue/image-conversion 这个库里面有个方法compressAccurately,这个方法可以比较精准地压缩。偷偷翻了一下源码。

async function compressAccurately(file: Blob, config: compressAccuratelyConfig = {}): Promise { if (!(file instanceof Blob)) { throw new Error('compressAccurately(): First arg must be a Blob object or a File object.'); } if (typeof config !== 'object') { config = Object.assign({ size: config, }); } // 如果指定体积不是数字或者数字字符串,则不做处理 config.size = Number(config.size); if (Number.isNaN(config.size)) { return file; } // 如果指定体积大于原文件体积,则不做处理; if (config.size * 1024 > file.size) { return file; } config.accuracy = Number(config.accuracy); if (!config.accuracy || config.accuracy < 0.8 || config.accuracy > 0.99) { config.accuracy = 0.95; // 默认精度0.95 } const resultSize = { max: config.size * (2 - config.accuracy) * 1024, accurate: config.size * 1024, min: config.size * config.accuracy * 1024, }; const dataURL = await filetoDataURL(file); let originalMime = dataURL.split(',')[0].match(/:(.*?);/)[1] as EImageType; // 原始图像图片类型 let mime = EImageType.JPEG; if (checkImageType(config.type)) { mime = config.type; originalMime = config.type; } const image = await dataURLtoImage(dataURL); const canvas = await imagetoCanvas(image, Object.assign({}, config)); /** * 经过测试发现,blob.size与dataURL.length的比值约等于0.75 * 这个比值可以同过dataURLtoFile这个方法来测试验证 * 这里为了提高性能,直接通过这个比值来计算出blob.size */ const proportion = 0.75; let imageQuality = 0.5; let compressDataURL; const tempDataURLs: string[] = [null, null]; /** * HTMLCanvasElement.toBlob()以及HTMLCanvasElement.toDataURL()压缩参数 * 的最小细粒度为0.01,而2的7次方为128,即只要循环7次,则会覆盖所有可能性 */ for (let x = 1; x CalculationSize) { compressDataURL = [compressDataURL, ...tempDataURLs] .filter(i => i) // 去除null .sort((a, b) => Math.abs(a.length * proportion - resultSize.accurate) - Math.abs(b.length * proportion - resultSize.accurate))[0]; } break; } if (resultSize.max < CalculationSize) { tempDataURLs[1] = compressDataURL; imageQuality -= 0.5 ** (x + 1); } else if (resultSize.min > CalculationSize) { tempDataURLs[0] = compressDataURL; imageQuality += 0.5 ** (x + 1); } else { break; } } const compressFile = await dataURLtoFile(compressDataURL, originalMime); // 如果压缩后体积大于原文件体积,则返回源文件; if (compressFile.size > file.size) { return file; } return compressFile; };

其实上一个方案的痛点就在于,如何在每一个压缩循环里处理尺寸和压缩比例。

尺寸 在imagetoCanvas里,修改了图片的宽和高如下 width = myConfig.width || myConfig.height * image.width / image.height || image.width; height = myConfig.height || myConfig.width * image.height / image.width || image.height; 压缩比例 然后看这个循环 for (let x = 1; x CalculationSize) { compressDataURL = [compressDataURL, ...tempDataURLs] .filter(i => i) // 去除null .sort((a, b) => Math.abs(a.length * proportion - resultSize.accurate) - Math.abs(b.length * proportion - resultSize.accurate))[0]; } break; } if (resultSize.max < CalculationSize) { tempDataURLs[1] = compressDataURL; imageQuality -= 0.5 ** (x + 1); } else if (resultSize.min > CalculationSize) { tempDataURLs[0] = compressDataURL; imageQuality += 0.5 ** (x + 1); } else { break; } }

总结

根据所要求的最终图片大小,得出误差范围; 在误差范围内比对压缩过后的图片大小,调整下一次压缩的质量; 初始化压缩比例为0.5,单次调整的质量尺度为±(1/2) ^ (x + 1) 。总循环次数为7次; pass:总循环为7次的原因是压缩比例最小细粒度为0.01,因为1/(2^8) 约等于0.0078,当压缩比例从1/2开始对(1/2^2) 0.25 这种值进行相加减将会趋近于最接近的压缩比例。 若循环的确到了第七次,则不一定是最准的,可以找前面的最接近的结果来作为最终计算结果。

如有纰漏,欢迎指正



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有