如何用纯JavaScript和HTML5压缩缩放Canvas图像?相机图片旋转偏移排查
嘿,我来帮你搞定这两个纯JS + HTML5 Canvas的问题,都是客户端就能实现的,不用额外依赖库~
核心思路就是用Canvas的drawImage做缩放,再通过toBlob或toDataURL导出压缩后的结果,步骤很清晰:
第一步:加载图像
不管是本地上传的文件还是网络图片,都用Image对象加载。本地文件用URL.createObjectURL生成临时URL;跨域图片记得给Image加crossOrigin="anonymous"(前提是服务器允许跨域)。第二步:计算等比例缩放尺寸
为了避免图像拉伸,要按原图宽高比计算最终尺寸。比如设定最大宽高,取宽高比的最小值来缩放。第三步:绘制到Canvas并导出
创建Canvas并设置好缩放后的宽高,用drawImage把原图缩放到Canvas上,最后用toBlob(适合大文件,内存占用低)或toDataURL(适合小图,直接生成base64)导出,第二个参数是压缩质量(0到1,1为无损)。
给你一个可复用的完整函数:
function compressAndScaleImage(file, maxWidth = 800, maxHeight = 800, quality = 0.8) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = function() { // 计算等比例缩放后的尺寸 let width = img.width; let height = img.height; if (width > maxWidth || height > maxHeight) { const scaleRatio = Math.min(maxWidth / width, maxHeight / height); width = Math.round(width * scaleRatio); height = Math.round(height * scaleRatio); } // 创建Canvas并绘制图像 const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); // 导出为Blob(推荐用于大文件上传) canvas.toBlob((blob) => { URL.revokeObjectURL(img.src); // 释放临时URL内存 resolve(blob); }, 'image/jpeg', quality); // 如果需要base64格式,替换成下面的代码: // const dataURL = canvas.toDataURL('image/jpeg', quality); // URL.revokeObjectURL(img.src); // resolve(dataURL); }; img.onerror = (err) => { reject(err); URL.revokeObjectURL(img.src); }; img.src = URL.createObjectURL(file); }); } // 使用示例:监听文件上传 document.querySelector('#imageUpload').addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; try { const compressedBlob = await compressAndScaleImage(file); // 预览压缩后的图片 const previewUrl = URL.createObjectURL(compressedBlob); const previewImg = document.createElement('img'); previewImg.src = previewUrl; document.body.appendChild(previewImg); // 这里也可以把Blob传给FormData上传到服务器 } catch (err) { console.error('压缩失败:', err); } });
你遇到的320像素偏移,大概率是Canvas旋转时的坐标变换没处理对——默认旋转是围绕(0,0)点的,而图像中心不在原点,旋转后就会“跑出去”。下面给你正确的处理思路和代码:
问题根源
直接旋转Canvas而不调整坐标系时,图像会以左上角为中心旋转,导致部分内容超出Canvas范围,看起来像是偏移;另外,旋转90/270度时图像宽高会互换,如果Canvas尺寸没跟着修改,也会出现裁剪或偏移。
正确的旋转步骤
- 根据旋转角度调整Canvas尺寸:比如旋转90度,Canvas的宽要改成原图的高,高改成原图的宽。
- 平移坐标系到Canvas中心:把旋转中心移到Canvas中心点,确保图像旋转后居中。
- 旋转画布,再平移回图像中心:让图像绘制在旋转后的坐标系里不会偏移。
给你一个靠谱的旋转函数:
function rotateImage(img, rotationAngle) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); let canvasWidth = img.width; let canvasHeight = img.height; // 旋转90/270度时,宽高互换 if (rotationAngle === 90 || rotationAngle === 270) { [canvasWidth, canvasHeight] = [canvasHeight, canvasWidth]; } canvas.width = canvasWidth; canvas.height = canvasHeight; // 保存当前画布状态,避免影响后续绘制 ctx.save(); // 1. 把坐标系原点移到Canvas中心 ctx.translate(canvasWidth / 2, canvasHeight / 2); // 2. 旋转画布(注意要转成弧度) ctx.rotate((rotationAngle * Math.PI) / 180); // 3. 把坐标系移回图像的左上角,让图像中心和Canvas中心重合 ctx.translate(-img.width / 2, -img.height / 2); // 绘制图像 ctx.drawImage(img, 0, 0); // 恢复画布初始状态 ctx.restore(); return canvas; } // 使用示例 const cameraImg = new Image(); cameraImg.src = 'your-camera-image-url.jpg'; // 或者来自相机的File对象生成的URL cameraImg.onload = function() { // 比如旋转90度 const rotatedCanvas = rotateImage(cameraImg, 90); document.body.appendChild(rotatedCanvas); };
额外注意:相机图片的EXIF旋转信息
很多手机拍摄的图片,实际像素是横向的,但EXIF里标记了旋转角度(比如竖着拍的照片,EXIF会标记旋转90度)。如果忽略这个,旋转后还是会偏移或方向错误。可以用纯JS解析EXIF的方向标记:
async function getImageOrientation(file) { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { const view = new DataView(e.target.result); // 检查是否是JPEG if (view.getUint16(0, false) !== 0xFFD8) { resolve(1); // 默认方向 return; } let offset = 2; const length = view.byteLength; while (offset < length) { const marker = view.getUint16(offset, false); offset += 2; if (marker === 0xFFE1) { // EXIF标记 const exifLength = view.getUint16(offset, false); offset += 2; // 检查是否是EXIF数据 if (view.getUint32(offset, false) !== 0x45786966) { resolve(1); return; } offset += 6; const littleEndian = view.getUint16(offset, false) === 0x4949; offset += 2; const tagOffset = view.getUint32(offset + 4, littleEndian); offset += tagOffset; // 查找方向标记(0x0112) const orientationTag = view.getUint16(offset, littleEndian); offset += 2; if (orientationTag === 0x0112) { const orientation = view.getUint16(offset + 8, littleEndian); resolve(orientation); return; } } else if ((marker & 0xFF00) !== 0xFF00) { break; } else { offset += view.getUint16(offset, false); } } resolve(1); }; // 只读取前64KB,足够获取EXIF信息 reader.readAsArrayBuffer(file.slice(0, 64 * 1024)); }); } // 使用示例:上传相机图片时自动处理旋转 document.querySelector('#cameraUpload').addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const orientation = await getImageOrientation(file); const img = new Image(); img.onload = function() { let rotateAngle = 0; // 根据EXIF方向映射旋转角度 switch(orientation) { case 3: rotateAngle = 180; break; case 6: rotateAngle = 90; break; case 8: rotateAngle = 270; break; default: rotateAngle = 0; } const rotatedCanvas = rotateImage(img, rotateAngle); document.body.appendChild(rotatedCanvas); }; img.src = URL.createObjectURL(file); });
这样处理后,旋转后的图像就不会有偏移了,也能正确处理相机拍摄的图片方向。
内容的提问来源于stack exchange,提问作者PalDev




