图片批量递归压缩方法

本文介绍了三种压缩图片的方法,包括使用TinyPNG网站的API、Node.js库Sharp以及Squoosh网站配合Selenium自动化。作者通过示例代码展示了如何在Markdown笔记中应用这些方法,并推荐了Sharp作为简单易用的选择。
摘要由CSDN通过智能技术生成

为了压缩markdown笔记图片等用途,寻找了图片批量压缩的方法。虽然实际方法肯定很多,但是以下仅涉及了3个方法。

1.使用TinyPNG网站

  • TinyPNG是国外的一个无需注册即可免费使用的图片压缩网站,限制是每月免费压缩500张,同时单次限制上传20张。

  • 通过点击以下链接可申请API密钥,使用Nodejs开发使用。

  • 项目中输入命令安装pnpm i tinify即可使用,以下是简单代码实现

  • 测试后感觉限制不少,同时可能网络原因完成压缩用时一般。

import tinify from 'tinify';
import fs from "fs";
import path from "path";
import config from './config.js';

tinify.key = config.api_key;
const imageExtensions = ['.jpg', '.jpeg', '.png', '.bmp', '.svg'];
const folderPath = './';

// 递归遍历文件夹
const getImagesInFolder = (folderPath, images = []) => {
  const files = fs.readdirSync(folderPath);
  files.forEach((file) => {
    const filePath = path.resolve(folderPath, file);
    const stat = fs.statSync(filePath);
    if (stat.isDirectory()) {
      getImagesInFolder(filePath, images);
    } else if (isImageFile(file)) {
      images.push(filePath);
    }
  });
  return images;
};


const isImageFile = (filename) => {
  const ext = path.extname(filename).toLowerCase();
  return imageExtensions.includes(ext);
};

const zipImage = (path) => {
  fs.readFile(path, (err, sourceData) => {
    if (err) {
      throw err;
    }
    tinify.fromBuffer(sourceData).toBuffer((err, resultData) => {
      if (err) {
        throw err;
      }
      fs.writeFile(path, resultData, (err => {
        if (err) {
          throw err;
        }
      }));
    });
  });
};

const imagePaths = getImagesInFolder(folderPath);
console.log('All image paths:', imagePaths);

imagePaths.forEach(item => zipImage(item));

2.Sharpjs

  • Sharp是nodejs中的一个常用图像处理包,功能也非常多,其功能之一就是用来压缩图片。
  • 项目中需安装pnpm i sharp,我还额外安装了chalk库便于打印显示,就是一个简单的展示效果。
  • 以下是简单代码实现,选择了压缩成webp,同时将压缩结果存放到运行脚本目录下的output文件夹中,结构和原来的目录相同。手动检查后,可以把output内的内容粘贴到原目录覆盖。
  • 测试后感觉速度还是可以的。
import sharp from 'sharp';
import fs from "fs";
import path from "path";
import chalk from "chalk";

const imageExtensions = ['.jpg', '.jpeg', '.png', '.bmp', '.svg'];
const outputFolder = './output';
const RunFolder = './';

// 递归遍历文件夹
const getImagesInFolder = (folderPath, images = []) => {
  const files = fs.readdirSync(folderPath);
  files.forEach((file) => {
    const filePath = path.resolve(folderPath, file);
    const stat = fs.statSync(filePath);
    if (stat.isDirectory()) {
      getImagesInFolder(filePath, images);
    } else if (isImageFile(file)) {
      images.push(filePath);
    }
  });
  return images;
};
const isImageFile = (filename) => {
  const ext = path.extname(filename).toLowerCase();
  return imageExtensions.includes(ext);
};


const imageSizeFormat = (imageSize) => {
  if (imageSize < 1024) {
    return `${imageSize} B`;
  } else if (imageSize < 1048576) {
    const sizeInKB = (imageSize / 1024).toFixed(2);
    return `${sizeInKB} KB`;
  } else {
    const sizeInMB = (imageSize / 1048576).toFixed(2);
    return `${sizeInMB} MB`;
  }
};


// 获取文件大小
const getImageSize = (filePath) => {
  const stats = fs.statSync(filePath);
  const fileSizeInBytes = stats.size;
  return fileSizeInBytes;
};

// 计算百分比变化
const calculatePercentageChange = (oldSize, newSize) => {
  const percentageChange = ((newSize - oldSize) / oldSize) * 100;
  if (percentageChange > 0)
    return "↑" + percentageChange.toFixed(2) + "%";
  else
    return "↓" + (-percentageChange.toFixed(2)) + "%";
};


const sharpComp = (inputPath) => {
  // 计算输出路径,没有则进行创建
  const outputFolderPath = path.join(outputFolder, path.dirname(path.relative(RunFolder, inputPath)));
  const outputPath = path.join(outputFolderPath, path.basename(inputPath));
  if (!fs.existsSync(outputFolderPath)) {
    fs.mkdirSync(outputFolderPath, { recursive: true });
  }

  sharp(inputPath).webp({ quality: 75, effort: 5 }).toFile(outputPath, (err) => {
    if (err) {
      console.error(err);
    }
    const oldImageSize = getImageSize(inputPath);
    const newImageSize = getImageSize(outputPath);
    const compPercent = calculatePercentageChange(oldImageSize, newImageSize);
    console.log(chalk.cyan(inputPath) + ": " + chalk.yellow(imageSizeFormat(oldImageSize)));
    console.log("├─ " + chalk.cyan(outputPath) + ": " + chalk.yellow(imageSizeFormat(newImageSize)));
    if (compPercent.startsWith('↓')) {
      console.log("└─ " + chalk.green(`[${compPercent}]`) + "\n");
    } else if (compPercent.startsWith('↑')) {
      console.log("└─ " + chalk.red(`[${compPercent}]`) + "\n");
    } else {
      console.log("└─ " + `[${compPercent}]` + "\n");
    }
  });
};

const startCompImage = () => {
  const imagePaths = getImagesInFolder(RunFolder);
  console.log('all image paths:', imagePaths);
  imagePaths.forEach(item => sharpComp(item));
};

startCompImage();

3.Squoosh网站+Selenium

  • Squoosh是Google开发的图像压缩工具。支持多种压缩格式,同时自定义选项也非常多。而且压缩比也比较高。测试了它的AVIF格式感觉挺不错。

  • 它还有一个API库@squoosh/lib和一个cli库@squoosh/cli。但是根据这两个项目npm上的介绍,目前已经不再维护了。我简单测试了这两个库的使用,结果是遇到了一些bug,不想再折腾了。

  • 如果这两个库确实无法使用,因为官方网站还是可以使用,所以也可以使用selenium自动化操作。

  • 以下是使用nodejs的selenium库操作的简单代码实现,项目需安装pnpm i chromedriver selenium-webdriver chalk 。当然使用python肯定也是可以的。

    • 这里使用了谷歌浏览器驱动,并且是npm安装形式,如果本地已经安装过应该是不需要再安装这个chromedriver了。
    • 还是选择了压缩成webp
    • 同时为了获取Squoosh网站生成的图片,选择了注入脚本执行请求blob链接转成base64获取结果,最后再转换成buffer写入到指定路径。当然也可以直接获取下载按钮模拟点击下载,会方便一些。
  • 测试后,压缩效果和sharp差不多,而且因为使用到了selenium,可能会不稳定,并且在使用代理的情况下速度也不快。

import 'chromedriver';
import { Builder, By, until } from 'selenium-webdriver';
import fs from 'fs';
import path from "path";
import chalk from 'chalk';
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'process';

const imageExtensions = ['.jpg', '.jpeg', '.png', '.bmp', '.svg'];
const outputFolder = './output';
const RunFolder = './';
const rl = readline.createInterface({ input, output });
const driver = new Builder().forBrowser('chrome').build();

// blob图片链接转base64编码脚本
const injectScript = function (blobUrl) {
  console.log('arguments', arguments);
  var uri = arguments[0];
  var callback = arguments[arguments.length - 1];
  var toBase64 = function (buffer) {
    for (var r, n = new Uint8Array(buffer), t = n.length, a = new Uint8Array(4 * Math.ceil(t / 3)), i = new Uint8Array(64), o = 0, c = 0; 64 > c; ++c)
      i[c] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charCodeAt(c); for (c = 0; t - t % 3 > c; c += 3, o += 4)r = n[c] << 16 | n[c + 1] << 8 | n[c + 2], a[o] = i[r >> 18], a[o + 1] = i[r >> 12 & 63], a[o + 2] = i[r >> 6 & 63], a[o + 3] = i[63 & r]; return t % 3 === 1 ? (r = n[t - 1], a[o] = i[r >> 2], a[o + 1] = i[r << 4 & 63], a[o + 2] = 61, a[o + 3] = 61) : t % 3 === 2 && (r = (n[t - 2] << 8) + n[t - 1], a[o] = i[r >> 10], a[o + 1] = i[r >> 4 & 63], a[o + 2] = i[r << 2 & 63], a[o + 3] = 61), new TextDecoder("ascii").decode(a);
  };
  var xhr = new XMLHttpRequest();
  xhr.responseType = 'arraybuffer';
  xhr.onload = function () { callback(toBase64(xhr.response)); };
  xhr.onerror = function () { callback(xhr.status); };
  xhr.open('GET', uri);
  xhr.send();
};


const chromeAutoOperation = async (inputPath) => {
  await driver.get('https://squoosh.app');
  await driver.findElement(By.className("_hide_vzxu7_18")).sendKeys(inputPath);
  await driver.wait(until.elementsLocated(By.className("_builtin-select_1onzk_5")), 5000);
  const selects = await driver.findElements(By.className("_builtin-select_1onzk_5"));
  await selects[1].sendKeys("WebP");
  try {
    await driver.wait(until.elementsLocated(By.className("_spinner-container_18q42_141")), 1000);
    const load = await driver.findElement(By.className("_spinner-container_18q42_141"));
    await driver.wait(until.stalenessOf(load));
  } catch (error) {
    // if (!(error instanceof TimeoutError))
    //   throw error;
    // if (!(error instanceof NoSuchElementError))
    //   throw error;
  }

  // 获取blob链接
  await driver.wait(until.elementsLocated(By.xpath('//*[@id="app"]/div/file-drop/div/div[4]/div[2]/a')), 1000);
  const link = await driver.findElement(By.xpath('//*[@id="app"]/div/file-drop/div/div[4]/div[2]/a'));
  let blobLink = null;
  const timeout = 2000;
  const startTime = Date.now();
  while (blobLink === null && (Date.now() - startTime) < timeout) {
    blobLink = await link.getAttribute("href");
  }
  if (blobLink === null) throw new Error('blob link not found');

  // 获取图片大小等文本
  const oldImageSize = await driver.findElement(By.xpath('//*[@id="app"]/div/file-drop/div/div[3]/div[2]/div[2]/div/div[1]/div')).getText();
  const newImageSize = await driver.findElement(By.xpath('//*[@id="app"]/div/file-drop/div/div[4]/div[2]/div[2]/div/div[1]/div')).getText();
  let compPercent = await driver.findElement(By.xpath('//*[@id="app"]/div/file-drop/div/div[4]/div[2]/div[2]/div/div[2]/div')).getText();
  compPercent = compPercent.replace(/\n/g, '');

  // 注入脚本请求获得图片buffer
  const base64Res = await driver.executeAsyncScript(injectScript, blobLink);
  if (typeof base64Res === 'number') throw new Error('request failed, code: ' + base64Res);
  const buffer = Buffer.from(base64Res, 'base64');

  // 计算输出路径,没有则进行创建
  const outputFolderPath = path.join(outputFolder, path.dirname(path.relative(RunFolder, inputPath)));
  const outputPath = path.join(outputFolderPath, path.basename(inputPath));
  if (!fs.existsSync(outputFolderPath)) {
    fs.mkdirSync(outputFolderPath, { recursive: true });
  }

  // 写入图片并打印结果
  fs.writeFile(outputPath, buffer, function (err) {
    if (err) { console.log(err); }
    console.log(chalk.cyan(inputPath) + ": " + chalk.yellow(oldImageSize));
    console.log("├─ " + chalk.cyan(outputPath) + ": " + chalk.yellow(newImageSize));
    if (compPercent.startsWith('↓')) {
      console.log("└─ " + chalk.green(`[${compPercent}]`) + "\n");
    } else if (compPercent.startsWith('↑')) {
      console.log("└─ " + chalk.red(`[${compPercent}]`) + "\n");
    } else {
      console.log("└─ " + `[${compPercent}]` + "\n");
    }
  });
};


// 递归遍历文件夹
const getImagesInFolder = (folderPath, images = []) => {
  const files = fs.readdirSync(folderPath);
  files.forEach((file) => {
    const filePath = path.resolve(folderPath, file);
    const stat = fs.statSync(filePath);
    if (stat.isDirectory()) {
      getImagesInFolder(filePath, images);
    } else if (isImageFile(file)) {
      images.push(filePath);
    }
  });
  return images;
};
const isImageFile = (filename) => {
  const ext = path.extname(filename).toLowerCase();
  return imageExtensions.includes(ext);
};


const startCompImage = async () => {
  const imagePaths = getImagesInFolder(RunFolder);
  console.log('all image paths:', imagePaths);
  for (const inputPath of imagePaths) {
    try {
      await chromeAutoOperation(inputPath);
    } catch (error) {
      console.error(chalk.red(`processing image with path ${inputPath} failed\n`), error);
    }
  }
};

startCompImage();

rl.on("close", async () => {
  await driver.close();
  process.exit(0);
});
  • 总结:还是使用sharpjs方案简单方便
  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dihw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值