1

All my images are pixel art images. I want to scale these without any anti-aliasing while maintain aspect-ratio. Currently I can maintain the aspect ratio but the scaling is anti aliased so the images are blurry.

Here is an image:

Here is how it looks with my current code:

<Image key={attr_name} source={{uri:attr_value}} resizeMode="contain" style={{ flex:1 }} resizeMethod="resize" />;

Here is a screenshot of it in my iOS simulator: https://i.stack.imgur.com/7dnzy.png

On the web we accomplish this with CSS:

.pixelated-img {
    image-rendering: optimizeSpeed;
    image-rendering: -moz-crisp-edges;
    image-rendering: -o-crisp-edges;
    image-rendering: -webkit-optimize-contrast;
    image-rendering: optimize-contrast;
    -ms-interpolation-mode: nearest-neighbor;
    image-rendering: pixelated;
}

Edit to add this a great topic I found on stackoverflow but it is for the web - Disable Interpolation when Scaling a <canvas>

Noitidart
  • 35,443
  • 37
  • 154
  • 323

3 Answers3

1

There is a solution: https://github.com/react-native-community/react-native-svg If you use the <Image /> provided by this lib, it will be pixelated (like the image-rendering: pixelated CSS effect).

In your case:

<Image key={attr_name} source={{uri:attr_value}} resizeMode="contain" style={{ flex:1 }} resizeMethod="resize" />;

becomes

<Svg><Image key={attr_name} href={{uri:attr_value}} height={...} width={...} /></Svg>

I am curious, did you finally found a solution?

  • Interesting, I didn't know react-native-svg would do that. The solution I used was to use a webview, I shared that solution here - https://stackoverflow.com/a/44225944/1828637 – Noitidart Nov 25 '19 at 15:42
  • I cannot make it work if the image is a base64. Do you know how to do it? – Esteban Chornet Oct 28 '21 at 09:54
0

I got a solution for myself, I don't like it but it's a stopgap for now.

Please do share your solution if you have one.

What I do is use a WebView. My original goal was to do canvas.toDataURL but it seems to not work, seems like full support is not there on iOS safari. So I am just resizing the WebView with a transparent backgroundColor. And in the html I use the style sheet from original post:

import React, { Component } from 'react'
import { Image, Text, View, WebView } from 'react-native'

const html = `
    <html>
        <head>
            <style>
                body {
                    margin: 0;
                }
                img {
                    image-rendering: optimizeSpeed;
                    image-rendering: -moz-crisp-edges;
                    image-rendering: -o-crisp-edges;
                    image-rendering: -webkit-optimize-contrast;
                    image-rendering: optimize-contrast;
                    -ms-interpolation-mode: nearest-neighbor;
                    image-rendering: pixelated;
                }
            </style>
            <script>
                function whenRNPostMessageReady(cb) {
                    if (postMessage.length === 1) cb();
                    else setTimeout(function() { whenRNPostMessageReady(cb) }, 1000);
                }

                function resizePixelated() {
                    var url = '%%%URL%%%';

                    var img = document.createElement('img');
                    document.body.appendChild(img);
                    img.addEventListener('load', handleImageLoad, false);
                    img.addEventListener('error', handleImageError, false);
                    img.setAttribute('id', 'image');
                    img.setAttribute('src', url);
                }

                function handleImageLoad(e) {
                    if (this.naturalHeight + this.naturalWidth === 0) {
                        this.onerror();
                        return;
                    }

                    var WANTED_HEIGHT = %%%HEIGHT%%%;
                    var WANTED_WIDTH = %%%WIDTH%%%;

                    var naturalHeight = this.naturalHeight;
                    var naturalWidth = this.naturalWidth;

                    postMessage('LOG:' + 'naturalHeight: ' + naturalHeight + ' naturalWidth: ' + naturalWidth);
                    postMessage('LOG:' + 'WANTED_HEIGHT: ' + WANTED_HEIGHT + ' WANTED_WIDTH: ' + WANTED_WIDTH);

                    var factorHeight = WANTED_HEIGHT / naturalHeight;
                    var factorWidth = WANTED_WIDTH / naturalWidth;

                    postMessage('LOG:' + 'factorHeight: ' + factorHeight + ' factorWidth: ' + factorWidth);

                    var byWidthHeight = naturalHeight * factorWidth;
                    var byHeightWidth = naturalWidth * factorHeight;
                    postMessage('LOG:' + 'byWidthHeight: ' + byWidthHeight + ' byHeightWidth: ' + byHeightWidth);

                    var sortable = [
                        { sorter:byWidthHeight, variable:'height', height:byWidthHeight, width:WANTED_WIDTH },
                        { sorter:byHeightWidth, variable:'width',  height:WANTED_HEIGHT, width:byHeightWidth }
                    ];

                    sortable.sort(function byDescSorter(a, b) {
                        return b.sorter - a.sorter;
                    });

                    postMessage('LOG:' + JSON.stringify(sortable));

                    for (var i=0; i<sortable.length; i++) {
                        var variable = sortable[i].variable;
                        var sorter = sortable[i].sorter;
                        if (variable == 'height') {
                            if (sorter <= WANTED_HEIGHT) {
                                break;
                            }
                        } else if (variable == 'width') {
                            if (sorter <= WANTED_WIDTH) {
                                break;
                            }
                        }
                    }

                    if (i >= sortable.length) {
                        postMessage('LOG: THIS SHOULD NEVER HAPPEN');
                    }

                    postMessage('LOG:' + i);

                    var drawWidth = Math.round(sortable[i].width);
                    var drawHeight = Math.round(sortable[i].height);

                    postMessage('LOG:will draw now at width: ' + drawWidth + ' drawHeight: ' + drawHeight);

                    var img = document.getElementById('image');
                    img.setAttribute('width', drawWidth);
                    img.setAttribute('height', drawHeight);

                    var dataurl = '';

                    postMessage('OK:' + drawWidth + '$' + drawHeight + '$' + dataurl);
                }

                function handleImageError() {
                    postMessage('Image failed to load.');
                }

                window.addEventListener('DOMContentLoaded', function() {
                    whenRNPostMessageReady(resizePixelated);
                }, false);
            </script>
        </head>
        <body></body>
    </html>
`;

const STATUS = {
    INIT: 'INIT',
    FAIL: 'FAIL',
    SUCCESS: 'SUCCESS'
}

class ImagePixelated extends Component {
    /* props
    url: dataURL or web url
    height?: number or undefined - set either height or width or both, but one must be set
    width?: number or undefined
    */
    state = {
        status: STATUS.INIT,
        reason: null, // set on STATUS.FAIL
        dataurl: null, // set on STATUS.SUCCESS
        height: null, // set on STATUS.SUCCESS
        width: null // set on STATUS.SUCCESS
    }
    handleMessage = e => {
        const {nativeEvent:{ data }} = e;

        const [action, payload] = data.split(/\:(.+)/); // split on first instance of colon
        // console.log('action:', action, 'payload:', payload);

        switch (action) {
            case 'LOG': {
                    // console.log(payload);
                break;
            }
            case 'OK': {
                    let [ width, height, dataurl ] = data.substr('OK:'.length).split('$');
                    width = parseInt(width);
                    height = parseInt(height);
                    console.log('width:', width, 'height:', height, 'dataurl:', dataurl);
                    this.setState(()=>({status:STATUS.SUCCESS, dataurl, height, width}));
                break;
            }
            default:
                // FAILED // TODO:
                this.setState(()=>({status:STATUS.FAIL, reason:data}));
        }
    }
    getHtml() {
        const { height, width, url } = this.props;
        let html_propified = html.replace('%%%URL%%%', url);

        // because my scaling in WebView is to get max height while maintaining aspect ratio, if one (height or width) is not specificed, instead of setting to undefined, set the other to 1000

        if (isNaN(height) || height === undefined || height === null) html_propified = html_propified.replace('%%%HEIGHT%%%', '1000');
        else html_propified = html_propified.replace('%%%HEIGHT%%%', height);

        if (isNaN(width) || width === undefined || width === null) html_propified = html_propified.replace('%%%WIDTH%%%', '1000');
        else html_propified = html_propified.replace('%%%WIDTH%%%', width);

        return html_propified;
    }
    render() {
        const { status } = this.state;
        switch (status) {
            case STATUS.INIT: {
                const { height, width } = this.state;
                // android: transparent the background in webview here too, because when switch to success, where display is not none, we see a flash of white
                // android: the wrap of view is needed because WebView does not respect height as its a RN bug
                return (
                    <View style={{ display:'none' }}>
                        <WebView source={{ html:this.getHtml() }} style={{ display:'none', backgroundColor:'transparent' }} onMessage={this.handleMessage} />
                    </View>
                )
            }
            case STATUS.FAIL: {
                const { reason } = this.state;
                return (
                    <View>
                        <Text>{reason}</Text>
                    </View>
                )
            }
            case STATUS.SUCCESS: {
                // const { dataurl, height, width } = this.state;
                // return <Image source={{ uri:dataurl, height, width }}  />
                const { height, width } = this.state;
                return (
                    <View style={{ height, width }}>
                        <WebView source={{ html:this.getHtml() }} style={{ height, width, backgroundColor:'transparent' }} />
                    </View>
                )
            }
            // no-default
        }
    }
}

export default ImagePixelated

Usage:

<ImagePixelated url={entity.image} height={90} width={90} />

Works on Android and iOS.

Noitidart
  • 35,443
  • 37
  • 154
  • 323
  • 1
    Hi, I have exactly the same problem. Is the solution efficient when there is, say, 20 images ? – dagatsoin Dec 30 '17 at 20:49
  • @dagatsoin I havent tested performance. I only ever had to show one image per screen so never noticed any issues with it. If you improve it, it would be way awesome of you to share please! Also it would be awesome if you could please up-vote the question and solution. – Noitidart Dec 31 '17 at 02:53
0

Sorry about the lateness of this suggestion, but I'm new here. Anyway, I'm not sure if you're asking for help with programming your own app to do this or just looking for any way to do this with an existing app, but if it's the latter, then here's one idea.

For any desktop OS, just use a good image editor (example = Gimp) & scale up your image using "Resize" & choosing the choice that most image editors call "Nearest neighbor" or "None" (it does exactly what you want).

For android (my only device anymore), my main image editor is "Photo editor" (by dev.macgyver). Using it, scale up your image using "Resize", then go into the "Effects" menu, choose "Mosaic", & adjust it so that it looks like it was scaled up using "Nearest neighbor/None". (Some other android image editors should also have a Mosaic function, but they may call it Pixelate.) I hope that that helps.

  • Thanks, but this was actually a react-native specific question. – Noitidart Apr 25 '18 at 07:44
  • Not only it artificially increases the asset size which is important in app development, but also requires you to store multiple copies of the same image, for every size you might need. – kszl Sep 02 '23 at 22:24