material-foundation / material-color-utilities

Color libraries for Material You
Apache License 2.0
1.67k stars 144 forks source link

[TypeScript] Quantization of relatively large images doesn't work in Chrome #130

Open FluorescentHallucinogen opened 10 months ago

FluorescentHallucinogen commented 10 months ago

Quantization of images that contains >= 50139473 pixels doesn't work in Chrome.

Tested in Chrome x64 on Windows.

Not reproducible in Firefox.

This means e.g. an image with a resolution of 7080x7080 px can still be quantized, but 7081x7081 px cannot.

Minimal repro:

import { sourceColorFromImage } from '@material/material-color-utilities';

const image = new Image();
image.src = './7081x7081.png';

const sourceColor = await sourceColorFromImage(image);

console.log('sourceColor', sourceColor);

Error:

image_utils.ts:71 Uncaught RangeError: Invalid array length
    at Array.push (<anonymous>)
    at sourceColorFromImage (image_utils.ts:71:12)
    at async (index):24:22
sourceColorFromImage @ image_utils.ts:71
load (async)
imageBytes @ image_utils.ts:56
sourceColorFromImage @ image_utils.ts:31
(anonymous) @ (index):24

Code line:

https://github.com/material-foundation/material-color-utilities/blob/f5d03da60c268b43928f3a24d6bf499e2564d39a/typescript/utils/image_utils.ts#L71

As far as I know, JavaScript arrays are zero-based and use 32-bit indexes: the index of the first element is 0, and the highest possible index is 4294967294 (2^32−2), for a maximum array size of 4,294,967,295 elements. Not 50,139,473. :)

FluorescentHallucinogen commented 9 months ago

@rodydavis @guidezpl Could you please take a look? 😉

FluorescentHallucinogen commented 9 months ago

@pennzht @marshallworks PTAL.

Nevro commented 8 months ago

why not modify existing buffer?

const _forEach = (tarray, cb) => tarray.forEach(cb) ?? tarray;
return _forEach(new Uint32Array(data.buffer), (abgr, index, tarray) => 
    tarray.set([abgr & 0xFF00FF00 | (abgr & 255) << 16 | abgr >> 16 & 255], index));

..or map new TypedArray if raw buffer is necessary.

return new Uint32Array(data.buffer).map((abgr) => 
    abgr & 0xFF00FF00 | (abgr & 255) << 16 | abgr >> 16 & 255);
Nevro commented 8 months ago

Working sample tested with Chromium

<html>
    <body>
        <script type="module">

            import { QuantizerCelebi, Score } from 'https://unpkg.com/@material/material-color-utilities';

            const image = new Image();
            image.crossOrigin = 'Anonymous';
            image.src = 'https://r4.wallpaperflare.com/wallpaper/727/861/207/spiderman-ps4-spiderman-games-hd-wallpaper-2460826541419b3727663083655888cf.jpg';

            async function sourceColorFromImage(image) {
                const imageBytes = await new Promise((resolve, reject) => {
                    const canvas = document.createElement('canvas');
                    const context = canvas.getContext('2d');
                    if (!context) {
                        reject(new Error('Could not get canvas context'));
                        return;
                    }
                    const callback = () => {
                        canvas.width = image.width;
                        canvas.height = image.height;
                        context.drawImage(image, 0, 0);
                        let rect = [0, 0, image.width, image.height];
                        const area = image.dataset['area'];
                        if (area && /^\d+(\s*,\s*\d+){3}$/.test(area)) {
                            rect = area.split(/\s*,\s*/).map(s => {
                                // tslint:disable-next-line:ban
                                return parseInt(s, 10);
                            });
                        }
                        const [sx, sy, sw, sh] = rect;
                        resolve(context.getImageData(sx, sy, sw, sh).data);
                    };
                    if (image.complete) {
                        callback();
                    }
                    else {
                        image.onload = callback;
                    }
                });

                const bufferArray = new Uint32Array(imageBytes.buffer);

                bufferArray.forEach((abgr, index, tarray) =>
                      tarray.set([abgr & 0xFF00FF00 | (abgr & 255) << 16 | abgr >> 16 & 255], index));

                const result = QuantizerCelebi.quantize(bufferArray, 128);
                const ranked = Score.score(result);
                const top = ranked[0];
                return top;
            }

            const sourceColor = await sourceColorFromImage(image);
            console.log('sourceColor', sourceColor);

        </script>
    </body>
</html>