Colour Look Up Table Processing:- Any Refinements?

I’ve successfully implemented colour LUT processing in UXP, but my current approach feels a bit cumbersome. I will explain my approach here and then perhaps other people can offer further insights or further understanding in response.

My goal was to generate a colour lookup table (LUT) using WebAssembly (WASM) and apply it to an image in Photoshop. Instead of processing the image directly in WASM, I chose to use an LUT method for two reasons. The use of Photoshop’s native functions for LUT application should ensure high efficiency. Using a fixed-size LUT avoids the need to handle images of varying dimensions. Effectively, I could delegate to Photoshop the task of adapting the processing to image size.

I started by recording the application of a color lookup table as a Photoshop Action and used the “Copy as JavaScript” feature to generate the corresponding code. This produced a script similar to the one discussed in this forum thread. Issue with creation colorLookup layer - #7 by Jarda

The generated code includes two encoded strings: one for the LUT data (base64) and another for the colour profile. Through experimentation, I found that the base64 LUT string was redundant and could be omitted, leaving only the colour profile string. (If only the reverse were true!)

Here’s the simplified structure of the code I used to apply the LUT:

async function applyColourLookup()
{

      var lutString= " --Very long Colour Profile String-- "

    let commands = [
            // Make adjustment layer
            {
            "_obj": "make", "_options": {"failureLabel": "make adjustment layer"},
            "_target": [{"_ref": "adjustmentLayer"}],
            "using": {"_obj": "adjustmentLayer","type": {"_class": "colorLookup"}}
            },                      
            // Set current adjustment layer
            {
            "_obj": "set",
            "_target": [
                    {
                    "_enum": "ordinal",
                    "_ref": "adjustmentLayer",
                    "_value": "targetEnum"
                    }
            ],
            "to": {
                    "_obj": "colorLookup",
                    "lookupType": {
                    "_enum": "colorLookupType",
                    "_value": "3DLUT",
                    },
                    "name": "TestLUT.CUBE",
                    "profile": {
                    "_data" : lutString,
                    "_rawData": "base64"
                    }
            }
            }
    ];
    return await require("photoshop").action.batchPlay(commands, {});}

The challenge then was to generate the colour profile string (which has been described elsewhere in this forum as an ICC profile with a header followed by LUT data, all encoded in base64). After some reading around and experimentation, I adopted an approach in which I used Photoshop to generate a profile from a known LUT file so that I could chop the LUT data section off from the header section and then append my own LUT data.

I decided that a 17x17 LUT table was adequate for my purposes, so I took an existing LUT table and replaced each of the 17 x 17 data lines by “0.5 0.5 0.5”. (This gave me an LUT which, when applied to an image, modified it to a uniform block of mid-range grey.)

The colour profile string generated by Photoshop gave me a sequence of the following form.

“AADoWEFEQkUEAAAAbGlua1JHQ………EREQAAAAAAAAAAAAAAAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AA…………/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAA=”

Each ‘0.5 0.5 0.5’ line in the LUT was represented by 16 bytes (/AAAAPwAAAD8AAAA). Each float point number is represented by 4 bytes prior to encoding and the base64 encoding procedure generates 4 bytes out for every 3 Bytes in. It is notable that the last symbol is a padding symbol “=”. From this one can infer that, prior to coding, the file length was of size 3n-1 bytes and that an extra byte had been added for base64 conversion. Over and above this, one can infer that the number of header bytes is of size 3m-1 bytes and that at least one output byte includes a combination of header and LUT data. The header actually comprised 524 bytes. If a way could be found to augment this to 525 bytes, then things could be simple. It would be possible to encode the LUT data and concatenate it to the encoded header data. As it is, the base64 string had to be decoded (4 bytes to 3) and the header bytes identified. These were then augmented by new LUT data. Base64 encoding was then applied to the full data sequence. The 4-byte floating point LUT data was represented in big endian format.

The preceding method allows the generation of a ‘Color Lookup’ adjustment layer. Unfortunately, Adjustment Layers haven’t made it into the DOM yet, so there is a further issue to overcome which can be addressed as follows. Within batchPlay, the layer to be processed should be selected as the active layer. All layers other than the adjustment layer should be hidden. Outside of batchPlay the following should be invoked

await app.activeDocument.activeLayers[0].merge();

subsequently hidden layers can be made visible as required.

I hope all of the above is clear. I can elaborate further if needed. I would welcome any suggestions or observations.

1 Like

I forgot to mention that in the above investigation, I accepted the Photoshop default settings for the input of an LUT in ‘.cube’ format. So, the image was assumed to be RGB (rather than BGR) and LUT data was assumed to be ordered BGR (rather than RGB).