# png图片自动新旧对比

# 背景

由于控制器包体积过大,其中大部分又是由图片大小导致的,所以之前对图片进行了压缩。现在遇到的问题是回归测试时,无法手动测试覆盖全(改动了一千多的图片),所以需要自动化的方法进行测试。

# 方案

在想怎么对比新旧图片时,忽然想到Ixs组件库的UI测试就是用页面截图去跟之前保存的截图进行对比,判断是否发生了变化。

于是顺藤摸瓜,找到了类似的纯图片对比的工具:pixelmatch

github: https://github.com/mapbox/pixelmatch

该库可以对png进行像素级别的对比,返回差异的像素数量,并可以画出差异的地方。

对于我们的项目,只需要确认差异值小于误差范围即可。

流程如下:

  1. 循环修改后的代码目录,找出所有png
  2. 根据png路径在老代码目录找出对应png,将两个图片传给pixelmatch进行对比。区分的敏感度threshold为0.1,最灵敏的程度,保证能找出区别。
  3. 这里的差异值范围我们取图片总像素的千分之一,如果差异值大于千分之一则记录下来,再人工进行对比

下面给出代码:

需要注意的是,存在非png的图片改为了png后缀的,所以需要先检查下真实的格式后再做对比。

import fs from 'fs';
import path from 'path';
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';

// 修改后的文件夹路径
const folder1 = '';
// 修改前的文件夹路径
const folder2 = '';

const outputFile = 'diffImages.txt'; // 记录差异大的图片路径的文件
let diffImagesLength = 0; // 记录差异大的图片数量

const isValidPng = (filePath) => {
    const buffer = fs.readFileSync(filePath);
    const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10];
    for (let i = 0; i < pngSignature.length; i++) {
        if (buffer[i] !== pngSignature[i]) {
            return false;
        }
    }
    return true;
};

// 递归读取指定文件夹中的 PNG 文件
const getPngFilesRecursively = (folder) => {
    let pngFiles = [];
    const files = fs.readdirSync(folder);

    for (const file of files) {
        const filePath = path.join(folder, file);
        const stat = fs.statSync(filePath);

        if (stat.isDirectory()) {
            // 如果是目录,递归调用
            pngFiles = pngFiles.concat(getPngFilesRecursively(filePath));
        } else if (path.extname(file).toLowerCase() === '.png') {
            pngFiles.push(filePath);
        }
    }

    return pngFiles;
};

// 对比两个 PNG 图片,并返回差异数量
const compareImages = (img1Path, img2Path, relativePath) => {
    const img1 = PNG.sync.read(fs.readFileSync(img1Path));
    const img2 = PNG.sync.read(fs.readFileSync(img2Path));

    const { width, height } = img1;

    // 确保两张图片大小相同
    if (width !== img2.width || height !== img2.height) {
        console.log(`图片大小不匹配: ${relativePath}`);
        return;
    }

    const diff = pixelmatch(img1.data, img2.data, null, width, height, {
        threshold: 0.1,
        includeAA: true,
    });

    // 计算允许的阈值,0.1%
    const totalPixels = width * height;
    const allowedDifference = totalPixels * 0.001;

    if (diff > allowedDifference) {
        diffImagesLength++;
        fs.appendFileSync(outputFile, `差异图片: ${relativePath},差异像素数量: ${diff}\n`);
    }

    return diff;
};

process.stdout.write(`正在查找png文件`);
// 读取文件夹中的 PNG 文件
const folder1Files = getPngFilesRecursively(folder1);
const totalFiles = folder1Files.length;
process.stdout.clearLine();

// 进行对比
folder1Files.forEach((file1, index) => {
    const relativePath = path.relative(folder1, file1);
    // 在第二个文件夹中生成对应的完整路径
    const file2 = path.join(folder2, relativePath);
    let diff = 0;

    if (fs.existsSync(file2)) {
        if (isValidPng(file1) && isValidPng(file2)) {
            diff = compareImages(file1, file2, relativePath);
            // 显示进度
            const progress = ((index + 1) / totalFiles * 100).toFixed(2);
            process.stdout.clearLine();
            process.stdout.write(`总进度: ${progress}% 图片像素差异: ${diff} 对比图片:${relativePath} \r`);
        } else {
            console.log(`无效的 PNG 文件: ${relativePath}`);
        }
    } else {
        console.log(`未找到对应文件: ${file2}`);
    }
});

process.stdout.clearLine();

if (diffImagesLength > 0) {
    console.log(`存在差异过大的图片,已记录到 ${outputFile}`);
} else {
    console.log('两个文件夹中的图片没有明显差异。');
}

# 总结

通过工具,1分钟即可完成上千张图片的像素级别的对比,既解决了手动无法覆盖全的问题,也保证了质量问题。

后续有图片对比的需求,也可参照该方案实现。