@segnavia/render 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ import { refuse } from "./types.js";
2
+ //#region src/color/binary.ts
3
+ function need(offset, length, bounds) {
4
+ if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > bounds.end) refuse(`truncated ${bounds.name} tag`, `${bounds.name} tag structure contained within its declared size`);
5
+ }
6
+ function signature(bytes, offset, bounds = {
7
+ end: bytes.length,
8
+ name: "profile"
9
+ }) {
10
+ need(offset, 4, bounds);
11
+ return String.fromCharCode(...bytes.subarray(offset, offset + 4));
12
+ }
13
+ function u16(view, offset, bounds) {
14
+ need(offset, 2, bounds);
15
+ return view.getUint16(offset, false);
16
+ }
17
+ function u32(view, offset, bounds) {
18
+ need(offset, 4, bounds);
19
+ return view.getUint32(offset, false);
20
+ }
21
+ function fixed(view, offset, bounds) {
22
+ need(offset, 4, bounds);
23
+ return view.getInt32(offset, false) / 65536;
24
+ }
25
+ const align4 = (value) => Math.ceil(value / 4) * 4;
26
+ //#endregion
27
+ export { align4, fixed, need, signature, u16, u32 };
@@ -0,0 +1,121 @@
1
+ import { checked } from "./types.js";
2
+ import { D50, labToXyz, matrix3, tableDecode, tableEncode, tryInvert3, xyzToLab } from "./spaces.js";
3
+ //#region src/color/black-point.ts
4
+ function detectSourceBlackPoint(colorSpace, pcs, major, relativeA2b, perceptualB2a) {
5
+ if (colorSpace !== "CMYK") return [
6
+ 0,
7
+ 0,
8
+ 0
9
+ ];
10
+ const device = perceptualB2a.run(tableEncode(pcs, perceptualB2a.encoding, major, [
11
+ 0,
12
+ 0,
13
+ 0
14
+ ], false));
15
+ const result = relativeA2b.run(device);
16
+ const returned = tableDecode(pcs, relativeA2b.encoding, major, [
17
+ checked(result, 0, "black-point A2B result"),
18
+ checked(result, 1, "black-point A2B result"),
19
+ checked(result, 2, "black-point A2B result")
20
+ ]);
21
+ const lab = pcs === "Lab " ? returned : xyzToLab(returned);
22
+ return [
23
+ Math.min(50, lab[0]),
24
+ 0,
25
+ 0
26
+ ];
27
+ }
28
+ function quadraticRoot(x, y) {
29
+ if (x.length < 4) return 0;
30
+ let sx = 0, sx2 = 0, sx3 = 0, sx4 = 0, sy = 0, syx = 0, syx2 = 0;
31
+ for (const [index, xn] of x.entries()) {
32
+ const yn = checked(y, index, "quadratic sample");
33
+ sx += xn;
34
+ sx2 += xn ** 2;
35
+ sx3 += xn ** 3;
36
+ sx4 += xn ** 4;
37
+ sy += yn;
38
+ syx += yn * xn;
39
+ syx2 += yn * xn ** 2;
40
+ }
41
+ const matrix = [
42
+ x.length,
43
+ sx,
44
+ sx2,
45
+ sx,
46
+ sx2,
47
+ sx3,
48
+ sx2,
49
+ sx3,
50
+ sx4
51
+ ];
52
+ const inverse = tryInvert3(matrix);
53
+ if (!inverse) return 0;
54
+ const [c, b, a] = matrix3(inverse, [
55
+ sy,
56
+ syx,
57
+ syx2
58
+ ]);
59
+ if (Math.abs(a) < 1e-10) return Math.abs(b) < 1e-10 ? 0 : Math.max(0, Math.min(50, -c / b));
60
+ const discriminant = b * b - 4 * a * c;
61
+ return discriminant <= 0 ? 0 : Math.max(0, Math.min(50, (-b + Math.sqrt(discriminant)) / (2 * a)));
62
+ }
63
+ function destinationBlackPoint(colorSpace, pcs, major, relativeA2b, relativeB2a, sourceBlack) {
64
+ if (colorSpace !== "CMYK") return [...sourceBlack];
65
+ const inputRamp = [], outputRamp = [];
66
+ for (let index = 0; index < 256; index++) {
67
+ const lightness = index * 100 / 255;
68
+ const lab = [
69
+ lightness,
70
+ Math.max(-50, Math.min(50, sourceBlack[1])),
71
+ Math.max(-50, Math.min(50, sourceBlack[2]))
72
+ ];
73
+ const value = pcs === "Lab " ? lab : labToXyz(lab);
74
+ const device = relativeB2a.run(tableEncode(pcs, relativeB2a.encoding, major, value, false));
75
+ const run = relativeA2b.run(device);
76
+ const returned = tableDecode(pcs, relativeA2b.encoding, major, [
77
+ checked(run, 0, "black-point A2B result"),
78
+ checked(run, 1, "black-point A2B result"),
79
+ checked(run, 2, "black-point A2B result")
80
+ ]);
81
+ inputRamp.push(lightness);
82
+ outputRamp.push(pcs === "Lab " ? returned[0] : xyzToLab(returned)[0]);
83
+ }
84
+ for (let index = 254; index > 0; index--) outputRamp[index] = Math.min(checked(outputRamp, index, "black-point output ramp"), checked(outputRamp, index + 1, "black-point output ramp"));
85
+ const minimum = checked(outputRamp, 0, "black-point output ramp"), maximum = checked(outputRamp, 255, "black-point output ramp");
86
+ if (!(minimum < maximum)) return [
87
+ 0,
88
+ 0,
89
+ 0
90
+ ];
91
+ if (inputRamp.every((value, index) => value <= minimum + .2 * (maximum - minimum) || Math.abs(value - checked(outputRamp, index, "black-point output ramp")) < 4)) return [...sourceBlack];
92
+ const x = [], y = [];
93
+ for (let index = 0; index < 256; index++) {
94
+ const normalized = (checked(outputRamp, index, "black-point output ramp") - minimum) / (maximum - minimum);
95
+ if (normalized >= .1 && normalized < .5) {
96
+ x.push(checked(inputRamp, index, "black-point input ramp"));
97
+ y.push(normalized);
98
+ }
99
+ }
100
+ return [
101
+ x.length < 3 ? 0 : quadraticRoot(x, y),
102
+ sourceBlack[1],
103
+ sourceBlack[2]
104
+ ];
105
+ }
106
+ function compensateBlackPoint(profile, lab, reverse) {
107
+ const point = reverse ? profile.sourceBlackPoint : profile.destinationBlackPoint;
108
+ const black = profile.pcs === "Lab " ? point : xyzToLab(point);
109
+ const blackXyz = labToXyz(black), xyz = labToXyz(lab);
110
+ const channel = (value, white, blackValue) => {
111
+ const scale = (white - blackValue) / white;
112
+ return reverse ? (value - blackValue) / scale : blackValue + value * scale;
113
+ };
114
+ return xyzToLab([
115
+ channel(xyz[0], D50[0], blackXyz[0]),
116
+ channel(xyz[1], D50[1], blackXyz[1]),
117
+ channel(xyz[2], D50[2], blackXyz[2])
118
+ ]);
119
+ }
120
+ //#endregion
121
+ export { compensateBlackPoint, destinationBlackPoint, detectSourceBlackPoint };
@@ -0,0 +1,18 @@
1
+ import { clamp, color4, refuse } from "./types.js";
2
+ import { labToXyz, srgbToLab, tableEncode } from "./spaces.js";
3
+ import { compensateBlackPoint } from "./black-point.js";
4
+ //#region src/color/engine.ts
5
+ function toPress(profile, rgb) {
6
+ if (profile.colorSpace !== "CMYK") refuse(`destination color space ${profile.colorSpace}`, "CMYK");
7
+ const lab = compensateBlackPoint(profile, srgbToLab(rgb), false);
8
+ const pcs = profile.pcs === "Lab " ? lab : labToXyz(lab);
9
+ const [cyan, magenta, yellow, black] = color4(profile.b2a.run(tableEncode(profile.pcs, profile.b2a.encoding, profile.majorVersion, pcs, true)), "B2A result");
10
+ return [
11
+ clamp(cyan),
12
+ clamp(magenta),
13
+ clamp(yellow),
14
+ clamp(black)
15
+ ];
16
+ }
17
+ //#endregion
18
+ export { toPress };
@@ -0,0 +1,74 @@
1
+ import { checked, clamp } from "./types.js";
2
+ //#region src/color/interpolation.ts
3
+ function interpolate(table, value) {
4
+ if (table.length === 0) throw new Error("ICC interpolation table is empty");
5
+ const position = clamp(value) * (table.length - 1);
6
+ const low = Math.floor(position);
7
+ const lower = table.get(low);
8
+ return lower + (table.get(Math.min(low + 1, table.length - 1)) - lower) * (position - low);
9
+ }
10
+ function layout(points, outputs, input) {
11
+ const base = input.map((value, axis) => {
12
+ const count = checked(points, axis, "CLUT dimensions");
13
+ return Math.min(count - 2, Math.floor(clamp(value) * (count - 1)));
14
+ });
15
+ return {
16
+ base,
17
+ fractions: input.map((value, axis) => clamp(value) * (checked(points, axis, "CLUT dimensions") - 1) - checked(base, axis, "CLUT base")),
18
+ stride: points.map((_, axis) => points.slice(axis + 1).reduce((a, b) => a * b, outputs))
19
+ };
20
+ }
21
+ function multilinear(grid, points, outputs, input) {
22
+ const { base, fractions, stride } = layout(points, outputs, input);
23
+ const result = Array.from({ length: outputs }, () => 0);
24
+ for (let mask = 0; mask < 2 ** input.length; mask++) {
25
+ let weight = 1;
26
+ let start = 0;
27
+ for (let axis = 0; axis < input.length; axis++) {
28
+ const high = (mask & 1 << axis) !== 0;
29
+ const fraction = checked(fractions, axis, "CLUT fractions");
30
+ weight *= high ? fraction : 1 - fraction;
31
+ start += (checked(base, axis, "CLUT base") + Number(high)) * checked(stride, axis, "CLUT stride");
32
+ }
33
+ for (let channel = 0; channel < outputs; channel++) result[channel] = checked(result, channel, "CLUT result") + weight * grid.get(start + channel);
34
+ }
35
+ return result;
36
+ }
37
+ function tetrahedral4(grid, points, outputs, input) {
38
+ const { base, fractions, stride } = layout(points, outputs, input);
39
+ const order = [
40
+ 1,
41
+ 2,
42
+ 3
43
+ ].sort((a, b) => checked(fractions, b, "CLUT fractions") - checked(fractions, a, "CLUT fractions"));
44
+ const plane = (first) => {
45
+ const corner = [
46
+ 0,
47
+ 0,
48
+ 0,
49
+ 0
50
+ ];
51
+ let at = base.reduce((sum, value, axis) => sum + (value + (axis === 0 ? first : 0)) * checked(stride, axis, "CLUT stride"), 0);
52
+ let previous = Array.from({ length: outputs }, (_, channel) => grid.get(at + channel));
53
+ const result = [...previous];
54
+ for (const axis of order) {
55
+ corner[axis] = 1;
56
+ at = base.reduce((sum, value, index) => sum + (value + (index === 0 ? first : checked(corner, index, "CLUT corner"))) * checked(stride, index, "CLUT stride"), 0);
57
+ const next = Array.from({ length: outputs }, (_, channel) => grid.get(at + channel));
58
+ for (let channel = 0; channel < outputs; channel++) result[channel] = checked(result, channel, "CLUT result") + checked(fractions, axis, "CLUT fractions") * (checked(next, channel, "CLUT corner") - checked(previous, channel, "CLUT corner"));
59
+ previous = next;
60
+ }
61
+ return result;
62
+ };
63
+ const low = plane(0), high = plane(1);
64
+ return low.map((value, channel) => value + checked(fractions, 0, "CLUT fractions") * (checked(high, channel, "CLUT high plane") - value));
65
+ }
66
+ function matrixInput(values) {
67
+ return [
68
+ checked(values, 0, "matrix input"),
69
+ checked(values, 1, "matrix input"),
70
+ checked(values, 2, "matrix input")
71
+ ];
72
+ }
73
+ //#endregion
74
+ export { interpolate, matrixInput, multilinear, tetrahedral4 };
@@ -0,0 +1,98 @@
1
+ import { REVERSE_LUT, checked, refuse } from "./types.js";
2
+ import { fixed, need, signature, u32 } from "./binary.js";
3
+ import { destinationBlackPoint, detectSourceBlackPoint } from "./black-point.js";
4
+ import { matrixTables, parseMab, parseMft } from "./tables.js";
5
+ //#region src/color/profile.ts
6
+ const MAX_PROFILE_BYTES = 33554432;
7
+ function header(bytes, view, bounds) {
8
+ need(0, 132, bounds);
9
+ if (signature(bytes, 36, bounds) !== "acsp") refuse(`header signature ${JSON.stringify(signature(bytes, 36, bounds))}`, "\"acsp\"");
10
+ const major = checked(bytes, 8, "ICC header"), minor = checked(bytes, 9, "ICC header");
11
+ if (major !== 2 && major !== 4) refuse(`ICC version ${major}.${minor >> 4}`, "ICC version 2 or 4");
12
+ const profileClass = signature(bytes, 12, bounds);
13
+ if (profileClass !== "prtr" && profileClass !== "mntr") refuse(`profile class ${JSON.stringify(profileClass)}`, "prtr or mntr");
14
+ const colorSpace = signature(bytes, 16, bounds);
15
+ if (colorSpace !== "CMYK" && colorSpace !== "RGB ") refuse(`color space ${JSON.stringify(colorSpace)}`, "CMYK or RGB");
16
+ const pcs = signature(bytes, 20, bounds);
17
+ if (pcs !== "Lab " && pcs !== "XYZ ") refuse(`PCS ${JSON.stringify(pcs)}`, "Lab or XYZ");
18
+ u32(view, 0, bounds);
19
+ return {
20
+ majorVersion: major,
21
+ profileClass,
22
+ colorSpace,
23
+ pcs
24
+ };
25
+ }
26
+ function records(bytes, view, bounds) {
27
+ const count = u32(view, 128, bounds);
28
+ if (count > 256) refuse(`tag count ${count}`, `at most 256 tags`);
29
+ if (count > (bytes.length - 132) / 12) refuse("truncated tag table", "tag records contained within the profile");
30
+ const result = {};
31
+ for (let index = 0; index < count; index++) {
32
+ const at = 132 + index * 12, name = signature(bytes, at, bounds), offset = u32(view, at + 4, bounds), size = u32(view, at + 8, bounds);
33
+ if (offset > bytes.length - 8 || size > bytes.length - offset) refuse(`truncated ${name} tag`, "tag data contained within the profile");
34
+ const record = {
35
+ offset,
36
+ size,
37
+ end: offset + size,
38
+ name,
39
+ type: ""
40
+ };
41
+ record.type = signature(bytes, offset, record);
42
+ result[name] = record;
43
+ }
44
+ return result;
45
+ }
46
+ function recordTable(bytes, view, record, inputs, outputs) {
47
+ if (record.type === "mft1" || record.type === "mft2") return parseMft(bytes, view, record, record.type, inputs, outputs);
48
+ if (record.type === "mAB " || record.type === "mBA ") return parseMab(bytes, view, record, record.type, inputs, outputs);
49
+ return refuse(`${record.name} tag type ${JSON.stringify(record.type)}`, `mft1, mft2, mAB , or ${REVERSE_LUT}`);
50
+ }
51
+ function table(bytes, view, all, names, inputs, outputs) {
52
+ const name = names.find((candidate) => all[candidate]);
53
+ if (!name) refuse(`missing ${names.join("/")} tag`, names.join(" or "));
54
+ const record = all[name];
55
+ if (!record) throw new Error(`ICC profile is missing ${name} after its tag set was validated`);
56
+ return recordTable(bytes, view, record, inputs, outputs);
57
+ }
58
+ function readProfile(bytes) {
59
+ if (bytes.length > 33554432) refuse(`profile length ${bytes.length}`, `at most ${MAX_PROFILE_BYTES} bytes`);
60
+ const bounds = {
61
+ end: bytes.length,
62
+ name: "profile"
63
+ }, view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
64
+ const parsed = header(bytes, view, bounds), all = records(bytes, view, bounds);
65
+ if (all.wtpt?.type !== "XYZ ") refuse(`white point tag type ${JSON.stringify(all.wtpt?.type ?? "missing")}`, "XYZ ");
66
+ const channels = parsed.colorSpace === "CMYK" ? 4 : 3;
67
+ const matrix = parsed.colorSpace === "RGB " ? matrixTables(bytes, view, all) : void 0;
68
+ const a2b = matrix?.a2b ?? table(bytes, view, all, ["A2B1", "A2B0"], channels, 3);
69
+ const b2a = matrix?.b2a ?? table(bytes, view, all, ["B2A1", "B2A0"], 3, channels);
70
+ const perceptualA2b = matrix?.a2b ?? table(bytes, view, all, ["A2B0", "A2B1"], channels, 3);
71
+ const perceptualB2a = matrix?.b2a ?? table(bytes, view, all, ["B2A0", "B2A1"], 3, channels);
72
+ const gamutRecord = all.gamt, gamut = gamutRecord ? recordTable(bytes, view, gamutRecord, 3, 1) : void 0;
73
+ const white = all.wtpt;
74
+ if (!white) throw new Error("ICC profile is missing wtpt after its tag set was validated");
75
+ need(white.offset, 20, white);
76
+ const whitePoint = [
77
+ fixed(view, white.offset + 8, white),
78
+ fixed(view, white.offset + 12, white),
79
+ fixed(view, white.offset + 16, white)
80
+ ];
81
+ const sourceBlackPoint = detectSourceBlackPoint(parsed.colorSpace, parsed.pcs, parsed.majorVersion, a2b, perceptualB2a);
82
+ const destination = destinationBlackPoint(parsed.colorSpace, parsed.pcs, parsed.majorVersion, a2b, b2a, sourceBlackPoint);
83
+ return {
84
+ ...parsed,
85
+ version: `${parsed.majorVersion}.${checked(bytes, 9, "ICC header") >> 4}.${checked(bytes, 9, "ICC header") & 15}`,
86
+ whitePoint,
87
+ tags: Object.fromEntries(Object.entries(all).map(([name, record]) => [name, record.type])),
88
+ a2b,
89
+ b2a,
90
+ perceptualA2b,
91
+ perceptualB2a,
92
+ gamut,
93
+ sourceBlackPoint,
94
+ destinationBlackPoint: destination
95
+ };
96
+ }
97
+ //#endregion
98
+ export { MAX_PROFILE_BYTES, readProfile };
@@ -0,0 +1,138 @@
1
+ import { clamp, refuse } from "./types.js";
2
+ //#region src/color/spaces.ts
3
+ const D50 = [
4
+ .9642,
5
+ 1,
6
+ .8249
7
+ ];
8
+ function matrix3(matrix, input) {
9
+ const [m0, m1, m2, m3, m4, m5, m6, m7, m8] = matrix;
10
+ const [x, y, z] = input;
11
+ return [
12
+ m0 * x + m1 * y + m2 * z,
13
+ m3 * x + m4 * y + m5 * z,
14
+ m6 * x + m7 * y + m8 * z
15
+ ];
16
+ }
17
+ function tryInvert3(m) {
18
+ const [m0, m1, m2, m3, m4, m5, m6, m7, m8] = m;
19
+ const determinant = m0 * (m4 * m8 - m5 * m7) - m1 * (m3 * m8 - m5 * m6) + m2 * (m3 * m7 - m4 * m6);
20
+ if (Math.abs(determinant) < 1e-12) return void 0;
21
+ return [
22
+ (m4 * m8 - m5 * m7) / determinant,
23
+ (m2 * m7 - m1 * m8) / determinant,
24
+ (m1 * m5 - m2 * m4) / determinant,
25
+ (m5 * m6 - m3 * m8) / determinant,
26
+ (m0 * m8 - m2 * m6) / determinant,
27
+ (m2 * m3 - m0 * m5) / determinant,
28
+ (m3 * m7 - m4 * m6) / determinant,
29
+ (m1 * m6 - m0 * m7) / determinant,
30
+ (m0 * m4 - m1 * m3) / determinant
31
+ ];
32
+ }
33
+ function invert3(m) {
34
+ return tryInvert3(m) ?? refuse("singular RGB colorant matrix", "an invertible rXYZ/gXYZ/bXYZ matrix");
35
+ }
36
+ function labToXyz(lab) {
37
+ const [lightness, a, b] = lab;
38
+ const fy = (lightness + 16) / 116;
39
+ const convert = (value, white) => white * (value ** 3 > 216 / 24389 ? value ** 3 : (116 * value - 16) / 903.3);
40
+ return [
41
+ convert(fy + a / 500, D50[0]),
42
+ convert(fy, D50[1]),
43
+ convert(fy - b / 200, D50[2])
44
+ ];
45
+ }
46
+ function xyzToLab(xyz) {
47
+ const convert = (value, white) => {
48
+ const ratio = value / white;
49
+ return ratio > 216 / 24389 ? Math.cbrt(ratio) : (903.3 * ratio + 16) / 116;
50
+ };
51
+ const [x, y, z] = xyz;
52
+ const fx = convert(x, D50[0]), fy = convert(y, D50[1]), fz = convert(z, D50[2]);
53
+ return [
54
+ 116 * fy - 16,
55
+ 500 * (fx - fy),
56
+ 200 * (fy - fz)
57
+ ];
58
+ }
59
+ function decodePcs(pcs, encoded) {
60
+ const [x, y, z] = encoded;
61
+ return pcs === "Lab " ? [
62
+ x * 100,
63
+ y * 255 - 128,
64
+ z * 255 - 128
65
+ ] : [
66
+ x * (65535 / 32768),
67
+ y * (65535 / 32768),
68
+ z * (65535 / 32768)
69
+ ];
70
+ }
71
+ function normalizePcs(pcs, value) {
72
+ const [x, y, z] = value;
73
+ return pcs === "Lab " ? [
74
+ x / 100,
75
+ (y + 128) / 255,
76
+ (z + 128) / 255
77
+ ] : [
78
+ x * (32768 / 65535),
79
+ y * (32768 / 65535),
80
+ z * (32768 / 65535)
81
+ ];
82
+ }
83
+ function tableEncode(pcs, encoding, major, value, clipped) {
84
+ const encoded = normalizePcs(pcs, value);
85
+ const prepared = clipped ? [
86
+ clamp(encoded[0]),
87
+ clamp(encoded[1]),
88
+ clamp(encoded[2])
89
+ ] : encoded;
90
+ if (pcs !== "Lab " || encoding !== "mft2" || major !== 2) return prepared;
91
+ const scale = 65280 / 65535;
92
+ return [
93
+ prepared[0] * scale,
94
+ prepared[1] * scale,
95
+ prepared[2] * scale
96
+ ];
97
+ }
98
+ function tableDecode(pcs, encoding, major, value) {
99
+ const scale = pcs === "Lab " && encoding === "mft2" && major === 2 ? 65535 / 65280 : 1;
100
+ return decodePcs(pcs, [
101
+ value[0] * scale,
102
+ value[1] * scale,
103
+ value[2] * scale
104
+ ]);
105
+ }
106
+ const TO_D50 = [
107
+ 1.0478112,
108
+ .0228866,
109
+ -.050127,
110
+ .0295424,
111
+ .9904844,
112
+ -.0170491,
113
+ -.0092345,
114
+ .0150436,
115
+ .7521316
116
+ ];
117
+ const SRGB_TO_XYZ = [
118
+ .4124564,
119
+ .3575761,
120
+ .1804375,
121
+ .2126729,
122
+ .7151522,
123
+ .072175,
124
+ .0193339,
125
+ .119192,
126
+ .9503041
127
+ ];
128
+ function srgbToLab(rgb) {
129
+ const convert = (value) => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4;
130
+ const [red, green, blue] = rgb;
131
+ return xyzToLab(matrix3(TO_D50, matrix3(SRGB_TO_XYZ, [
132
+ convert(red),
133
+ convert(green),
134
+ convert(blue)
135
+ ])));
136
+ }
137
+ //#endregion
138
+ export { D50, decodePcs, invert3, labToXyz, matrix3, srgbToLab, tableDecode, tableEncode, tryInvert3, xyzToLab };