Color Thief 找出圖片裡的顏色
Tags: javascript, tutorial, image, color
Published: 2022-08-25 15:01:41

## 本篇要解決的問題

原本以為辨識圖片裡的顏色，是需要 AI 還是什麼超越柯南智商才能做的功能，前陣子好奇查了一下，才發現原來前端就可以做到了！原理是 `canvas` 有一個 [getImageData](https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/getImageData) 的 function，可以抓出每個 pixel 的 RGBA 資訊，再把所有取到的 RGBA 資訊做一個連一年級小學生柯南也算不出的數學處理，就可以算出圖片的色碼出來。

然後，對，工程師的世界裡充滿著維護世界和平的大神，會把這些高難度的數學動作給做成套件。

本篇要使用的是 Color Thief 這個套件，實作一個取出圖片裡色碼的功能，也會延伸做一個讓使用者自己選擇圖片檔案後，顯示出這個圖檔的主色碼以及其他色碼。

推薦 Color Thief 的原因：

- 支援 Browser、Node.js
- 文件清楚明暸
- 簡單好用、範例清晰（讚讚）
- 開源免費

Color Thief GitHub：

<https://github.com/lokesh/color-thief/>

Color Thief 說明文件、Demo：

<https://lokeshdhakar.com/projects/color-thief/>

這邊再提供二個相關資源，是 August 找資料時覺得很有用的資訊。

- 原理說明：[Extracting a color palette from an image with javascript](https://dev.to/producthackers/creating-a-color-palette-with-javascript-44ip)
- 相關套件：[Rgbaster.js](https://github.com/briangonzalez/rgbaster.js)

本篇最後完成的 Demo：

<https://letswritetw.github.io/letswrite-color-thief/>

最近想開始碰 Vue.js 3，所以 Demo 的原始檔，JS 是用 Vue 3 寫的。

---

## Color Thief 基本使用

### 引用 JS

Color Thief 的 JS 檔共有四個：

- color-thief.js：Node.js 用。
- color-thief.mjs：ES6 import 用，可用於 Webpack 和 Rollup。
- color-thief.umd.js、color-thief.min.js：直接當 CDN 引用。

最簡單的方式就是直接 [CDN](https://cdnjs.com/libraries/color-thief) 引用：

```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
```

本篇 Demo 用的是 ES6，終端機執行 `yarn add colorthief` 後，引用方式如下：

```javascript
import ColorThief from './../node_modules/colorthief/dist/color-thief.mjs';
```

Color Thief 的好用之處在於，它來來去去也就二個 function，不用看那種落落長到天荒地老的文件。

以下說明二個 function：`getColor`、`getPalette`。

### getColor 取得主要色碼

`getColor` 是取得圖檔的主要色碼，也就是圖檔裡最多使用到的顏色的色碼。

```javascript
getColor(image [, quality])
// => [R, G, B]
```

`image`：要給的是 image 本身，而不是給圖檔的路徑。

比方我們引用圖片進來的 HTML 如下：

```html
<img id="i_am_image" src="xxx.jpg">
```

那 `image` 要給的不是 `xxx.jpg`，而是：

```javascript
const img = document.getElementById('i_am_image');
```

`quality`：可選的參數，給的值是大於 1 的整數，默認值是 `10`。

`quality` 代表的意思是「每幾個 pixel 執行一次抓值？」，開頭說過，前端判斷圖片的色碼，是把每一個 pixel 抓出來並取得它的 RGBA 值後，做一堆數學運算去算出最常被使用到 RGBA。

如果一張圖只有 10px * 10px，每一個 pixel 都抓就有 100 個 pixel 要抓要計算。

那如果今天圖片是 1024px * 1024px 呢？甚至更大呢？每一個 pixel 都抓就會很耗時間。

`quality` 就是設定每幾個 pixel 抓一次，默認值 `10` 就是每 10 個 pixel 抓一次。所以設為 `1` 時最精準，但如果圖檔很大回傳的速度就會很慢。

### getPalette 取得調色盤

一張圖除了主要色碼，也會用到其它色碼。`getPalette` 就是把這些顏色都抓出來。

```javascript
getPalette(image [, colorCount, quality]
// => [[R, G, B], [R, G, B], ... ]
```

`image`、`quality` 這二個參數跟 `getColor` 相同，這邊不再重覆說明。

`colorCount`：要抓出幾個顏色出來，默認值是 `10`。

回傳的結果是一個陣列，裡面包有指定數量（colorCount）的陣列，每個陣列的三個值就是 R、G、B。

### 使用範例

這邊來抓一張圖片，取它的主要色碼跟其它調色盤中的顏色色碼。

圖片是從 [Lorem Picsum](https://picsum.photos/) 找的，這也是一個很方便的工具，可以隨機生成圖片，切版時需要用到假圖的話很好用。

Color Thief 官方文件提供的範例是這樣：

```javascript
const colorThief = new ColorThief();
const img = document.querySelector('img');

if (img.complete) {
  colorThief.getColor(img);
  colorThief.getPalette(img);
} else {
  image.addEventListener('load', function() {
    colorThief.getColor(img);
    colorThief.getPalette(img);
  });
}
```

`img.complete`：確保圖片載入完成。

本篇 Demo 稍微修改一下，把確保圖片載入完成這段，寫成一個 Vue 的 methods。

```javascript
// ...
methods: {
  // 確定圖片載入完成
  waitImageLoad(img) {
    return new Promise((resolve, reject) => {
      let timer;
      timer = () => {
        setTimeout(() => {
          if(img.complete) resolve(true);
          else timer();
        }, 100);
      }
      timer();
    })
  },
  // 基本使用，取得圖片色碼
  async getColors() {
    const colorThief = new ColorThief();
    const img = document.getElementById('i_am_image');
    await this.waitImageLoad(img);
    let color = this.colorThief.getColor(img);
    let palette = this.colorThief.getPalette(img);
  }
},
mounted() {
  this.getColors();
}
```

Demo 頁有用 Tailwind CSS 修一下，基本的使用最後會看到像這樣子的結果：

![Color Thief 基本使用範例](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jo14pcdn2p2nc9jdw11p.png)

調色盤的部份，August 有另外做一個處理，就是讓顏色從亮排到暗。下面一段來說明。

色碼的部份也從 RGB 改成六進位的 Hex，一樣後面幾段會說明。

---

## 進階使用：排序色碼由亮到暗

這個方式是 August 自行猜測的，還沒有找其它科學實驗來證明 XD。

我們一般用 CSS 寫顏色，黑、白二色會是這樣：

- 黑：#000 || rgb(0, 0, 0)
- 白：#FFF || rgb(255, 255, 255)

從 `rgb` 可以知道，三數加總的結果愈小，顏色愈暗，加總的結果愈大則愈亮。

所以我們只需要把 `rgb` 的三個數字加總起來後，再由大排到小，就可以讓調色盤的色碼是由亮到暗來呈現。

Javascript 中，`reduce` 可以加總陣列裡的數字。嗯…總覺得很違和，reduce 的英文單字意思不是「減少」嗎？JS 使用時卻是累加，覺得好怪 ~ " ~。（[reduce MDN 說明](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce)）

```javascript
sortPalette(array) {
  // 建一個空陣列，把調色盤裡的 RGB 存進去，並存一個三數加總的值
  let tempForCalc = [];
  Array.prototype.forEach.call(array, palette => {
    const sum = palette.reduce((a, b) => a + b, 0);
    const item = {
      color: palette,
      sum: sum
    }
    tempForCalc.push(item);
  });
  
  // 用三數加總的值做 大 -> 小 排序
  const result = tempForCalc.concat().sort((a, b) => {
    return a.sum > b.sum ? -1 : 1;
  });
  return result;
}
```

---

## 進階使用：取得使用者選擇的圖檔

客製一個 `input type="file"` 請看之前寫的筆記文，這邊不再說明：

[File API 客製上傳檔案按鈕 / input file](https://www.letswrite.tw/file-api-custom-input/)

當使用者選好檔案後，有二種方式可以把選擇的圖檔塞進 `img src` 裡：[Blob](https://developer.mozilla.org/zh-TW/docs/Web/API/Blob)、[Base64](https://developer.mozilla.org/en-US/docs/Glossary/Base64)。

本篇 Demo 用的是抓圖檔的 Blob，因為寫的行數比較少。~~（我就廢）~~

轉 Base64 的 method 也有寫在原始碼裡，可以在原始檔裡的 src/main.js 搜尋 `getUploadImgBase64`。

```javascript
async getUploadImgBlob(file) {
  const fileData = file.target.files[0];
  const customImg = window.URL.createObjectURL(fileData);
  
  // 確定 #i_am_image 的圖片載入完成
  // waitImageLoad：前面幾段「確定圖片載入完成」的 method
  const img = document.getElementById('i_am_image');
  await this.waitImageLoad(img);

  // 執行 colorThief
  const colorThief = new ColorThief();
  const color = colorThief.getColor(img, 5);
  
  // sortPalette：前面幾段「排序色碼由亮到暗」的 method
  let tempPalette = colorThief.getPalette(img, 10, 5);
  const palette = this.sortPalette(tempPalette);
},
```

---

## 進階使用：RGB 轉 Hex 色碼

我們在寫 CSS 的時候，寫顏色的部份很少是寫 RGB，大部份是寫像「#000000」的六進位 Hex 色碼，好啦，至少 August 的習慣是這樣。

官方文件有提供 RGB 轉成 Hex 的方法：

```javascript
const rgbToHex = (r, g, b) => '#' + [r, g, b].map(x => {
  const hex = x.toString(16)
  return hex.length === 1 ? '0' + hex : hex
}).join('')

rgbToHex(102, 51, 153); // #663399
```

本篇 Demo 把這段 function 改為 Vue 的 method，渲染時會把得到的 RGB 轉成 Hex：

```javascript
toHex(colors) {
  let hex = colors.map(x => {
    return x.toString(16);
  }).join('');
  return '#' + hex
}
```

---

## 本篇原始碼及 Demo

本篇的原始碼有放在 GitHub 上，也用 GitHub Pages 生成 Demo 頁。

取用之前麻煩分享本篇文章，或是在 GitHub 上點個星星，你的一個小小動作對本站都是大大的鼓勵。

原始碼：

<https://github.com/letswritetw/letswrite-color-thief>

Demo：

<https://letswritetw.github.io/letswrite-color-thief/>