From b580aa17339eecc6532ecc054c0237b8dbfc3572 Mon Sep 17 00:00:00 2001 From: Vlad Shilov <omgovich@ya.ru> Date: Sun, 18 Jul 2021 14:54:57 +0300 Subject: [PATCH] Parse CMYK strings, update specs --- CHANGELOG.md | 12 +++-- README.md | 27 ++++++++-- package.json | 12 ++++- src/colorModels/cmyk.ts | 92 +++++++++++++++-------------------- src/colorModels/cmykString.ts | 32 ++++++++++++ src/plugins/cmyk.ts | 5 +- tests/plugins.test.ts | 8 ++- 7 files changed, 124 insertions(+), 64 deletions(-) create mode 100644 src/colorModels/cmykString.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 455d760..c6fc406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 2.2.0 + +- New plugin: CMYK color space ❤️ [@EricRovell](https://github.com/EricRovell) + ### 2.1.0 - Add new `hue` and `rotate` methods @@ -51,8 +55,8 @@ ### 1.2.1 -- Fix: Do not treat 7-digit hex as a valid color ❤️ @subzey -- Parser update: Turn NaN input values into valid numbers ❤️ @subzey +- Fix: Do not treat 7-digit hex as a valid color ❤️ [@subzey](https://github.com/subzey) +- Parser update: Turn NaN input values into valid numbers ❤️ [@subzey](https://github.com/subzey) ### 1.2.0 @@ -72,7 +76,7 @@ ### 0.10.2 -- Sort named colors dictionary for better compression ❤️ @subzey +- Sort named colors dictionary for better compression ❤️ [@subzey](https://github.com/subzey) ### 0.10.1 @@ -109,7 +113,7 @@ ### 0.6.2 -- 20% speed improvement ❤️ @jeetiss +- 20% speed improvement ❤️ [@jeetiss](https://github.com/jeetiss) ### 0.6.1 diff --git a/README.md b/README.md index e429bdd..0adfa24 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,25 @@ colord("#555aaa").toCmyk(); // { c: 50, m: 47, y: 0, k: 33, a: 1 } </details> +<details> + <summary><b><code>.toCmykString()</code></b> (<b>cmyk</b> plugin)</summary> + +Converts a color to color space. + +Converts a color to [CMYK](https://en.wikipedia.org/wiki/CMYK_color_model) color space and expresses it through the [functional notation](https://www.w3.org/TR/css-color-4/#device-cmyk) + +```js +import { colord, extend } from "colord"; +import cmykPlugin from "colord/plugins/cmyk"; + +extend([cmykPlugin]); + +colord("#99ffff").toCmykString(); // "device-cmyk(40% 0% 0% 0%)" +colord("#00336680").toCmykString(); // "device-cmyk(100% 50% 0% 60% / 0.5)" +``` + +</details> + <details> <summary><b><code>.toHwb()</code></b> (<b>hwb</b> plugin)</summary> @@ -655,9 +674,9 @@ colord("#e60000").isReadable("#ffff47", { level: "AAA", size: "large" }); // tru </details> <details> - <summary><b><code>cmyk</code> (CMYK color space)</b> <i>1.38 KB</i></summary> + <summary><b><code>cmyk</code> (CMYK color space)</b> <i>0.6 KB</i></summary> -Adds support of [CMYK](https://www.sttmedia.com/colormodel-cmyk) color model. The conversion logic is ported from [CSS Color Module Level 4 Specification](https://www.w3.org/TR/css-color-4/#color-conversion-code). +Adds support of [CMYK](https://www.sttmedia.com/colormodel-cmyk) color model. ```js import { colord, extend } from "colord"; @@ -666,7 +685,9 @@ import cmykPlugin from "colord/plugins/cmyk"; extend([cmykPlugin]); colord("#ffffff").toCmyk(); // { c: 0, m: 0, y: 0, k: 0, a: 1 } +colord("#999966").toCmykString(); // "device-cmyk(0% 0% 33% 40%)" colord({ c: 0, m: 0, y: 0, k: 100, a: 1 }).toHex(); // "#000000" +colord("device-cmyk(0% 61% 72% 0% / 50%)").toHex(); // "#ff634780" ``` </details> @@ -833,4 +854,4 @@ const bar: RgbColor = { r: 0, g: 0, v: 0 }; // ERROR - [x] [LAB](https://www.w3.org/TR/css-color-4/#resolving-lab-lch-values) color space (via plugin) - [x] [LCH](https://lea.verou.me/2020/04/lch-colors-in-css-what-why-and-how/) color space (via plugin) - [x] Mix colors (via plugin) -- [ ] CMYK color space (via plugin) +- [x] CMYK color space (via plugin) diff --git a/package.json b/package.json index db5ab4b..8712274 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "colord", - "version": "2.1.0", + "version": "2.2.0", "description": "👑 A tiny yet powerful tool for high-performance color manipulations and conversions", "keywords": [ "color", @@ -18,6 +18,7 @@ "css", "color-names", "a11y", + "cmyk", "mix" ], "repository": "omgovich/colord", @@ -37,6 +38,11 @@ "require": "./plugins/a11y.js", "default": "./plugins/a11y.mjs" }, + "./plugins/cmyk": { + "import": "./plugins/cmyk.mjs", + "require": "./plugins/cmyk.js", + "default": "./plugins/cmyk.mjs" + }, "./plugins/hwb": { "import": "./plugins/hwb.mjs", "require": "./plugins/hwb.js", @@ -140,6 +146,10 @@ "path": "dist/plugins/a11y.mjs", "limit": "0.5 KB" }, + { + "path": "dist/plugins/cmyk.mjs", + "limit": "1 KB" + }, { "path": "dist/plugins/hwb.mjs", "limit": "1 KB" diff --git a/src/colorModels/cmyk.ts b/src/colorModels/cmyk.ts index 14caaea..5840c90 100644 --- a/src/colorModels/cmyk.ts +++ b/src/colorModels/cmyk.ts @@ -5,44 +5,35 @@ import { clamp, isPresent, round } from "../helpers"; /** * Clamps the CMYK color object values. */ -export function clampCmyka({ c, m, y, k, a = 1 }: CmykaColor): CmykaColor { - return { - c: clamp(c, 0, 100), - m: clamp(m, 0, 100), - y: clamp(y, 0, 100), - k: clamp(k, 0, 100), - a: clamp(a), - }; -} +export const clampCmyka = (cmyka: CmykaColor): CmykaColor => ({ + c: clamp(cmyka.c, 0, 100), + m: clamp(cmyka.m, 0, 100), + y: clamp(cmyka.y, 0, 100), + k: clamp(cmyka.k, 0, 100), + a: clamp(cmyka.a), +}); /** * Rounds the CMYK color object values. */ -export function roundCmyka({ c, m, y, k, a = 1 }: CmykaColor): CmykaColor { - return { - c: round(c, 2), - m: round(m, 2), - y: round(y, 2), - k: round(k, 2), - a: round(a, ALPHA_PRECISION), - }; -} +export const roundCmyka = (cmyka: CmykaColor): CmykaColor => ({ + c: round(cmyka.c, 2), + m: round(cmyka.m, 2), + y: round(cmyka.y, 2), + k: round(cmyka.k, 2), + a: round(cmyka.a, ALPHA_PRECISION), +}); /** * Transforms the CMYK color object to RGB. * https://www.rapidtables.com/convert/color/cmyk-to-rgb.html */ -export function cmykaToRgba(color: CmykaColor): RgbaColor { - const c = color.c / 100; - const m = color.m / 100; - const y = color.y / 100; - const k = color.k / 100; - const a = color?.a ?? 1; +export function cmykaToRgba(cmyka: CmykaColor): RgbaColor { return { - r: round(255 * (1 - c) * (1 - k)), - g: round(255 * (1 - m) * (1 - k)), - b: round(255 * (1 - y) * (1 - k)), - a: a ? round(a, ALPHA_PRECISION) : 1, + r: round(255 * (1 - cmyka.c / 100) * (1 - cmyka.k / 100)), + g: round(255 * (1 - cmyka.m / 100) * (1 - cmyka.k / 100)), + b: round(255 * (1 - cmyka.y / 100) * (1 - cmyka.k / 100)), + a: cmyka.a, }; } @@ -50,16 +41,18 @@ export function cmykaToRgba(color: CmykaColor): RgbaColor { * Convert RGB Color Model object to CMYK. * https://www.rapidtables.com/convert/color/rgb-to-cmyk.html */ -export function rgbaToCmyka(color: RgbaColor): CmykaColor { - const [r, g, b, a] = [color.r / 255, color.g / 255, color.b / 255, color.a ?? 1]; - const k = 1 - Math.max(r, g, b); - const [c, m, y] = [(1 - r - k) / (1 - k), (1 - g - k) / (1 - k), (1 - b - k) / (1 - k)]; +export function rgbaToCmyka(rgba: RgbaColor): CmykaColor { + const k = 1 - Math.max(rgba.r / 255, rgba.g / 255, rgba.b / 255); + const c = (1 - rgba.r / 255 - k) / (1 - k); + const m = (1 - rgba.g / 255 - k) / (1 - k); + const y = (1 - rgba.b / 255 - k) / (1 - k); + return { - c: Number.isNaN(c) ? 0 : round(c * 100), - m: Number.isNaN(m) ? 0 : round(m * 100), - y: Number.isNaN(y) ? 0 : round(y * 100), + c: isNaN(c) ? 0 : round(c * 100), + m: isNaN(m) ? 0 : round(m * 100), + y: isNaN(y) ? 0 : round(y * 100), k: round(k * 100), - a: round(a, 2), + a: rgba.a, }; } @@ -67,22 +60,15 @@ export function rgbaToCmyka(color: RgbaColor): CmykaColor { * Parses the CMYK color object into RGB. */ export function parseCmyka({ c, m, y, k, a = 1 }: InputObject): RgbaColor | null { - if (isPresent(c) && isPresent(m) && isPresent(y) && isPresent(k)) { - const cmyk = clampCmyka({ - c: Number(c), - m: Number(m), - y: Number(y), - k: Number(k), - a: Number(a), - }); - return cmykaToRgba(cmyk); - } - return null; -} + if (!isPresent(c) || !isPresent(m) || !isPresent(y) || !isPresent(k)) return null; + + const cmyk = clampCmyka({ + c: Number(c), + m: Number(m), + y: Number(y), + k: Number(k), + a: Number(a), + }); -export function cmykToCmykaString(rgb: RgbaColor): string { - const { c, m, y, k, a } = roundCmyka(rgbaToCmyka(rgb)); - return a < 1 - ? `device-cmyk(${c}% ${m}% ${y}% ${k}% / ${a})` - : `device-cmyk(${c}% ${m}% ${y}% ${k}%)`; + return cmykaToRgba(cmyk); } diff --git a/src/colorModels/cmykString.ts b/src/colorModels/cmykString.ts new file mode 100644 index 0000000..3fe48aa --- /dev/null +++ b/src/colorModels/cmykString.ts @@ -0,0 +1,32 @@ +import { RgbaColor } from "../types"; +import { clampCmyka, cmykaToRgba, rgbaToCmyka, roundCmyka } from "./cmyk"; + +const cmykMatcher = /^device-cmyk\(\s*([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i; + +/** + * Parses a valid CMYK CSS color function/string + * https://www.w3.org/TR/css-color-4/#device-cmyk + */ +export const parseCmykaString = (input: string): RgbaColor | null => { + const match = cmykMatcher.exec(input); + + if (!match) return null; + + const cmyka = clampCmyka({ + c: Number(match[1]) * (match[2] ? 1 : 100), + m: Number(match[3]) * (match[4] ? 1 : 100), + y: Number(match[5]) * (match[6] ? 1 : 100), + k: Number(match[7]) * (match[8] ? 1 : 100), + a: match[9] === undefined ? 1 : Number(match[9]) / (match[10] ? 100 : 1), + }); + + return cmykaToRgba(cmyka); +}; + +export function rgbaToCmykaString(rgb: RgbaColor): string { + const { c, m, y, k, a } = roundCmyka(rgbaToCmyka(rgb)); + + return a < 1 + ? `device-cmyk(${c}% ${m}% ${y}% ${k}% / ${a})` + : `device-cmyk(${c}% ${m}% ${y}% ${k}%)`; +} diff --git a/src/plugins/cmyk.ts b/src/plugins/cmyk.ts index 6b4ea59..a3a0ce7 100644 --- a/src/plugins/cmyk.ts +++ b/src/plugins/cmyk.ts @@ -1,7 +1,7 @@ import { CmykaColor } from "../types"; import { Plugin } from "../extend"; import { parseCmyka, roundCmyka, rgbaToCmyka } from "../colorModels/cmyk"; -import { cmykToCmykaString } from "../colorModels/cmyk"; +import { parseCmykaString, rgbaToCmykaString } from "../colorModels/cmykString"; declare module "../colord" { interface Colord { @@ -30,10 +30,11 @@ const cmykPlugin: Plugin = (ColordClass, parsers): void => { }; ColordClass.prototype.toCmykString = function () { - return cmykToCmykaString(this.rgba); + return rgbaToCmykaString(this.rgba); }; parsers.object.push([parseCmyka, "cmyk"]); + parsers.string.push([parseCmykaString, "cmyk"]); }; export default cmykPlugin; diff --git a/tests/plugins.test.ts b/tests/plugins.test.ts index 102ce2a..0b2096b 100644 --- a/tests/plugins.test.ts +++ b/tests/plugins.test.ts @@ -63,6 +63,12 @@ describe("cmyk", () => { expect(colord({ c: 0, m: 0, y: 0, k: 0, a: 1 }).toHex()).toBe("#ffffff"); }); + it("Parses CMYK color string", () => { + expect(colord("device-cmyk(0% 0% 0% 100%)").toHex()).toBe("#000000"); + expect(colord("device-cmyk(0% 61% 72% 0% / 50%)").toHex()).toBe("#ff634780"); + expect(colord("device-cmyk(0 0.61 0.72 0 / 0.5)").toHex()).toBe("#ff634780"); + }); + it("Converts a color to CMYK object", () => { // https://htmlcolors.com/color-converter expect(colord("#000000").toCmyk()).toMatchObject({ c: 0, m: 0, y: 0, k: 100, a: 1 }); @@ -74,7 +80,7 @@ describe("cmyk", () => { }); it("Converts a color to CMYK string", () => { - https://en.wikipedia.org/wiki/CMYK_color_model + // https://en.wikipedia.org/wiki/CMYK_color_model expect(colord("#999966").toCmykString()).toBe("device-cmyk(0% 0% 33% 40%)"); expect(colord("#99ffff").toCmykString()).toBe("device-cmyk(40% 0% 0% 0%)"); expect(colord("#00336680").toCmykString()).toBe("device-cmyk(100% 50% 0% 60% / 0.5)");