From ac8c888bf70e6295c3da698553d6902b2ed1426e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=B6=A9=EC=9D=BC?= Date: Mon, 25 Sep 2023 02:32:24 +0900 Subject: [PATCH] feat(image): add extractColors (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary a new feature in `@fepack/image` which allows users to extract a palette of colors from a given image URL. The extraction can be customized based on user-defined parameters such as the number of colors to extract and the quality of extraction. ![code](https://github.com/fepack/image/assets/77133565/0038c307-971d-417b-8f1e-df54b4957a83) 스크린샷 2023-09-22 오전 12 56 18 ## Checks Please check the following: - [x] I have written documents and tests, if needed. --------- Co-authored-by: Jonghyeon Ko --- .changeset/fuzzy-impalas-applaud.md | 5 ++ configs/eslint-config-js/noimport.js | 4 ++ packages/image/src/extractRGBAs.spec.ts | 28 ++++++++++++ packages/image/src/extractRGBAs.ts | 61 +++++++++++++++++++++++++ packages/image/src/index.ts | 1 + 5 files changed, 99 insertions(+) create mode 100644 .changeset/fuzzy-impalas-applaud.md create mode 100644 packages/image/src/extractRGBAs.spec.ts create mode 100644 packages/image/src/extractRGBAs.ts diff --git a/.changeset/fuzzy-impalas-applaud.md b/.changeset/fuzzy-impalas-applaud.md new file mode 100644 index 0000000..cdf02b2 --- /dev/null +++ b/.changeset/fuzzy-impalas-applaud.md @@ -0,0 +1,5 @@ +--- +"@fepack/image": minor +--- + +feat: add add extractRGBAs diff --git a/configs/eslint-config-js/noimport.js b/configs/eslint-config-js/noimport.js index 8aec20e..732b293 100644 --- a/configs/eslint-config-js/noimport.js +++ b/configs/eslint-config-js/noimport.js @@ -18,4 +18,8 @@ module.exports = { extends: ["plugin:vitest/recommended"], }, ], + rules: { + "arrow-body-style": ["warn", "as-needed"], + "newline-before-return": "warn", + }, }; diff --git a/packages/image/src/extractRGBAs.spec.ts b/packages/image/src/extractRGBAs.spec.ts new file mode 100644 index 0000000..0c8197e --- /dev/null +++ b/packages/image/src/extractRGBAs.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { extractRGBAs } from "."; + +const load = (src: HTMLImageElement["src"]) => + new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => resolve(image); + image.onerror = () => reject(); + image.src = src; + }); + +describe("extractRGBAs", () => { + it("should return an array of colors", async () => { + const image = await load("src/images/test.png"); + const result = extractRGBAs(image, { quality: 10 }); + expect(result.length).toBeGreaterThan(0); + }); + + it("should throw an error if quality is out of range", async () => { + const image = await load("src/images/test.png"); + expect(() => extractRGBAs(image, { quality: 101 })).toThrow( + "options.quality should be between 1 and 100", + ); + expect(() => extractRGBAs(image, { quality: 0 })).toThrow( + "options.quality should be between 1 and 100", + ); + }); +}); diff --git a/packages/image/src/extractRGBAs.ts b/packages/image/src/extractRGBAs.ts new file mode 100644 index 0000000..eea0608 --- /dev/null +++ b/packages/image/src/extractRGBAs.ts @@ -0,0 +1,61 @@ +type RGBA = readonly [number, number, number, number]; +type ExtractRGBAsOptions = { quality: number }; +const initialOptions: ExtractRGBAsOptions = { quality: 100 }; +/** + * Filters and extracts relevant pixels from image data based on quality setting. + */ +export const extractRGBAs = ( + /** + * The image pixel data. + */ + image: HTMLImageElement, + /** + * Quality setting for filtering pixels. Higher values mean more pixels are processed. Range: [1, 100] + */ + options = initialOptions, +) => { + if (options.quality < 1 || options.quality > 100) { + throw new Error("options.quality should be between 1 and 100"); + } + const uint8ClampedArray = extractUint8ClampedArray(image); + const step = Math.ceil(100 / options.quality); + + return Array.from({ + length: Math.ceil(uint8ClampedArray.length / (4 * step)), + }).map((_, index) => { + const rIndex = index * 4 * step; + + return [ + uint8ClampedArray[rIndex], + uint8ClampedArray[rIndex + 1], + uint8ClampedArray[rIndex + 2], + uint8ClampedArray[rIndex + 3], + ] as RGBA; + }); +}; + +/** + * Extracts pixel data from an image. + */ +const extractUint8ClampedArray = ( + /** + * The image element + */ + image: HTMLImageElement, +) => { + const canvas = document.createElement("canvas"); + const canvasRenderingContext2D = canvas.getContext("2d"); + if (!canvasRenderingContext2D) { + throw new Error("canvasRenderingContext2D is not supported"); + } + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + canvasRenderingContext2D.drawImage(image, 0, 0); + + return canvasRenderingContext2D.getImageData( + 0, + 0, + canvas.width, + canvas.height, + ).data; +}; diff --git a/packages/image/src/index.ts b/packages/image/src/index.ts index d6f9a58..144c379 100644 --- a/packages/image/src/index.ts +++ b/packages/image/src/index.ts @@ -1,3 +1,4 @@ export { checkWebPSupport } from "./checkWebPSupport"; export { detect } from "./detect"; +export { extractRGBAs } from "./extractRGBAs"; export { load, type ImageSource } from "./load";