前端實現圖片處理的原理及問題解決方案

一、簡述

前段時間完成了自己的圖片處理工具,純前端實現,不借助第三方圖片處理庫,收獲頗多,想要了解的可以看我的上一篇文章:給所有前端使用的圖片處理工具。在開發的過程中也是遇到了各種問題,所以想通過這篇文章進行一個技術總結和分享。

二、實現圖片處理原理

想要實現圖片翻轉、旋轉、修改亮度、修改透明度等等這些功能,就必須要通過修改圖片中每一個像素點來實現,那么如何拿到圖片的像素點數據就成了關鍵。

從剛開始點擊上傳或拖拽上傳圖片開始,得到FileList實例對象files,然后通過new一個FileReader對象獲取到buffer,通過new Blob([buffer])獲取到blob對象,然后將blob對象轉換成DataURL,最后通過const image = new Image(),在image.onload回調函數中獲取到ImageData對象,ImageData對象中保存了圖片每個像素的RGBA通道值,所有的操作都是通過修改ImageData對象中每個像素點的RGBA通道值來實現的。

部分實現代碼如下:

const file = files[i];
const { type } = file;
const typeArr = type.split("/");
if (typeArr[0] !== "image") return;
let fileType = typeArr[1].toUpperCase();
var reader = new FileReader();
reader.onload = function (e: any) {
    const buffer = e.target.result;
    const imageType = getImageType(buffer);
    if (imageType) {
        fileType = imageType;
    }
    const blob = new Blob([buffer]);
    fileOrBlobToDataURL(blob, function (dataUrl: string | null) {
        if (dataUrl) {
            const image = new Image();
            image.onload = function () {
                const width = image.width;
                const height = image.height;
                const imageData = getCanvasImgData(dataUrl, width, height);
                if (imageData) {
                } else {}
            };
            image.onerror = function () { };
            image.src = dataUrl;
        } else { }
    });
};
reader.readAsArrayBuffer(file);

// File或Blob對象轉DataURL
const fileOrBlobToDataURL = (
  obj: File | Blob,
  cb: (result: string | null) => void
) => {
  if (!obj) {
    cb(null);
    return;
  }
  const reader = new FileReader();
  reader.readAsDataURL(obj);
  reader.onload = function (e) {
      if (e.target) {
          cb(e.target.result as string);
      } else {
          cb(null);
      }
   };
};

// 獲取圖片二進制數據
const getCanvasImgData = (
  imgUrl: string,
  width: number = 0,
  height: number = 0
) => {
  if (imgUrl && width && height) {
    const img = new Image();
    img.src = imgUrl;
    const canvas = document.createElement("canvas") as HTMLCanvasElement;
    const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
    canvas.width = width;
    canvas.height = height;
    ctx.drawImage(img, 0, 0, width, height);
    const imageData = ctx.getImageData(0, 0, width, height) as ImageData;
    return imageData;
  }
  return null;
};

三、遇到的問題及解決方案

1. 為何必須獲取圖片的真實格式?如何獲???

我們在進行各種操作或導出圖片時必須要知道圖片的真實格式,這樣我們才能根據圖片格式做相應的處理,如果我們獲取的圖片格式不對,那么我們在操作時使用的算法就不對,最終導致我們導出的圖片顯示就會有問題。

相信大家也都知道,通過文件后綴名得到的圖片格式是不準確的,其實通過上面的代碼中const { type } = file獲取到的圖片格式也是不準確的,那么我們如何獲取到圖片的真實格式呢?

// 根據buffer中的文件頭信息判斷圖片類型
const getImageType = (buffer: Buffer) => {
    let fileType = "";
    if (buffer) {
        const view = new DataView(buffer);
        const first4Byte = view.getUint32(0, false);
        const hexValue = Number(first4Byte).toString(16).toUpperCase();
        switch (hexValue) {
            case "FFD8FFDB":
                fileType = "JPG";
                break;
            case "FFD8FFE0":
            case "FFD8FFE1":
            case "FFD8FFE2":
            case "FFD8FFE3":
                fileType = "JPEG";
                break;
            case "89504E47":
                fileType = "PNG";
                break;
            case "47494638":
                fileType = "GIF";
                break;
            case "52494646":
                fileType = "WEBP";
                break;
            default:
                break;
        }
    }
    return fileType;
};
2. 如何導出生成圖片?
// 導出圖片
const exportToImage = (blob: Blob, imgName: string) => {
    if (!blob) return;
    var a = document.createElement("a");
    a.style.visibility = "hidden";
    document.body.appendChild(a);
    a.download = imgName;
    const objUrl = window.URL.createObjectURL(blob);
    a.href = objUrl;
    a.click();
    document.body.removeChild(a);
    window.URL.revokeObjectURL(objUrl);
};
3. PNG轉換成JPG時為何背景是黑色的?如何解決?

當我們將一個半透明的PNG圖片轉換成JPG格式時,直接使用canvas.toBlob方法獲取blob對象并導出,生成的圖片背景則是黑色的,與我們期望的白色背景不符,為何會出現這種問題。

經過我多次驗證發現,由于半透明圖片的各像素點的第四個A通道值不是255,canvas.toBlob方法在生成PNG格式的數據時,會將該像素點的RGB三個通道值都改為0,所以導致生成的圖片背景是黑色的。

那么如何解決呢,將每個像素的A通道值都改為255不就搞定了,但這樣的話還有個問題,眾所周知,當我們將一個半透明圖片放在白色背景圖上時,看到的視覺效果其實是將上面圖片的每個像素的RGB值都改變后的,所以我們還需要通過算法去計算出每個像素在和白色背景疊加后的值。

// PNG轉JPG
const pngToJpg = (imageData: ImageData) => {
    if (imageData) {
        const { data, width, height } = imageData;
        const newImgData = new Uint8ClampedArray(data.length);
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                const startIndex = (y * width + x) * 4;
                if (data[startIndex + 3] === 0) {
                    newImgData[startIndex] = 255;
                    newImgData[startIndex + 1] = 255;
                    newImgData[startIndex + 2] = 255;
                    newImgData[startIndex + 3] = 255;
                } else {
                    const newColor = colorStacks(
                        [
                            data[startIndex],
                            data[startIndex + 1],
                            data[startIndex + 2],
                            data[startIndex + 3],
                        ],
                        [255, 255, 255, 255]
                    );
                    newImgData[startIndex] = newColor[0];
                    newImgData[startIndex + 1] = newColor[1];
                    newImgData[startIndex + 2] = newColor[2];
                    newImgData[startIndex + 3] = newColor[3];
                }
            }
        }
        const newImageData = new ImageData(newImgData, width, height);
        return newImageData;
    }
    return null;
};
4. 兩個像素顏色疊加合成一個像素的算法是怎樣的?
  • 如果上面像素的A通道值為255或者下面像素的A通道值為0,則直接返回上面的像素值。

  • 如果上面像素的A通道值為0,則直接返回下面的像素值。

  • 上面像素A通道值的比例 = 上面像素A通道值 / 255
    下面像素A通道值的比例 = 下面像素A通道值 / 255

    最終像素R通道值 = 上面像素R通道值 * 上面像素A通道值的比例 +下面像素R通道值 * 下面像素A通道值的比例 * (1 - 上面像素A通道值的比例)

    最終像素G通道值 = 上面像素G通道值 * 上面像素A通道值的比例 +下面像素G通道值 * 下面像素A通道值的比例 * (1 - 上面像素A通道值的比例)

    最終像素B通道值 = 上面像素B通道值 * 上面像素A通道值的比例 +下面像素B通道值 * 下面像素A通道值的比例 * (1 - 上面像素A通道值的比例)

    最終像素A通道值 = (上面像素A通道值的比例 + 下面像素A通道值的比例 * (1 - 上面像素A通道值的比例)) * 255

// 顏色疊加算法
const colorStacks = (
    aboveColor: [number, number, number, number],
    belowColor: [number, number, number, number]
) => {
    if (
        aboveColor &&
        aboveColor.length === 4 &&
        belowColor &&
        belowColor.length === 4
    ) {
        const aboveA = aboveColor[3];
        const belowA = belowColor[3];
        if (aboveA === 255 || belowA === 0) {
            return aboveColor;
        } else if (aboveA === 0) {
            return belowColor;
        } else {
            const aboveDiaphaneity = aboveA / 255;
            const belowDiaphaneity = belowA / 255;
            const newColorR = Math.max(
                Math.min(
                    Math.floor(
                        aboveColor[0] * aboveDiaphaneity + belowColor[0] * belowDiaphaneity * (1 - aboveDiaphaneity)
                    ),
                    255
                ),
                0
            );
            const newColorG = Math.max(
                Math.min(
                    Math.floor(
                        aboveColor[1] * aboveDiaphaneity + belowColor[1] * belowDiaphaneity * (1 - aboveDiaphaneity)
                    ),
                    255
                ),
                0
            );
            const newColorB = Math.max(
                Math.min(
                    Math.floor(
                        aboveColor[2] * aboveDiaphaneity + belowColor[2] * belowDiaphaneity * (1 - aboveDiaphaneity)
                    ),
                    255
                ),
                0
            );
            const newColorA = Math.max(
                Math.min(
                    Math.floor(
                        (aboveDiaphaneity + belowDiaphaneity * (1 - aboveDiaphaneity)) * 255
                    ),
                    255
                ),
                0
            );
            return [newColorR, newColorG, newColorB, newColorA];
        }
   }
   return [255, 255, 255, 255];
};
5. 實現圖片壓縮的原理?

圖片壓縮就是使用canvas.toBlob方法,它的第三個參數是圖片的質量,支持0到1范圍的值,默認為0.92,但是它只針對于第二個參數傳時的情況,對于png的壓縮它是無效的,所以處理png我們得另想辦法,我也是在github上copy的第三方庫來實現的(技術不行,勿噴)。

// 圖片壓縮
const compression = async (
    imageUrl: string | ImageData,
    width: number,
    height: number,
    imageType: string,
    compressionDegree: number,
    cb: (blob: Blob | null) => void
) => {
    if (imageUrl && imageType) {
        const degree = compressionDegree / 100;
        if (["JPG", "JPEG"].includes(imageType.toUpperCase())) {
            const img = new Image();
            img.src = imageUrl as string;
            const canvas = document.createElement("canvas") as HTMLCanvasElement;
            const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
            canvas.width = width;
            canvas.height = height;
            ctx.drawImage(img, 0, 0, width, height);
            canvas.toBlob(
                (blob: Blob | null) => {
                    cb && cb(blob);
                },
               `image/${imageType.toLowerCase()}`,
               degree
            );
        } else {
            const bit = Math.floor(degree * 256);
            const png = UPNG.encode(
                [(imageUrl as ImageData).data.buffer],
                width,
                height,
                bit
            );
            const blob = new Blob([png]);
            cb && cb(blob);
        }
    } else {
        cb && cb(null);
    }
};

具體的代碼實現可以參考我開源項目的源碼,最后感謝大家的支持和喜歡。
https://github.com/hepengwei/visualization-collection

更多個人文章

  1. hashHistory和browserHistory的區別
  2. 全網最全Autoit3基礎教程及實戰案例
  3. 面試秘籍之排序算法
  4. 一款將打包后的Chrome插件自動化加載到瀏覽器的webpack插件
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。