JS压缩图片并保留图片元信息

HaoOuBa
2022-07-13 / 3 评论 / 351 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2022年07月13日,已超过138天没有更新,若内容或图片失效,请留言反馈。

JS实现图片压缩比较简单,但是图片经过压缩后,压缩后的图片的元信息(拍摄时间、设备、地点)等会丢失掉,如果在特殊场景中需要使用这些元信息的话,就会出现问题了,因此需要将未压缩前的图片元信息填充至压缩后的图片中,以下是实现代码

// 封装一个获取变量的数据类型函数
const getType = (data: unknown): string => {
  const toStingResult = Object.prototype.toString.call(data);
  const type = toStingResult.replace(/^\[object (\w+)\]$/, "$1");
  return type.toLowerCase();
};

// 封装一个将 Base64 的字符串转换成 Blob 流的函数
const dataURLtoBlob = (dataURL: string): Blob | null => {
  const dataType = getType(dataURL);
  if (dataType !== "string") return null;
  const arr = dataURL.split(",");
  if (!arr[0] || !arr[1]) return null;
  const code = window.atob(arr[1]);
  const mimeExpRes = arr[0].match(/:(.*?);/);
  if (!mimeExpRes) return null;
  let len = code.length;
  const mime = mimeExpRes[1];
  if (!mime) return null;
  const ia = new Uint8Array(len);
  while (len--) ia[len] = code.charCodeAt(len);
  return new Blob([ia], { type: mime });
};

// 利用规律编码格式把里面的标记以及值等分割开来,传原图片的 ArrayBuffer 进来
const getSegments = (arrayBuffer: ArrayBuffer): number[][] => {
  if (!arrayBuffer.byteLength) return [];
  let head = 0;
  let length, endPoint, seg;
  const segments = [];
  const arr = [].slice.call(new Uint8Array(arrayBuffer), 0);
  while (1) {
    if (arr[head] === 0xff && arr[head + 1] === 0xda) break;
    if (arr[head] === 0xff && arr[head + 1] === 0xd8) {
      head += 2;
    } else {
      length = arr[head + 2] * 256 + arr[head + 3];
      endPoint = head + length + 2;
      seg = arr.slice(head, endPoint);
      head = endPoint;
      segments.push(seg);
    }
    if (head > arr.length) break;
  }
  return segments;
};

// 传入上面 getSegments 的返回值,取出EXIF图片元信息
const getEXIF = (segments: number[][]): Array<number> => {
  if (!segments.length) return [];
  let seg: Array<number> = [];
  for (let i = 0; i < segments.length; i++) {
    const item = segments[i];
    if (item[0] === 0xff && item[1] === 0xe1) {
      seg = seg.concat(item);
    }
  }
  return seg;
};

// 将 getEXIF 获取的元信息,插入到压缩后的图片的 Blob 中,传 压缩图片后的 Blob 流
const insertEXIF = (blob: Blob, exif: number[]): Promise<Blob> => {
  return new Promise((resolve, reject) => {
    const fileReader = new FileReader();
    fileReader.onload = () => {
      const arr = [].slice.call(new Uint8Array(fileReader.result as ArrayBuffer), 0);
      if (arr[2] !== 0xff || arr[3] !== 0xe0) {
        return reject(new Error("Couldn't find APP0 marker from blob data"));
      }
      const length = arr[4] * 256 + arr[5];
      const newImage = [0xff, 0xd8].concat(exif, arr.slice(4 + length));
      const uint8Array = new Uint8Array(newImage);
      const newBlob = new Blob([uint8Array], { type: "image/jpeg" });
      resolve(newBlob);
    };
    fileReader.readAsArrayBuffer(blob);
  });
};

// 压缩图片逻辑
const compressImage = (file: File, quality: number): Promise<Blob | null> => {
  return new Promise((resolve, reject) => {
    const fileReader = new FileReader();
    fileReader.onload = () => {
      const img = new Image();
      img.src = fileReader.result as string;
      img.onload = () => {
        const { width, height } = img;
        const canvas = window.document.createElement("canvas");
        const ctx = <CanvasRenderingContext2D>canvas.getContext("2d");
        canvas.width = width;
        canvas.height = height;
        ctx.drawImage(img, 0, 0, width, height);
        const fileData = canvas.toDataURL("image/jpeg", quality);
        const fileBlob = dataURLtoBlob(fileData);
        resolve(fileBlob);
      };
      img.onerror = (err) => reject(err);
    };
    fileReader.onerror = (err) => reject(err);
    fileReader.readAsDataURL(file);
  });
};

/**
 * @description: 完整的压缩图片,最终对外暴露的函数
 * @param {File} file
 * @param {number} quality 0 - 1
 * @return {Promise<File>}
 */
export default (file: File, quality = 0.5): Promise<File> => {
  return new Promise((resolve, reject) => {
    const dataType = getType(file);
    if (dataType !== "file") return reject(new Error(`Expected parameter type is file, You passed in ${dataType}`));
    if (file.type.indexOf("image") === -1) return resolve(file);
    // 压缩图片
    compressImage(file, quality)
      .then((compressdBlob) => {
        if (!compressdBlob) return resolve(file);
        const fileReader = new FileReader();
        fileReader.onload = () => {
          // 获取图片元信息
          const segments = getSegments(fileReader.result as ArrayBuffer);
          const exif = getEXIF(segments);
          // 没有元数据的时候, 直接抛出压缩后的图片
          if (!exif.length) return resolve(new File([compressdBlob], file.name, { type: file.type, lastModified: file.lastModified }));
          // 有元数据的时候, 将元信息合并到压缩图片里
          insertEXIF(compressdBlob, exif)
            .then((newBlob) => resolve(new File([newBlob], file.name, { type: file.type, lastModified: file.lastModified })))
            .catch(() => resolve(file));
        };
        fileReader.onerror = () => resolve(file);
        fileReader.readAsArrayBuffer(file);
      })
      .catch(() => resolve(file));
  });
};
7

评论 (3)

取消
  1. 头像
    歪比巴卜
    Android · Google Chrome

    歪比巴卜

    回复
  2. 头像
    vian
    Windows 10 · Google Chrome

    新主题咋没在博客放出点消息啊表情

    回复
  3. 头像
    汇享
    Windows 10 · Google Chrome

    坐等重构的大版本表情

    回复