Search code examples
javascripthtml5-canvassvg-filters

Why is HTML5 canvas svg filter lossy?


Below is a sample code that you should be able to run in a browser. It should first load the image. After the image is shown, you can click on it to see the difference between the original image and image with an identitdy svg filter.

It might be hard to see on the browser directly, but you can download the result as image and open it in any image viewer. The result is not all black weirdly. It has blotchy regions that resemble some sort of compression artifact.

So my questions are:

  1. Does applying any svg filter incur quality loss?
  2. Why is it the case? (Maybe I missed it, but it wasn't mentioned in the documents)
  3. How can I achieve a lossless svg filter?

<body>
    <canvas id='canvas' width=850 height=850></canvas>
    <svg width="0" height="0" style="position:absolute;z-index:-1;">
        <defs>
            <filter id="identity" x="0" y="0" width="100%" height="100%">
                <feComponentTransfer></feComponentTransfer>
            </filter>
        </defs>
    </svg>
</body>


<script>
    canvas = document.getElementById('canvas')
    image = new Image()
    image.src = 'https://cms-assets.tutsplus.com/uploads/users/127/posts/31341/final_image/1_10.png'
    image.onload = function() {
        let ctx = canvas.getContext('2d')
        ctx.drawImage(this, 0, 0)
        canvas.image = this
    }

    canvas.onclick = function() {
        let ctx = this.getContext('2d')
        ctx.clearRect(0, 0, this.width, this.height)
        ctx.filter = 'none'
        ctx.drawImage(this.image, 0, 0)
        ctx.globalCompositeOperation = 'difference'
        ctx.filter = 'url(#identity)'
        ctx.drawImage(this.image, 0, 0)
        // 20x the difference
        ctx.filter = 'brightness(20)'
        ctx.drawImage(canvas, 0, 0)
    }
</script>


Solution

  • Linear RGB V sRGB (logarithmic)

    Using a linear color model can often result in loss of color precision. Always use sRGB for the best results.

    Default color interpolation filters are Linear RGB

    The default color model for feComponentTransfer is linear RGB and can result in some artifacts.

    To force the filter to use sRGB use attribute color-interpolation-filters="sRGB"

    Example snippet below shows no loss when using sRGB. When you see the image click it to see result. Should be all black in no loss.

    <body>
        <canvas id='canvas' width=850 height=850></canvas>
        <svg width="0" height="0" style="position:absolute;z-index:-1;">
            <defs>
                <filter id="identity" x="0" y="0" width="100%" height="100%">
                    <feComponentTransfer color-interpolation-filters="sRGB"></feComponentTransfer>
                </filter>
            </defs>
        </svg>
    </body>
    
    
    <script>
        canvas = document.getElementById('canvas')
        image = new Image()
        const ctx = canvas.getContext('2d')
        image.src = 'https://cms-assets.tutsplus.com/uploads/users/127/posts/31341/final_image/1_10.png'
        image.onload = () => ctx.drawImage(image, 0, 0);
    
        canvas.onclick = function() {
            ctx.globalCompositeOperation = 'source-over'
            ctx.filter = 'none'
            ctx.drawImage(image, 0, 0)
            ctx.globalCompositeOperation = 'difference'
            ctx.filter = 'url(#identity)'
            ctx.drawImage(image, 0, 0)
    
            ctx.filter = 'brightness(20)'
            ctx.drawImage(canvas, 0, 0)
        }
    </script>