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

一、简述

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

二、实现图片处理原理

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

从刚开始点击上传或拖拽上传图片开始,得到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插件
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342