
Building a free Photoshop alternative
From file to photograph
LUT parsing techniques, interpolation and ICC engines already existed. Building an alternative editor meant validating different inputs and connecting them to the same adjustment layer, with clear attribution for references and incorporated components.
AI-generated study photograph. Editor captures and outputs show Photoshot in use.
From the source.
A table and a color management engine
Reading a color table is only one stage. Parsing, validation, grid coordinates, interpolation and layer compositing must agree. AI helped us write and connect these stages to Photoshot’s input and output model.
OpenColorIO material informed our understanding of LUT structures. The ICC path actually incorporates lcms-wasm 1.0.5 and LittleCMS 2.16. A documentation reference and an integrated software component are different kinds of reuse.
Translate files into a common grid
CUBE, 3DL, LOOK and CSP readers produce a common Cube containing grid size, input range and RGB data. A preceding one-dimensional input curve is retained as a shaper.
The file limit is 32 MiB and the maximum 3D grid is 65 per axis. Data must contain size³×3 values. Nonfinite numbers and invalid domains are rejected. Support is limited to the structures accepted by the parsers, not every file sharing an extension.
L19–38Photoshot implementation ↗
export function validateCube(c: Cube) {
if (
!c ||
!Number.isInteger(c.size) ||
c.size < 2 ||
c.size > MAX_SIZE ||
typeof c.name !== 'string' ||
c.name.length > 200 ||
!Array.isArray(c.min) ||
!Array.isArray(c.max) ||
c.min.length !== 3 ||
c.max.length !== 3 ||
c.min.some((x, i) => !num(x) || !num(c.max[i]) || x >= c.max[i]) ||
!Array.isArray(c.data) ||
c.data.length !== c.size ** 3 * 3 ||
c.data.some((x) => !num(x) || Math.abs(x) > 65536)
)
fail();
if (c.only1d !== undefined && typeof c.only1d !== 'boolean') fail();
if (c.shaper) {Inside the
calculation.
Calculate colors between grid points
The input shaper runs first. Normalization produces grid coordinates; integer parts select a cell and fractional parts locate the color within it. Trilinear interpolation uses eight surrounding vertices; tetrahedral interpolation selects four.
Interpolation affects the result. Today we reran existing checks for 1D-plus-3D stage order, axis order and both interpolation paths. An attractive portrait alone would not validate the parser.
Integrate an actual ICC engine
ICC required more than reading a numeric table. After checking headers and tag bounds, LittleCMS builds a transform and samples a 65³ sRGB input grid. The sampled result then follows the shared LUT interpolation path.
The accepted classes are Lab/XYZ Abstract and RGB-to-RGB Device Link. Monitor profiles and CMYK Device Links are rejected here. Sampling a grid is also distinct from executing the original ICC transform for every pixel.
Read the code.
L777–790Photoshot implementation ↗
if (t === 'lookup' && cube) {
const cr = shaperValue(cube, 0, r),
cg = shaperValue(cube, 1, g),
cb = shaperValue(cube, 2, b);
const size = cube.size,
rx = clamp((cr - cube.min[0]) / (cube.max[0] - cube.min[0])) * (size - 1),
gy = clamp((cg - cube.min[1]) / (cube.max[1] - cube.min[1])) * (size - 1),
bz = clamp((cb - cube.min[2]) / (cube.max[2] - cube.min[2])) * (size - 1),
lr = Math.floor(rx),
lg = Math.floor(gy),
lb = Math.floor(bz),
fr = rx - lr,
fg = gy - lg,
fb = bz - lb;L32–37Photoshot implementation ↗
if (kind === 'link' && (input !== 'RGB ' || output !== 'RGB '))
throw Error('현재 RGB → RGB Device Link만 지원합니다.');
if (kind === 'abst' && (!['Lab ', 'XYZ '].includes(input) || !['Lab ', 'XYZ '].includes(output)))
throw Error('Lab/XYZ Abstract 프로파일만 지원합니다.');
engine ??= instantiate(
wasmBinaryL65–77Photoshot implementation ↗
transform(p, input === 'Lab ' ? LAB : XYZ, 0, output === 'Lab ' ? LAB : XYZ);
transform(last, output === 'Lab ' ? LAB : XYZ, srgb, RGB);
}
const size = 65,
total = size ** 3,
data: number[] = [];
for (let start = 0; start < total; start += 4096) {
const count = Math.min(4096, total - start);
let values = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const index = start + i;
values[i * 3] = (index % size) / (size - 1);
values[i * 3 + 1] = (Math.floor(index / size) % size) / (size - 1);In the editor.

Separate the demonstration from validation
The saved demonstration uses Photoshot / Soft Film, tetrahedral interpolation, dithering off and opacity 100%. It shows an internal preset, not ICC import.
Today all nineteen upgrade test groups passed, along with identity Link, inverted Link, identity Abstract and malformed ICC tag-bound checks. This is an example of connecting an existing engine to custom parsing and controls. Next comes Invert: a short formula that still needs document integration.
The visible result.


AI-generated study photograph. Editor captures and outputs show Photoshot in use.