How can I apply a linear color transformation on an image (like 'decodearray' in the LaTeX package graphicx)?

I work at a company where the presentation is supposed to be alost-but-not-really white, say, 90% R, 90% G, 100% B.
WhenI come across an image that has a white background (as default exported by matplotlib, other companies logo’s, etc) I can import them in a LaTeX presentation as:

\includegraphics[decodearray=0.0 0.9 0.0 0.9 0.0 1.0]{...}

and it will look as if the image creator gave it a transparent background.
In TikZ this is called ‘multiply’ (Transparency - PGF/TikZ Manual)
In python I would do this by multiplying the raw image data by (0.9, 0.9, 1.0) so that makes sense…

How could I do this in Typst?
I’m not very experienced in Rust but it looks like it should be a simple addition here:

Could I do this? I would be happy to add more features…

But if there is a simpler way I’d be happy too ;)

Do I understand that you only need to scale each R, G, B channel by some constant, or is it more complicated? I had a look if there any packages that do something similar, and grayness implements transparency through a WASM plugin, see the raster implementation here: grayness/src/raster/mod.rs at 5fb22a720dd3ed1f941cccf84b3bc68508aea351 · nineff/grayness · GitHub

It doesn’t seem difficult to modify the function to change transparency on a per-channel basis

Yes, a simple per-channel multiplication is all I need. There is no alpha channel needed now, although, to generalize for other use cases a full matrix transform on the colors (SVG Basics Tutorials - Colour Transforms) would be a nice extra

If that’s all you need then you can just do operations on each channel individually. I see no reason why you couldn’t use matrix transformations:

let m = [
    [0.9, 0.0, 0.0],
    [0.0, 1.0, 0.0],
    [0.0, 0.0, 0.9],
];

for y in 0..res.height() {
    for x in 0..res.width() {
        let pixel = res.get_pixel_mut(x, y);
        let r = pixel[0] as f32;
        let g = pixel[1] as f32;
        let b = pixel[2] as f32;
    
        let nr = m[0][0]*r + m[0][1]*g + m[0][2]*b;
        let ng = m[1][0]*r + m[1][1]*g + m[1][2]*b;
        let nb = m[2][0]*r + m[2][1]*g + m[2][2]*b;
    
        pixel[0] = nr.clamp(0.0, 255.0) as u8;
        pixel[1] = ng.clamp(0.0, 255.0) as u8;
        pixel[2] = nb.clamp(0.0, 255.0) as u8;
    }
}

That looks great! So how does one open an image as a pixel array and how to show a pixel array as an image? I recall maybe seeing the latter somewhere, but not how to get the raw data out of an image… would be great if you could add that to your snippet :)

The snippet above is for a WASM plugin built in rust, I basically just yoinked the code I linked in my original response and edited the function that gets applied to each pixel. If you want to implement this yourself, it would probably be easier for both of us if you analyse the source code and see how they do it.

You can have a look here for a hello world example, to ensure your development environment is set up correctly: wasm-minimal-protocol/examples/hello_rust at main · typst-community/wasm-minimal-protocol · GitHub

Note that grayness package has an additional layer of typst code to interact with the WASM plugin, see here: packages/packages/preview/grayness/0.5.0/lib.typ at main · typst/packages · GitHub

Getting the bytes out of an image inside typst can be done through the read() function, see the grayness manual for how they then use these bytes: https://raw.githubusercontent.com/nineff/grayness/5fb22a720dd3ed1f941cccf84b3bc68508aea351/doc/manual.pdf