0

I want to convert an Uint8Array of bytes that contains an ARGB image into its RGBA representation, however I would like to get that with something more opimized than what I propose here, using byte shifting for example.

What I do right now is to just swap the bytes order in the sense that (for example, but length can be any multiple of 4):

let input = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 ];
let expected = [0x22, 0x33, 0x44, 0x11, 0x88, 0x55, 0x66, 0x77 ];

So the simple case would be:

function argbToRgba(src) {
    let dest = new Uint8Array(src.length)

    for (let i = 0; i < src.length; i += 4) {
        let a = src[i]
        let r = src[i + 1]
        let g = src[i + 2]
        let b = src[i + 3]

        dest[i] = r;
        dest[i + 1] = g;
        dest[i + 2] = b;
        dest[i + 3] = a;
    }

    return dest;
}

However, ideally I'd like to build the RGBA value using byte shifting (and that's the trivial part), but my problem is then how to put such uint32 number into the dest (which must be an Uint8Array and ideally even the same of src so to do inplace-changes).

I mean, in C once I've my uint32_t rgbaColor defined, I can just memwrite or assign the int to the array index and the overflow will do the rest. But how do I do this in JS?


Edit: After some tests, it looks that the initial proposal is still the fastest, in fact

function argbToRgbaDataView(src, inline=false) {
    let dest = inline ? src : new Uint8Array(src.length);
    let srcView = new DataView(src.buffer);
    let destView = new DataView(dest.buffer);

    for (let i = 0; i < src.length; i += 4) {
        let argb = srcView.getUint32(i);
        let rgba = (argb & 0x00FFFFFF) << 8 |
                   (argb & 0xFF000000) >>> 24;
        destView.setUint32(i, rgba);
    }

    return dest;
}

function argbToRgbaDataViewInline(src) {
    return argbToRgbaDataView(src, true);
}

function argbToRgbaSwap(src, inline=false) {
    let dest = inline ? src : new Uint8Array(src.length);

    for (let i = 0; i < src.length; i += 4) {
        let a = src[i]
        let r = src[i + 1]
        let g = src[i + 2]
        let b = src[i + 3]

        dest[i] = r;
        dest[i + 1] = g;
        dest[i + 2] = b;
        dest[i + 3] = a;
    }

    return dest;
}

function argbToRgbaSwapInline(src) {
    return argbToRgbaSwap(src, true);
}

function argbToRgbaSwapNoVars(src, inline = false) {
    let dest = inline ? src : new Uint8Array(src);

    for (let i = 0; i < src.length; i += 4) {
        let a = src[i]

        dest[i] = src[i + 1];
        dest[i + 1] = src[i + 2];
        dest[i + 2] = src[i + 3];
        dest[i + 3] = a;
    }

    return dest;
}

function argbToRgbaSwapNoVarsInline(src) {
    return argbToRgbaSwapNoVars(src, true);
}

// From https://stackoverflow.com/a/60639510/210151
function argb2rgbaStackOverflow(inArr) {
    return inArr.reduce((a, c, i, t) => {
        if (i % 4 === 0) {
            let [A, R, G, B] = t.slice(i, i + 4)
            a.push(R, G, B, A)
        }
        return a
    }, [])
}

function measureFunction(func) {
    let preTime = new Date().getTime();
    let ret = func.call(...arguments);
    console.log(`Calling ${func.name} took ${new Date().getTime() - preTime}ms`);
    return ret;
}

function createRandomArray(size) {
    return new Uint8Array(size).fill().map((a, i) =>
        a = i).sort(() => Math.random() - 0.5);
}

function iconSizeToBytes(iconSize) {
    const bytesPerPixel = 4;
    return iconSize * iconSize * bytesPerPixel;
}

// This is to add support to console.log to gjs
try {
    console;
} catch(e) {
    window.console = {
        log: function() { print(...arguments) },
    };
}

let allSizes = [
    iconSizeToBytes(32),
    iconSizeToBytes(64),
    iconSizeToBytes(512),
    iconSizeToBytes(1024),
    iconSizeToBytes(2048),
];

for (let size of allSizes) {
    console.log(`Creating random array of ${size/(1024 * 1024)}Mbyte...`);
    let randomArray = measureFunction(createRandomArray, size);

    measureFunction(argbToRgbaDataView, randomArray);
    measureFunction(argbToRgbaDataViewInline, randomArray);
    measureFunction(argbToRgbaSwap, randomArray);
    measureFunction(argbToRgbaSwapInline, randomArray);
    measureFunction(argbToRgbaSwapNoVars, randomArray);
    measureFunction(argbToRgbaSwapNoVarsInline, randomArray);
    measureFunction(argb2rgbaStackOverflow, randomArray);

    console.log('------------------------------------------------------');
}
<script src="https://rawgit.com/eu81273/jsfiddle-console/master/console.js"></script>
Treviño
  • 2,999
  • 3
  • 28
  • 23
  • for those who are not very familiar with the Uint8Array could you provide an example of incoming and outgoing values? I may have an idea for an answer, but failing to answer correctly at least you will have been educational. – Mister Jojo Mar 11 '20 at 14:51
  • @MisterJojo ok, I've just added that. – Treviño Mar 11 '20 at 14:59
  • @MisterJojo yeah, was just a typo (fixed), but well... of course this would work, but I want an optimized version that uses bit shifting for this – Treviño Mar 11 '20 at 15:03
  • @MisterJojo please check the updated post, it looks like that the initial proposed version is still the fastest so far... – Treviño Mar 12 '20 at 13:47
  • you didn't test my 'first' proposal ( function named **c_ARGB_2_RGBA_8** -> `const c_ARGB_2_RGBA_8=([A1,R1,G1,B1,A2,R2,G2,B2])=>[R1,G1,B1,A1,R2,G2,B2,A2]`... – Mister Jojo Mar 12 '20 at 16:01
  • @MisterJojo no because it's never happening to be 8 values... That was just an example. The array can be any length (but always multiple of 4) – Treviño Mar 12 '20 at 17:00

3 Answers3

1

It looks like that while writing this I found out of the creation of DataView and this indeed simplifies my life for this!

So the problem can be solved with:

function argbToRgba(src) {
    let dest = new Uint8Array(src.length);
    let srcView = new DataView(src.buffer);
    let destView = new DataView(dest.buffer);

    for (let i = 0; i < src.length; i += 4) {
        let argb = srcView.getUint32(i);
        let rgba = (argb & 0x00FFFFFF) << 8 |
                   (argb & 0xFF000000) >>> 24;
        destView.setUint32(i, rgba);
    }

    return dest;
}

Using let dest = src; the change can be done inline as well.

Treviño
  • 2,999
  • 3
  • 28
  • 23
  • Unfortunately this is slower than the proposed one, it looks like the data access isn't optimized as expected – Treviño Mar 12 '20 at 13:59
0

I made my own speed tests about this. On my Linux computer speeds awards are differents on Firefox and Chromium !

you can test by yourself:

function fRGB_0(src)  // optimized
  {
  let dest = new Uint8Array(src.length)
  for (let i = 0; i < src.length; i += 4)
    {
    dest[i]    = src[i +1];  // R
    dest[i +1] = src[i +2];  // G
    dest[i +2] = src[i +3];  // B
    dest[i +3] = src[i];     // A
    }
  return dest;
  }
function fRGB_x(xInOut)
  {
  for (let i = 0; i < xInOut.length; i += 4)
    {
    let x0 = xInOut[i]
    xInOut[i]    = xInOut[i +1];  // R
    xInOut[i +1] = xInOut[i +2];  // G
    xInOut[i +2] = xInOut[i +3];  // B
    xInOut[i +3] = x0;            // A
    }
  }
const twoMb = 2 * 1024 * 1024
  ,   Uint8  =_=>new Uint8Array(8).map(e=>Math.floor(Math.random()*256))
  ,   fRGB_1 =([A1,R1,G1,B1,A2,R2,G2,B2])=>[R1,G1,B1,A1,R2,G2,B2,A2]
  ,   fRGB_2 =inArr=>inArr.reduce((a,c,i,t)=>{if(i%4===0){let [A,R,G,B]=t.slice(i,i+4);a.push(R,G,B,A)}return a},[])
  ;

console.log('generate 2Mb Array for testing...')
let ArrayTest = []
for(let i=twoMb;i--;)  ArrayTest.push( Uint8() );

console.log('start test RGB_0')
console.time('Test RGB_0')
for(let i=twoMb;i--;)  { let bob = fRGB_0(ArrayTest[i]) }
console.timeEnd('Test RGB_0')

console.log('start test RGB_x')
console.time('Test RGB_x')
for(let i=twoMb;i--;)  { fRGB_x(ArrayTest[i]) }
console.timeEnd('Test RGB_x')

console.log('start test RGB_1')
console.time('Test RGB_1')
for(let i=twoMb;i--;)  { let bob = fRGB_1(ArrayTest[i]) }
console.timeEnd('Test RGB_1')

console.log('start test RGB_2')
console.time('Test RGB_2')
for(let i=twoMb;i--;)  { let bob = fRGB_2(ArrayTest[i]) }
console.timeEnd('Test RGB_2')
.as-console-wrapper { max-height: 100% !important; top: 0; }
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40
0

I think I found the quickest solution.

It is largely inspired by yours, but instead of recreating a new Uint8Array with each call,
the displacement of A (Argb -> rgbA) is done directly on the object itself.

function fRGB_x(xInOut)
  {
  for (let i = 0; i < xInOut.length; i += 4)
    {
    let x0 = xInOut[i]
    xInOut[i]    = xInOut[i +1];  // R
    xInOut[i +1] = xInOut[i +2];  // G
    xInOut[i +2] = xInOut[i +3];  // B
    xInOut[i +3] = x0;            // A
    }
  }

let bob = new Uint8Array(8).map((_,i)=>i)

AnswerProof.textContent = `before -> [ ${bob.join(', ')  } ]`  

fRGB_x(bob)

AnswerProof.textContent += `\n\nafter --> [ ${bob.join(', ')  } ]` 
<pre id="AnswerProof"></pre>
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40