How to persist JavaFX Image?

103 views Asked by At

Imagine, we have a Painting Tool written in JavaFX 8 (soon migrating to 11), where the user can draw shapes on a background image. Several types of images (PNG/JPG/GIF) as background are supported.

Lately our customers started using bigger and bigger background images, so we struggle with graphic memory issues, since we are not using any tiling principles or anything like this yet.

To achieve some small performance gains, when the user wants to load the project, we created our own binary image format, instead of storing images in the original format.

When we save the image, we always use all 4 Channels (r,g,b and alpha), eventhough this is not necessary with JPGs, which are used in most cases, when it comes to big (15000x10000 px) background images:

public class ImageExample extends Application
{
  public static void save( final OutputStream out, final Image image )
      throws IllegalArgumentException, IOException
  {
    if ( image.getProgress() != 1.0 )
    {
      throw new IllegalArgumentException( "Image still loading in Background." );
    }

    final int width = (int) image.getWidth();
    final int height = (int) image.getHeight();
    final PixelReader pixelReader = image.getPixelReader();

    if ( width <= 0 || height <= 0 )
    {
      throw new IllegalArgumentException(
          MessageFormat.format( "Width and Height have to be > 0 [ w{0}, h{1} ].", width, height ) );
    }

    System.out.println( "Pixelreader knows the image format: " + image.getPixelReader().getPixelFormat() );

    //Here it would be great, if we could choose the number of channels and format ourselfs,
    //but we failed to extends WritablePixelFormat... BYTE_RGB is the only one with 3 Channels, but
    //we can't use it in getPixels(...) since its not extending WritablePixelFormat.
    final int pixelChannels = 4;
    final WritablePixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteBgraInstance();

    final byte[] widthXheight = ByteBuffer.allocate( 8 )
        .putInt( width )
        .putInt( height )
        .array();
    out.write( widthXheight );

    final byte[] pixelBuffer = new byte[width * pixelChannels];
    for ( int row = 0; row < height; ++row )
    {
      //But we can only use WritablePixelFormat here, so we can't write with PIXELFORMAT BYTE_RGB here...
      pixelReader.getPixels( 0, row, width, 1, pixelFormat, pixelBuffer, 0, pixelBuffer.length );
      out.write( pixelBuffer );
    }

    out.flush();
  }


  @Override
  public void start( final Stage primaryStage ) throws Exception
  {
    final Image image = new Image( "image.jpg" );

    final Path tempFile = Files.createTempFile( "raw_image.", ".data" );
    tempFile.toFile().deleteOnExit();

    try (
        final OutputStream fileOut = Files.newOutputStream( tempFile );
        final CheckedOutputStream hashOut = new CheckedOutputStream( fileOut, new CRC32() ); )
    {
      save( hashOut, image );
      System.out.println( "generated hash: " + hashOut.getChecksum().getValue() );
    }
  }

  public static void main( final String[] args )
  {
    launch( args );
  }
}

So to reduce the memory costs by 25%, we wanted to reduce the PixelFormat to 3 channels by extending WritablePixelFormat in case we are handling a JPG, but unfortunatly all WritablePixelFormats have class protected constructors, so its not possible for me to write my own implementation. The already existing ByteRGB Format is the only one implementing PixelFormat, but does not extend WritablePixelFormat, so I can't use this either to pass it to the pixelreader.getPixels(...) Method.

Has anyone any ideas on how to move forward from here?

1

There are 1 answers

1
swpalmer On

You have a couple options to get 3 Bytes-Per-Pixel data:

Process the array you get that has 4 bytes per pixel to throw out the alpha channel and pack the remaining RGB bytes together before writing.

or

Use the Swing interoperability classes to convert to an AWT BufferedImage:

BufferedImage bimg = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
BufferedImage res = SwingFXUtils.fromFXImage(image, bimg);

However, since memory issues are your concern, creating a whole new image isn't likely to be the best option. I would just suck-up the performance hit and process your 4 BPP array down to 3 BPP.

You can also file an enhancement request for JavaFX, but of course that will take awhile and may never result in anything.