Treviño
Treviño

Reputation: 3528

JavaScript optimized version of ARGB to RGBA array conversion

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>

Upvotes: 0

Views: 466

Answers (3)

Mister Jojo
Mister Jojo

Reputation: 22265

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; }

Upvotes: 0

Mister Jojo
Mister Jojo

Reputation: 22265

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>

Upvotes: 0

Trevi&#241;o
Trevi&#241;o

Reputation: 3528

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.

Upvotes: 1

Related Questions