Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
# @suaveplan/colour-convert-cmyk - Bidirectional RGB <-> CMYK colour space conversion with black generation for print workflows
Bidirectional conversion between RGB and CMYK colour spaces using the standard Euclidean algorithm with black generation. CMYK is the subtractive model used in professional printing; these conversions are essential for print preview, pre-press workflows, and bridging screen colours with physical output.
<!-- begin:auto-docs -->
## Documentation tree
- [cmyk](./src/cmyk/cmyk.md) — `cmyk.ts` provides bidirectional conversion between RGB and CMYK colour spaces. CMYK is the subtractive model used in p…
- [cmyk](./src/cmyk/cmyk.md) — `cmyk.ts` provides bidirectional conversion between RGB and CMYK colour spaces. CMYK is the subtractive model used in p…
<!-- end:auto-docs -->
## Installation
```bash
bun add @suaveplan/colour-convert-cmyk
```
## Quick Start
```ts
import { rgbToCmyk, cmykToRgb } from "@suaveplan/colour-convert-cmyk";
const ink = rgbToCmyk({ r: 59, g: 130, b: 246 });
console.log(ink); // { c: 76, m: 47, y: 0, k: 4 }
const screen = cmykToRgb(ink);
console.log(screen); // { r: 59, g: 130, b: 246, a: 1 }
```
## Modules
- [`src/cmyk/cmyk.md`](./src/cmyk/cmyk.md) — RGB↔CMYK conversion implementation and API reference

View File

@@ -0,0 +1,45 @@
{
"name": "@suaveplan/colour-convert-cmyk",
"version": "0.1.0",
"sideEffects": false,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./cmyk/cmyk": {
"import": {
"types": "./dist/cmyk/cmyk.d.ts",
"default": "./dist/cmyk/cmyk.js"
}
}
},
"files": [
"dist"
],
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/colours": "workspace:*",
"tslib": "^2.8.1"
},
"devDependencies": {
"@suaveplan/testing": "workspace:*",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-dts": "^4.5.3",
"vitest": "^4.1.5"
}
}

View File

@@ -0,0 +1,47 @@
# cmyk
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`cmyk.ts` provides bidirectional conversion between RGB and CMYK colour spaces. CMYK is the subtractive model used in professional printing, where colours are created by mixing cyan, magenta, yellow, and black inks. These conversions are essential for print preview, pre-press workflows, and any application that needs to bridge screen colours with physical output. While RGB is additive (light-emitting), CMYK is subtractive (light-absorbing), making direct translation necessary for accurate colour reproduction across media.
## Features
- RGB to CMYK conversion with black generation
- CMYK to RGB conversion
- Percentage-based CMYK output (0–100)
- Handles edge cases like pure black and pure white
## Usage: Basic Example
```ts
import { rgbToCmyk } from "@suaveplan/colour-convert-cmyk";
const cmyk = rgbToCmyk({ r: 128, g: 64, b: 32 });
```
## Usage: Advanced Example
```ts
import { rgbToCmyk, cmykToRgb } from "@suaveplan/colour-convert-cmyk";
const ink = rgbToCmyk({ r: 59, g: 130, b: 246 });
const screen = cmykToRgb(ink);
```
## API Reference
### `rgbToCmyk(rgb: RGB): CMYK`
Converts an RGB colour to CMYK percentages. The black channel (`k`) is generated from the maximum of the inverted RGB channels, and the cyan, magenta, and yellow channels are computed relative to this black. This is the standard Euclidean algorithm used in most graphics software.
### `cmykToRgb(cmyk: CMYK): RGBA`
Converts CMYK percentages back to RGB. The formula applies the subtractive model in reverse: each RGB channel is the product of `(1 - ink) × (1 - black)`. The alpha channel is always set to 1 because CMYK has no native alpha channel.
## Implementation Notes
The conversion uses the standard Euclidean algorithm for RGB to CMYK. When the input colour is pure black (`k = 100%`), the cyan, magenta, and yellow channels are defined as zero to avoid division by zero. All CMYK values are rounded to the nearest integer percentage. The inverse conversion always produces alpha 1 because CMYK has no native alpha channel. Round-trip conversion may introduce small quantization errors due to the integer rounding of CMYK percentages.

View File

@@ -0,0 +1,63 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { cmykToRgb, rgbToCmyk } from "./cmyk.js";
describe("colour-convert-cmyk", () => {
it("rgb to cmyk round-trips", { timeout: 5000 }, () => {
const original = { r: 128, g: 64, b: 32 };
const cmyk = rgbToCmyk(original);
const back = cmykToRgb(cmyk);
expect(back.r).toBeCloseTo(original.r, -1);
expect(back.g).toBeCloseTo(original.g, -1);
expect(back.b).toBeCloseTo(original.b, -1);
});
it("white is zero cmyk", { timeout: 5000 }, () => {
const cmyk = rgbToCmyk({ r: 255, g: 255, b: 255 });
expect(cmyk.c).toBe(0);
expect(cmyk.m).toBe(0);
expect(cmyk.y).toBe(0);
expect(cmyk.k).toBe(0);
});
it("black is full k", { timeout: 5000 }, () => {
const cmyk = rgbToCmyk({ r: 0, g: 0, b: 0 });
expect(cmyk.c).toBe(0);
expect(cmyk.m).toBe(0);
expect(cmyk.y).toBe(0);
expect(cmyk.k).toBe(100);
});
it("pure red is magenta+yellow", { timeout: 5000 }, () => {
const cmyk = rgbToCmyk({ r: 255, g: 0, b: 0 });
expect(cmyk.c).toBe(0);
expect(cmyk.m).toBe(100);
expect(cmyk.y).toBe(100);
expect(cmyk.k).toBe(0);
});
it("pure green is cyan+yellow", { timeout: 5000 }, () => {
const cmyk = rgbToCmyk({ r: 0, g: 255, b: 0 });
expect(cmyk.c).toBe(100);
expect(cmyk.m).toBe(0);
expect(cmyk.y).toBe(100);
expect(cmyk.k).toBe(0);
});
it("pure blue is cyan+magenta", { timeout: 5000 }, () => {
const cmyk = rgbToCmyk({ r: 0, g: 0, b: 255 });
expect(cmyk.c).toBe(100);
expect(cmyk.m).toBe(100);
expect(cmyk.y).toBe(0);
expect(cmyk.k).toBe(0);
});
it("cmyk to rgb produces valid colours", { timeout: 5000 }, () => {
const rgb = cmykToRgb({ c: 50, m: 25, y: 75, k: 10 });
expect(rgb.r).toBeGreaterThanOrEqual(0);
expect(rgb.r).toBeLessThanOrEqual(255);
expect(rgb.g).toBeGreaterThanOrEqual(0);
expect(rgb.g).toBeLessThanOrEqual(255);
expect(rgb.b).toBeGreaterThanOrEqual(0);
expect(rgb.b).toBeLessThanOrEqual(255);
});
});

View File

@@ -0,0 +1,40 @@
import type { RGB, RGBA } from "@suaveplan/colours";
export interface CMYK {
readonly c: number;
readonly m: number;
readonly y: number;
readonly k: number;
}
export function rgbToCmyk(rgb: RGB): CMYK {
const r = rgb.r / 255;
const g = rgb.g / 255;
const b = rgb.b / 255;
const k = 1 - Math.max(r, g, b);
const c = k === 1 ? 0 : (1 - r - k) / (1 - k);
const m = k === 1 ? 0 : (1 - g - k) / (1 - k);
const y = k === 1 ? 0 : (1 - b - k) / (1 - k);
return {
c: Math.round(c * 100),
m: Math.round(m * 100),
y: Math.round(y * 100),
k: Math.round(k * 100),
};
}
export function cmykToRgb(cmyk: CMYK): RGBA {
const c = cmyk.c / 100;
const m = cmyk.m / 100;
const y = cmyk.y / 100;
const k = cmyk.k / 100;
const r = 255 * (1 - c) * (1 - k);
const g = 255 * (1 - m) * (1 - k);
const b = 255 * (1 - y) * (1 - k);
return {
r: Math.round(r),
g: Math.round(g),
b: Math.round(b),
a: 1,
};
}

View File

@@ -0,0 +1,2 @@
export type { CMYK } from "./cmyk/cmyk.js";
export { cmykToRgb, rgbToCmyk } from "./cmyk/cmyk.js";

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import pkg from "../package.json";
describe("dep-graph", () => {
it("only depends on allowed @suaveplan packages", () => {
const deps = Object.keys(pkg.dependencies ?? {});
const allowed = ["@suaveplan/colours"];
for (const dep of deps) {
if (dep.startsWith("@suaveplan/")) {
expect(allowed).toContain(dep);
}
}
});
});

View File

@@ -0,0 +1 @@
// no polyfills needed

View File

@@ -0,0 +1,13 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import pkg from "../package.json";
describe("tree-shake", () => {
it("has sideEffects false", () => {
expect(pkg.sideEffects).toBe(false);
});
it("has at least one subpath export", () => {
const exports = Object.keys(pkg.exports ?? {});
expect(exports.length).toBeGreaterThan(1);
});
});

View File

@@ -0,0 +1,15 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,19 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
build: {
lib: {
entry: {
index: "./src/index.ts",
"cmyk/cmyk": "./src/cmyk/cmyk.ts",
},
formats: ["es"],
},
sourcemap: true,
rollupOptions: {
external: [/^@suaveplan\//, "tslib"],
},
},
plugins: [dts({ rollupTypes: false, include: ["src/**/*.ts"], tsconfigPath: "./tsconfig.lib.json" })],
});

View File

@@ -0,0 +1,21 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
setupFiles: ["./tests/setup/polyfills.ts"],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: ["src/**/*.test.ts", "src/**/index.ts"],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
},
});