Node端使用sharp图片处理的封装

465 阅读3分钟

Node端使用sharp图片处理的封装

在将上游货品分销到下游平台时,我们需要将上游平台的图片数据按照下游平台商品的发品规则进行调整。其中就包括了分辨率修改、图片大小限制、图片格式限制等处理。

那么为了进行这些处理,就选择了使用Sharp库进行处理,同时为了能够让每一个下游平台使用同一套处理方式,就需要将Sharp库进行封装,来方便各下游平台进行使用。

sharp库封装

sharp库的官方文档指南:sharp.pixelplumbing.com/

关于Sharp库,就不过多介绍了,大家可以前往上方的指南,前往其文档进行查看,这里有一点特别需要指明的是,在使用Sharp库的时候,大家的Node版本需要>=18.17.0

图像保存

图片下载到本地保存的过程,实际就是通过请求获取到图片Buffer数据,然后通过配合使用fs\path两个模块,来写入数据,然后再通过Sharp的格式转换,将图片转换成我们需要的格式,然后再进行保存。

下面就是图像保存的完整实现

/**
 * @description 判断文件是否存在
 */
isExistFile (filePath: string) {
    const isExist = fs.existsSync(filePath);
    return isExist;
}
/**
 * @description 创建文件目录,支持多级创建
 * @param {string} destroyPath 文件目录路径
 * @param {boolean} parse 是否规范化
 */
createDestroy (destroyPath: string, parse = true) {
    try {
        if (this.isExistFile(destroyPath)) {
            return;
        }
        const realDirectory = parse ? path.parse(destroyPath).dir : destroyPath;
        const newPath = path.normalize(realDirectory);
        const pathList = newPath.replace(global.__dirname, '').split(path.sep)
            .filter(value => value !== '');
        let pathCur = '';
        for (let index = 0; index < pathList.length; index++) {
            pathCur = path.resolve('../../../', pathCur, pathList[index]);
            if (this.isExistFile(pathCur)) {
                continue;
            }
            fs.mkdirSync(pathCur, { recursive: true });
        }
        fs.chmodSync(destroyPath, '777');
    } catch (error) {
        throw new Error(error.message);
    }
}
/**
 * @description 图像获取
 * @param {string} imageUrl 图片url地址
 * @param {string} localFileName 本地路径图片名称
 * @param {string} imageFilePath 图片保存路径
 */
async function getImageAndSaveLocal (imageUrl: string, localFileName = `${uuid.v4()}_${Math.ceil(Math.random() * 100000)}`, imageFilePath: string = this.baseImagePath) {
    imageFilePath = Path.resolve(imageFilePath, `${this.fileSaveDirectoryPath}/`);
    this.templateFileFolder = imageFilePath;
    // NOTE: 提取url链接中,图片的类型
    const url = new URL(imageUrl);
    const { pathname } = url;
    const parts = pathname.split('.');
    const fileType = parts[parts.length - 1];
    // NOTE: 请求图片流数据
    try {
        await this.downloadImageAndSave(imageUrl, {
            path: imageFilePath,
            name: `${localFileName}.${fileType}`,
        });
        return Path.resolve(imageFilePath, `${localFileName}.${fileType}`);
    } catch (error) {
        this.logger.info(`图片下载失败:${imageUrl}`);
        return null;
    }
}
/**
 * @description 图像下载,并保存在本地
 * @param {string} imageUrl 图像地址
 */
async function downloadImageAndSave (imageUrl: string, saveOption: { path: string, name: string }): Promise<any> {
    if (!this.isExistFile(saveOption.path)) {
        this.createDestroy(saveOption.path, false);
    }
    const imageResponse = await axios({
        url: imageUrl,
        responseType: 'arraybuffer',
    });
    this.templateWaitClearImage.push(Path.resolve(saveOption.path, saveOption.name));
    if (saveOption.name.includes('.gif')) {
        saveOption.name = saveOption.name.replace('.gif', '.jpg');
        await sharp(Buffer.from(imageResponse.data))
            .jpeg()
            .toFile(Path.resolve(saveOption.path, saveOption.name));
    } else {
        await sharp(Buffer.from(imageResponse.data)).toFile(Path.resolve(saveOption.path, saveOption.name));
    }
    fs.chmodSync(Path.resolve(saveOption.path, saveOption.name), '644');
}

图像压缩

在使用Sharp进行图像压缩时,实际就是通过调整图片的质量,来进行图片压缩的。下面就是图片压缩的具体实现。

我们可以通过设置最大压缩次数,来实现压缩到我们的目标大小,同时我们可以设置图片损失质量,来实现压缩大小的调整。

/**
 * @description 图像压缩
 * @param {string} imageLocalPath 图像本地地址
 * @param {number} targetSize 图片压缩配置项目标大小, 大小kb
 * @param {number} handleCount 处理次数,默认为1
 * @param {number} compressRatio 压缩比例0~100
 */
async compressImage (imageLocalPath: string, targetSize = 300, handleCount = 1, compressRatio = 50) {
    try {
        // NOTE: 设置最大压缩次数3次
        if (handleCount > this.maxCompressCount) {
            return imageLocalPath;
        }
        const templatePath = handleCount === 1 ? imageLocalPath.replace('.jpg', `_compressImage_${handleCount}.jpg`) : imageLocalPath.replace(`_compressImage_${handleCount - 1}.jpg`, `_compressImage_${handleCount}.jpg`);
        const imageData = fs.readFileSync(imageLocalPath);
        await sharp(imageData, { limitInputPixels: false })
            .jpeg({ quality: Math.max(compressRatio, 50) })
            .toFile(templatePath);
        fs.chmodSync(templatePath, '777');
        this.templateWaitClearImage.push(templatePath);
        const imageFileSize = this.file.getFileSize(templatePath);
        this.logger.info(`图片压缩处理----图片压缩处理成功----处理次数${handleCount}`);
        if (parseFloat(imageFileSize) > targetSize) {
            return this.compressImage(templatePath, targetSize, handleCount + 1, compressRatio - 20);
        }
        return templatePath;
    } catch (error) {
        throw new Error(`图片压缩处理----图片压缩处理失败----${error.message}`);
    }
}

图像分辨率调整

sharp库进行分辨率调整十分方便,只需要借用resize函数就可以轻松实现了。但是根据最终的呈现的话,这里我就给出了两种我的处理方式,一种就是直接的分辨率调整;还一种就是更加复杂的一种实现,借助白底图的生成,将原图片和白底图进行合成后的分辨率调整。

下面就是两种方式的实现代码:

方式一:直接调整

直接调整则是直接借助Sharp的resize函数进行调整,传入我们需要调整的分辨率值即可。

/**
 * @description 图片像素调整
 */
async function imagePixelHandle (imagePixel: {width: number, height:number, localPath: string}) {
    const { width, height, localPath } = imagePixel;
    const imageCanvasData = fs.readFileSync(localPath);
    const targetImagePath = localPath.replace('.jpg', '_pixel.jpg');
    await sharp(imageCanvasData)
        .resize({ width: Math.floor(width), height: Math.floor(height) })
        .toFile(targetImagePath);
    fs.chmodSync(targetImagePath, '777');
    return targetImagePath;
}

方式二:白底图合成调整

白底图合成调整的话会相对比较复杂,他需要先计算出需要调整的白底图的大小,然后进行白底图的生成,最后再将原图片与白底图进行合成,最后将合成图片进行返回。下面是代码实现:

/**
 * @description 图像比例调整
 * @param {{widthScale: number, heightScale: number, localPath: string}} imageScale 图像缩放信息
 */
async function imageModifyScale (imageScale: { widthScale: number, heightScale: number, localPath: string }): Promise<string> {
    const { widthScale, heightScale, localPath } = imageScale;
    // NOTE: 图片比例:宽/高比
    const proportion = widthScale / heightScale;
    const imageInfo = await this.getImageInfo(localPath);
    this.logger.info('图片比例处理----图片信息');
    if ((imageInfo.width / imageInfo.height).toFixed(2) === proportion.toFixed(2)) {
        return localPath;
    }
    // NOTE: 最大边长,最小边长
    const maxiumLength = Math.max(imageInfo.height, imageInfo.width);
    const minimunLength = Math.min(imageInfo.height, imageInfo.width);
    const isEqualMaxAndMin = maxiumLength === minimunLength;
    // NOTE: 反转比例,用于计算白底图大小
    const reverseProportion = (heightScale / widthScale);
    this.logger.info([maxiumLength, minimunLength, reverseProportion, proportion].toString());
    const isEqualOne = proportion === 1;
    const isLessOne = proportion < 1;
    const whiteBackSize = {
        width: 0,
        height: 0,
    };
    // NOTE: 根据比例,来计算白底图大小(一定大于等于原大小)
    if (isLessOne) {
        whiteBackSize.width = isEqualMaxAndMin ? minimunLength : Math.ceil(minimunLength * reverseProportion);
        whiteBackSize.height = Math.ceil(maxiumLength * reverseProportion);
    } else if (isEqualOne) {
        whiteBackSize.width = maxiumLength;
        whiteBackSize.height = maxiumLength;
    } else {
        whiteBackSize.width = isEqualMaxAndMin ? Math.ceil(maxiumLength * proportion) : Math.ceil(maxiumLength * proportion);
        whiteBackSize.height = isEqualMaxAndMin ? minimunLength : maxiumLength;
    }
    const whiteCanvasPath = await this.createWhiteImage(whiteBackSize.width, whiteBackSize.height, localPath);
    const saveHandleImagePath = await this.imageSynthesis(localPath, localPath.replace('.jpg', '_whiteBack.jpg'), whiteCanvasPath, whiteBackSize);
    this.templateWaitClearImage.push(saveHandleImagePath);
    return saveHandleImagePath;
}

/**
 * @description 图像白底图制作
 * @param {number} width 长度
 * @param {number} height 宽度
 * @param {string} localPath 本地图片路径
 */
async function createWhiteImage (width: number, height: number, localPath: string): Promise<string> {
    const whiteImagePath = localPath.replace('.jpg', '_whiteCanvas.jpg');
    // NOTE: 生成一张白底图
    await sharp({
        create: {
            width,
            height,
            channels: 3,
            background: { r: 255, g: 255, b: 255 },
        },
    }).toFile(whiteImagePath);
    fs.chmodSync(whiteImagePath, '777');
    this.templateWaitClearImage.push(whiteImagePath);
    return whiteImagePath;
}

/**
 * @description 图片合成
 * @param {string} originImage 源图片,本地路径
 * @param {string} targetImage 目标图片
 * @param {any} whiteCanvas 白底图
 * @param {{width: number, height: numebr}} 白底图信息
 */
async function imageSynthesis (originImage: string, targetImage: string, whiteCanvas: any, whiteCanvasInfo: { width: number, height: number }): Promise<string> {
    const { width: whiteBackWidth, height: whiteBackHeight } = whiteCanvasInfo;
    const whiteCanvasData = fs.readFileSync(whiteCanvas);
    const originImageData = fs.readFileSync(originImage);
    // NOTE: 计算中心坐标
    await sharp(whiteCanvasData)
        .resize({ width: whiteBackWidth, height: whiteBackHeight })
        .composite([{
            input: originImageData,
            blend: 'over',
        }])
        .toFile(targetImage);
    fs.chmodSync(targetImage, '777');
    return targetImage;
}

格式转化

其实格式转化的代码非常容易实现,因为Sharp提供了最终输出格式的函数,所以很轻松的就可以实现,在上面的内容处理中,其实也已经存在了格式转化的代码,这里就展示一下将webp图片转换成jpg图片的操作。

/**
 * @description 将webp图片转成jpg格式
 * @param {string} imageUrl 外链地址
 */
async function converWebpToJpg (imageUrl: string) {
    const fileName = `${uuid.v4()}_${Math.floor(Math.random() * 100000)}.webp`;
    const localFileName = await this.getImageAndSaveLocal(imageUrl, fileName);
    const saveFileName = localFileName.replace('.webp', 'jpg');
    const imageData = fs.readFileSync(localFileName);
    await sharp(imageData)
        .jpeg()
        .toFile(saveFileName);
    this.templateWaitClearImage.push(saveFileName);
    return saveFileName;
}

注意事项

在使用Sharp时,有一个非常需要注意的点就是:如果需要在本地处理好的图片,上传到oss服务生成对应外链后,需要删除本地图片时,那么在使用Sharp处理时,一定要先将图片数据转换成Buffer,然后再交由Sharp进行处理,否则就会出现无法关闭图片的情况,导致删除本地图片失败。

注:一部分在上面代码中出现的内容参数数据:

baseImagePath: string;
maxCompressCount = 3;
templateWaitClearImage: string[] = [];
templateFileFolder = '';
/** 图片保存目录路径 */
fileSaveDirectoryPath = '';

大家在使用上有啥不懂的地方或者认为可以优化的地方,可以在评论中留言,哈哈哈哈