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 希望你能有所收獲 😁


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM