openpencil/tests/engine/color/document-space-conversion.test.ts
Danila Poyarkov e532f616ba
refactor!: move shared primitives below dom-css and core (#771)
* refactor!: move shared primitives below dom-css and core

dom-css depended on core for color conversion, base64 helpers, text
direction, and web-font assets, so core could not use dom-css and every
caller special-cased HTML and Tailwind output.

Color conversion and management, base64 helpers, and text/layout
direction now live in scene-graph under `color`, `bytes`, and
`text-direction`. dom-css takes web-font resolution as an injected
`fonts` option and owns the font face types, so it depends only on
scene-graph and core can depend on it.

BREAKING CHANGE: `@open-pencil/core/color` and `@open-pencil/core/bytes`
are removed, and the direction helpers are no longer exported from
`@open-pencil/core/text`; import them from `@open-pencil/scene-graph`
subpaths. `exportHTMLBundle` takes a font resolver in `fonts` instead of
`'assets'`.

* fix(tools): import color parsing from scene-graph in visual bisect

* fix(mcp): declare the scene-graph dependency

MCP imports `@open-pencil/scene-graph/bytes` since base64 helpers moved
there, but only reached scene-graph through core, so isolated installs
and package checks depended on transitive resolution.

* refactor!: use js-base64 directly instead of a base64 wrapper

Base64 helpers had moved into scene-graph only to sit below dom-css,
but they are a thin wrapper over js-base64 and unrelated to the graph;
fig already called js-base64 directly.

Callers use js-base64 and check `isValid` where input comes from outside
(clipboard, imported HTML, tool arguments, the plugin API). A new
`open-pencil/no-hand-rolled-base64` lint rule rejects atob, btoa, and
Buffer Base64 conversions, and AGENTS.md records the convention.

BREAKING CHANGE: `@open-pencil/core/bytes` is removed; use `js-base64`.

* fix(dom-css): keep images with invalid Base64 inline in HTML export

`exportHTMLBundle` accepts documents parsed from outside HTML, and
js-base64 drops characters it cannot decode, so extracting an invalid
image data URL wrote different bytes. Such images now stay inline.
2026-09-26 11:39:11 +04:00

71 lines
2.8 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { resolveOkHCLForPreview, resolveRGBAForPreview } from '@open-pencil/scene-graph/color'
/** #2563EB as authored in an sRGB document. */
const BLUE = { r: 0.1450980392156863, g: 0.38823529411764707, b: 0.9215686274509803, a: 1 }
/** Saturated P3 red, far outside sRGB. */
const P3_RED = { r: 1, g: 0, b: 0, a: 1 }
describe('document colour profile conversion', () => {
test('an sRGB document painting into an sRGB surface keeps its numbers', () => {
const resolved = resolveRGBAForPreview(BLUE, {
documentColorSpace: 'srgb',
colorSpace: 'srgb'
})
expect(resolved.color.r).toBeCloseTo(BLUE.r, 5)
expect(resolved.color.b).toBeCloseTo(BLUE.b, 5)
expect(resolved.clipped).toBe(false)
})
test('a P3 document painting into a P3 surface keeps its numbers', () => {
const resolved = resolveRGBAForPreview(P3_RED, {
documentColorSpace: 'display-p3',
colorSpace: 'display-p3'
})
expect(resolved.color).toEqual(P3_RED)
expect(resolved.clipped).toBe(false)
})
test('a P3 document converts into an sRGB surface and reports clipping', () => {
const resolved = resolveRGBAForPreview(P3_RED, {
documentColorSpace: 'display-p3',
colorSpace: 'srgb'
})
// P3 red is outside sRGB: the coordinates go out of range and the surface clamps them
// when painting, which is the "closest approximate colour" the profile change documents.
expect(resolved.color.r).toBeGreaterThan(1)
expect(resolved.color.g).toBeLessThan(0)
expect(resolved.color.b).toBeLessThan(0)
expect(resolved.clipped).toBe(true)
expect(resolved.cssColor).toBe('rgb(255, 0, 0)')
})
test('an sRGB document converts into a P3 surface', () => {
const resolved = resolveRGBAForPreview(BLUE, {
documentColorSpace: 'srgb',
colorSpace: 'display-p3'
})
// The same colour, expressed in P3 coordinates: slightly different numbers.
expect(resolved.color).not.toEqual(BLUE)
expect(resolved.color.b).toBeCloseTo(0.89, 2)
expect(resolved.clipped).toBe(false)
})
test('recognised document colours report their own space when no target is requested', () => {
const resolved = resolveRGBAForPreview(BLUE, { documentColorSpace: 'display-p3' })
expect(resolved.targetSpace).toBe('display-p3')
expect(resolved.color).toEqual(BLUE)
})
test('OKHCL colours resolve into the requested target space', () => {
const okhcl = { l: 0.6, c: 0.3, h: 20, a: 1 }
const srgb = resolveOkHCLForPreview(okhcl, { colorSpace: 'srgb' })
const p3 = resolveOkHCLForPreview(okhcl, { colorSpace: 'display-p3' })
expect(srgb.targetSpace).toBe('srgb')
expect(p3.targetSpace).toBe('display-p3')
// P3 is the wider space, so the same colour needs fewer of its primaries.
expect(p3.color.r).toBeLessThan(srgb.color.r)
})
})