Search code examples
javarailojaicfimagecfml

How do I save/convert a 24-bit PNG as an 8-bit PNG?


I am creating images using ImageNew (and related) in Railo, which uses JAI under the covers.

When I save an image, I'm getting a 24-bit PNG, but I only need 8-bit. (Simply re-saving the file with as 8-bit with a graphics editor results a quarter to half as many bytes.)

ImageWrite doesn't offer any functionality regarding PNG bit depth, and I can't find any details of doing this with JAI itself either (getting a DNS error for http://jai-core.dev.java.net/)

Update:

Using the Quantize ImageFilter I can reduce the number of colours to 256 - this reduces the file size significantly (but still not as far as processing manually does), but still results in a 24-bit/unpaletted PNG file. Unfortunately, it also removes the transparency, which I need preserved (or at least re-applied.)

If I take the file this produces and run it through OptiPNG (a lossless PNG optimiser), it does produce the indexed 8-bit file and shaves off quite a few bytes and gives acceptable filesizes.

So, the remaining step of the puzzle: how do I re-apply transparency after ImageFilter has removed it (or better, prevent it being removed).

I guess I need some way to do Image.replace('white','transparent') either as a Railo/Java-based solution, or a cross-platform command-line tool.


Solution

  • Ok, so I have a working solution that produces acceptable results - the final files are slightly smaller than my original manual process but visually indistinguishable.

    The solution is not as cross-platform as I'd like (need to go find/build Linux binary for OptiPNG), but it's still a good enough solution.

    As Leigh suggested in the question comments, what I've used is a Quantizer to reduce the colours, then using the MapColorsFilter to fix the fact that the quantizer breaks transparency, then finally, using OptiPNG to compress the resulting file to a reasonable size.

    Here's the relevant code:

    <cfscript>
        var Filename = './filename.png'
        var MyImage = NewImage(Filename)
    
        ImageFilter(MyImage,'quantize',{numColors:256,dither:false})
    
        // ImageFilter(MyImage,'MapColors',{oldColor:'white',newColor:'ffffff00'})
        var TransImage = ImageMapColors(MyImage,'white','ffffff00')
    
        ImageWrite( TransImage , Filename )
    </cfscript>
    
    <cfexecute
        name      = "#Variables.OptiPngExecutable#"
        arguments = "-o9 #Filename#"
        timeout   = 30
    />
    

    There's currently a bug in Railo's ImageFilter for the MapColors filter, so I've had to access the filter directly, here's the code I've used to workaround that:

    <cffunction name="ImageMapColors" output=false >
        <cfargument name="Image" rtype="Image" required />
        <cfargument name="Old"   type="String" required />
        <cfargument name="New"   type="String" required />
    
        <cfset var ObjKey = 'ColorReplacer_#Arguments.Old#_#Arguments.New#' />
    
        <cfif NOT StructKeyExists(Variables,ObjKey)>
            <cfset var Old = createObject('java','railo.commons.color.ColorCaster').toColor(Arguments.Old) />
            <cfset var New = createObject('java','railo.commons.color.ColorCaster').toColor(Arguments.New) />
            <cfset Variables[ObjKey] = createObject("java","railo.runtime.img.filter.MapColorsFilter")
                .init(Old.getRGB(),New.getRGB()) />
        </cfif>
    
        <cfreturn ImageNew( Variables[ObjKey].filter(ImageGetBufferedImage(Arguments.Image),{}) ) />
    </cffunction>