You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何用纯JavaScript和HTML5压缩缩放Canvas图像?相机图片旋转偏移排查

嘿,我来帮你搞定这两个纯JS + HTML5 Canvas的问题,都是客户端就能实现的,不用额外依赖库~

一、如何实现Canvas图像的压缩与缩放?

核心思路就是用Canvas的drawImage做缩放,再通过toBlobtoDataURL导出压缩后的结果,步骤很清晰:

  • 第一步:加载图像
    不管是本地上传的文件还是网络图片,都用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尺寸没跟着修改,也会出现裁剪或偏移。

正确的旋转步骤

  1. 根据旋转角度调整Canvas尺寸:比如旋转90度,Canvas的宽要改成原图的高,高改成原图的宽。
  2. 平移坐标系到Canvas中心:把旋转中心移到Canvas中心点,确保图像旋转后居中。
  3. 旋转画布,再平移回图像中心:让图像绘制在旋转后的坐标系里不会偏移。

给你一个靠谱的旋转函数:

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

火山引擎 最新活动