AI-generated study image / before editing
DEVELOPMENT JOURNAL / 12
Photoshot
GITHUB × AI
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.

PHOTOSHOT DEVELOPMENT JOURNAL / 12

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

PhotoshotDEVELOPMENT JOURNAL / 12FOUNDATION

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.

src/lut.ts · v0.16.0
L19–38
Photoshot 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) {
PhotoshotDEVELOPMENT JOURNAL / 12IMPLEMENTATION

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.

PHOTOSHOT / SOURCE NOTES12

Read the code.

src/adjustments.ts · v0.16.0
L777–790
Photoshot 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;
src/icc.ts · v0.16.0
L32–37
Photoshot 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(
    wasmBinary
src/icc.ts · v0.16.0
L65–77
Photoshot 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);
PhotoshotDEVELOPMENT JOURNAL / 12IN PRACTICE

In the editor.

Actual Photoshot workspace. Settings and the image are shown together.
Actual Photoshot workspace. Settings and the image are shown together.

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.

PhotoshotDEVELOPMENT JOURNAL / 12BEFORE / AFTER

The visible result.

AI-generated study image / before editing
01 / AI-generated study image / before editing
Photoshot output / edited result
02 / Photoshot output / edited result

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