node實現批量修改圖片尺寸

前言

大家在工作中肯定有沒有遇到過圖片尺寸和我們要求的尺寸不一致的情況吧?通常我們會在網上找一下找在線的或者下載一個小工具,再或者通過ps的批處理解決。但是,作為程式猿,當然還是通過程式碼來解決這種小問題啦。所以,閑話不多說啦,開始寫我們的程式碼啦~~

簡單的搭建一下

  • 新建一個 canvas-image-resize 目錄

  • 初始化一個node項目工程

    npm init -y
    
  • 安裝依賴,這裡主要用到了三個依賴,分別是處理圖片批量處理文件壓縮成zip文件

    npm i canvas glob archiver -S
    

    沒錯,這裡我們又用到了canvas這個庫,驚不驚喜,意不意外 😂

簡單的使用一下

同樣,有了前面我們使用canvas的經驗,書寫這個程式碼應該問題也不大,主要是對api的熟練問題

查看文檔我們不難發現,drawImage的第四和第五個參數就是設置圖片的寬高,知道這個之後,我們書寫程式碼就簡單不少了

drawImage(image: Canvas|Image, dx: number, dy: number, dw: number, dh: number): void

所以,我們的程式碼大概如下,

// 創建寫入流
const { createWriteStream } = require("fs");
// 獲取文件名
const { basename } = require("path");
// 壓縮文件
const archiver = require("archiver");
// 導入canvas庫,用於裁剪圖片
const { createCanvas, loadImage } = require("canvas");
// 批量獲取路徑
const glob = require("glob");
!(async () => {
  const paths = glob.sync("./images/*");
  // 壓縮成zip
  const archive = archiver("zip", {
    zlib: { level: 9 }, // Sets the compression level.
  });
  // 輸出到當前文件夾下的 image-resize.zip
  const output = createWriteStream(__dirname + "/image-resize.zip");
  archive.pipe(output);
  for (let i = 0; i < paths.length; i++) {
    const path = paths[i];
    const image = await loadImage(path);
    const { width, height } = image;
    const options = [width, height].map((item) => item / 2);
    const canvas = createCanvas(...options);
    const ctx = canvas.getContext("2d");
    ctx.drawImage(image, 0, 0, ...options);
    archive.append(canvas.toBuffer(), { name: `${basename(path)}` });
  }
  archive.finalize();
})();

從上面程式碼可以看出,這裡我只是對寬高進行了縮放一倍,沒有做更多的配置,為了程式碼的健壯性,我們修改下我們的options,使得整個程式可以自定義寬高、可以根據寬度進行縮放、根據高度進行縮放

定義一下我們可配置的參數,基本配置是這樣的:

module.exports = {
  // 自定義寬度,傳一個根據寬度等比縮放
  width: "",
  // 自定義高度,傳一個根據高度等比縮放
  height: "",
  // 根據寬度等比縮放,優先順序更高
  isWidth: false,
  // 根據高度等比縮放
  isHeight: false,
  // 寬高整體縮放
  scale: 1,
};

ps:因為我們暫時沒有圖形介面,所以就定義一個config.js來模擬我們的插件啦

所以,在當前目錄下,新建一個config.js,書寫上我們那些配置,然後在app.js導入下,基本程式碼就變成了如下:

// ....
// 導入配置文件(用戶傳過來的配置)
const config = require("./config");
// 根據配置獲取寬高
function getOptions(options, config) {
  // 書寫配置相關的程式碼,默認縮放兩倍
  return options.map((item) => item / 2);
}
!(async () => {
  //  ....
  for (let i = 0; i < paths.length; i++) {
    const path = paths[i];
    const image = await loadImage(path);
    const { width, height } = image;
    const options = getOptions({ width, height }, config);
    const canvas = createCanvas(...options);
    const ctx = canvas.getContext("2d");
    ctx.drawImage(image, 0, 0, ...options);
    archive.append(canvas.toBuffer(), { name: `${basename(path)}` });
  }
  //  ....
})();

然後根據我們的配置文件來寫邏輯的話,大概會出現如下邏輯:

// 根據配置獲取寬高
function getOptions(options, config) {
  const [sourceWidth, sourceHeight] = options;
  const { width, height, isWidth, isHeight, scale } = config;
  if (width === 0 || height === 0) return [0, 0];
  if (width && height) {
    if (isWidth) {
      return [width, (sourceHeight * width * scale) / sourceWidth];
    }
    if (isHeight) {
      return [(sourceWidth * height * scale) / sourceHeight, height];
    }
    return [width / scale, height / scale];
  }
  if (width && !height) {
    return [width, (sourceHeight * width * scale) / sourceWidth];
  }
  if (height && !width) {
    return [(sourceWidth * height * scale) / sourceHeight, height];
  }
  return options.map((item) => item / scale);
}

發現了嗎?是不是感覺很亂?就算我們把一些公有部分提取出來改寫如下:

// 根據配置獲取寬高
function getOptions(options, config) {
  const [sourceWidth, sourceHeight] = options;
  const { width, height, isWidth, isHeight, scale } = config;
  if (width === 0 || height === 0) return [0, 0];
  const widthOfOptions = [
    width * scale,
    (sourceHeight * width * scale) / sourceWidth,
  ];
  const heightOfOptions = [
    (sourceWidth * height * scale) / sourceHeight,
    height * scale,
  ];
  if (width && height) {
    if (isWidth) {
      return widthOfOptions;
    }
    if (isHeight) {
      return heightOfOptions;
    }
    return [width / scale, height / scale];
  }
  if (width && !height) {
    return widthOfOptions;
  }
  if (height && !width) {
    return heightOfOptions;
  }
  return options.map((item) => item / scale);
}

其實就算經過我們這麼優化,其實看起來也不是特別優雅,不知道大家是否還記得我之前的一篇文章 從零搭建 Window 前端開發環境,這裡說過,我們可以使用使用 Map 代替 if/else,讓我們的程式碼變得更優雅,可讀性也更好。所以,接下來我們就通過 Map 來改寫我們的程式碼吧

ps: 如果判斷簡單,其實用{}對象也可以,這裡只是用Map做個延申

思考一下,為什麼用 Map 更好呢?

說到這個,就不得不說 Map 對象和 Object 的區別了,他兩有不少語法上的區別,比如Map獲取值需要get(key),設置值需要set(key,value),但是這些區別不在我們討論的範圍內,我們說說他兩最主要也是最重要的區別:

  • 一個對象的鍵只能是字元串或者 Symbols,但一個 Map 的鍵可以是任意值。
  • Map 自身有 size 屬性,可以自己維護自己的 size,而對象的鍵值對個數只能手動確認。

優化程式碼

知道了他兩的區別後,我們就可以邊寫程式碼啦~~剛剛說到,Mapkey可是是任意值,所以我們就可以使用正則類型(RegExp)來作為我們的key了,而正是因為有了正則,那麼我們的判斷就有了無限可能,可以適應各種情況。

思考一下怎麼通過正則來實現我們的程式碼呢?

首先我們可以先觀察下我們之前if/else這個版本的程式碼,最先判斷的是不是有沒有寬高,即寬高是否為 0,所以我們就可以通過這個條件把我們的判斷改為布爾值,因為js是弱類型的,所以我們就可以用0或者1來表示了,又因為這裡存在不傳則根據傳的值縮放的情況,所以我們需要額外判斷當他為空字元串時取01之外的數字,這裡我取的是2

這裡可能有點繞口,我舉兩個例子大家可能就就懂了,假如我們傳入的數據為默認數據:

module.exports = {
  // 自定義寬度,傳一個根據寬度等比縮放
  width: "",
  // 自定義高度,傳一個根據高度等比縮放
  height: "",
  // 根據寬度等比縮放,優先順序更高
  isWidth: false,
  // 根據高度等比縮放
  isHeight: false,
  // 寬高整體縮放
  scale: 1,
};

那麼得出的字元串就是22001,假設我們傳入了寬度,即數據:

module.exports = {
  // 自定義寬度,傳一個根據寬度等比縮放
  width: 1920,
  // 自定義高度,傳一個根據高度等比縮放
  height: "",
  // 根據寬度等比縮放,優先順序更高
  isWidth: false,
  // 根據高度等比縮放
  isHeight: false,
  // 寬高整體縮放
  scale: 1,
};

那麼得出的字元串就是12001,看到這裡大家應該懂了吧?所以我們只需要判斷config這個配置的value值來生成我們的字元串即可,即得出如下程式碼

// 獲取config字元串,即傳入了就是true,即1,沒傳就是0,為空字元串就是2
function getConfigStr(config) {
  return Object.values(config).map((el) => (el === "" ? "2" : Number(!!el)));
}

ps:如果不懂,請評論說出來,我看到會第一時間回復的。。。

拓展閱讀:object 屬性的輸出順序是無序的問題了解

拓展閱讀:5 分鐘徹底理解 Object.keys

正式編寫優化後的程式碼

通過上面的思考,我們基本分析出了我們的程式碼需要怎麼寫,如何寫,我想大家應該很容易就能書寫出來了,這裡還是貼一下我的(僅供參考):

// 獲取config字元串
function getConfigStr(config) {
  return Object.values(config).map((el) => (el === "" ? "2" : Number(!!el)));
}
// 根據配置獲取寬高
function getOptions(options, config) {
  const [sourceWidth, sourceHeight] = options;
  const { width, height, scale } = config;
  const widthOfOptions = [
    width * scale,
    (sourceHeight * width * scale) / sourceWidth,
  ];
  const heightOfOptions = [
    (sourceWidth * height * scale) / sourceHeight,
    height * scale,
  ];
  const configStr = getConfigStr(config);
  const map = new Map([
    [/^0|^\d0/, [0, 0]],
    [/^1\d1|^1[0|2]0/, widthOfOptions],
    [/^\d101|^210/, heightOfOptions],
    [/^1100/, [width / scale, height / scale]],
    [/^2{2}\d{2}1/, options.map((item) => item / scale)],
  ]);
  return [...map].find(([key]) => key.test(configStr.join("")))[1] || options;
}

ps: 這裡使用了正則,如果有對正則不太了解的,建議可以去看下正則,因為正則對字元串的處理有著極大的意義,以極大程度上方便了我們的開發

也許你看到這裡,你就會像,你這裡寫的不是比以前更複雜了嗎?還用了這些看不太懂的正則,可讀性就更差了。。。

但是其實我這裡只是想引申出使用 Map 代替 if/else這個思想(思路),通過這個例子,我想以後我們寫的程式碼也可以使用Map書寫出讓我們更好維護的程式碼了

gitee 地址,github 地址

最後

感謝各位觀眾老爺的觀看 O(∩_∩)O 希望你能有所收穫 😁