前端实现图片处理的原理及问题解决方案

一、简述

前段时间完成了自己的图片处理工具,纯前端实现,不借助第三方图片处理库,收获颇多,想要了解的可以看我的上一篇文章:给所有前端使用的图片处理工具。在开发的过程中也是遇到了各种问题,所以想通过这篇文章进行一个技术总结和分享。

二、实现图片处理原理

想要实现图片翻转、旋转、修改亮度、修改透明度等等这些功能,就必须要通过修改图片中每一个像素点来实现,那么如何拿到图片的像素点数据就成了关键。

从刚开始点击上传或拖拽上传图片开始,得到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

猜你喜欢

转载自blog.csdn.net/weixin_43552879/article/details/129822079