4

There's an old bug in JavaFX which prevents you to save images properly using

ImageIO.write(SwingFXUtils.fromFXImage( wi, null), "jpg", new File( fileName1));

The problem occurs when you take a snapshot from a node and try to save it as jpg file. It doesn't occur when you load a jpg into and image and save that image.

It's been a while now and it still isn't fixed. Is there a proper workaround for this without having to use awt?

I know that SwingFXUtils uses awt internally, but having it in your own project feels just wrong.

I checked the bug reports. They were closed with

Not an Issue and Fixed

However, it is an issue and it isn't fixed.

Here's full example code, please change the fileName1 and fileName2 variables to match your path:

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import javax.imageio.ImageIO;

public class ImageSave extends Application {

    String fileName1 = "c:/temp/1.jpg"; // TODO: change filepath
    String fileName2 = "c:/temp/2.jpg"; // TODO: change filepath

    ImageView imageView;

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

    @Override
    public void start(Stage primaryStage) {

        primaryStage.setTitle("Image Crop");

        BorderPane root = new BorderPane();


        Button button = new Button( "Save");
        button.setOnAction(e -> save());

        root.setTop(button);

        // container for image layers
        ScrollPane scrollPane = new ScrollPane();

        // image layer: a group of images
        Group imageLayer = new Group(); 

        // load the image
//      Image image = new Image( getClass().getResource( "cat.jpg").toExternalForm());
        Image image = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Gatto_europeo4.jpg/1024px-Gatto_europeo4.jpg");

        // the container for the image as a javafx node
        imageView = new ImageView( image);

        // add image to layer
        imageLayer.getChildren().add( imageView);

        // use scrollpane for image view in case the image is large
        scrollPane.setContent(imageLayer);

        // put scrollpane in scene
        root.setCenter(scrollPane);

        primaryStage.setScene(new Scene(root, 1024, 768));
        primaryStage.show();
    }

    private void save() {

        SnapshotParameters parameters = new SnapshotParameters();
        // parameters.setFill(Color.TRANSPARENT);

        WritableImage wi = new WritableImage( (int) imageView.getBoundsInLocal().getWidth(), (int) imageView.getBoundsInLocal().getHeight());
        imageView.snapshot(parameters, wi);

        // save image 
        // !!! has bug because of transparency (use approach below) !!!
        // --------------------------------
        try {

            ImageIO.write(SwingFXUtils.fromFXImage( wi, null), "jpg", new File( fileName1));

            System.out.println( "Image saved to " + fileName1);

        } catch (IOException e) {
            e.printStackTrace();
        }


        // save image (without alpha)
        // --------------------------------
        BufferedImage bufImageARGB = SwingFXUtils.fromFXImage(wi, null);
        BufferedImage bufImageRGB = new BufferedImage(bufImageARGB.getWidth(), bufImageARGB.getHeight(), BufferedImage.OPAQUE);

        Graphics2D graphics = bufImageRGB.createGraphics();
        graphics.drawImage(bufImageARGB, 0, 0, null);

        try {

            ImageIO.write(bufImageRGB, "jpg", new File( fileName2)); 

            System.out.println( "Image saved to " + fileName2);

        } catch (IOException e) {
            e.printStackTrace();
        }

        graphics.dispose();

    }

}

The JavaFX version:

enter image description here

The awt version:

enter image description here

Roland
  • 18,114
  • 12
  • 62
  • 93
  • Maybe I'm missing something, but this works correctly for me on Java 8u60 ea, OS X 10.9.5. Issues like this were also fixed for me in Java 8u20 (see: http://stackoverflow.com/questions/16721917/display-rtp-mjpeg). Note when testing I commented out your snapshot code, made the `image` variable a member of the `ImageSave` class and used `fromFXImage( image, null)` instead of `fromFXImage( wi, null)`. Not sure if those modifications compromised your test or what you were trying to demonstrate. If you still believe it is an issue, there is always: http://bugreport.java.com – jewelsea Jun 23 '15 at 08:43
  • 1
    The snapshot method is the problem, because it sets the PixelWriter.Type to INT_ARGB_PRE, so after that you have an alpha in your Image (even in the Image). And the SwingFXUtils.fromFXImage() will do the rest, even if you try to add a BufferedImage to the method with type RGB. The method should not set alpha on jpeg files! He wants to add the transparency to the snapshot. – aw-think Jun 23 '15 at 09:47
  • @jewelsea: I just tried, you're right. So is NwDx. The snapshot method is the problem. When I load the image and then save it, all is okay. When I snapshot the image and save it, the problem occurs. – Roland Jun 23 '15 at 10:04
  • @NwDX: I don't want to add transparency. I want snapshot & save to jpg work correctly. – Roland Jun 23 '15 at 10:04
  • @Roland Thank you for clearing the problem. – aw-think Jun 23 '15 at 10:14

1 Answers1

2

I've done a bit more reasearch and made an example showing the wrong handling of some methods:

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.IntBuffer;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SplitPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javax.imageio.ImageIO;

public class WritableImageDemo extends Application {

  private Image src;
  private int width;
  private int height;
  ImageView srcView;
  ImageView srcView2;
  ImageView srcView3;

  @Override
  public void start(Stage primaryStage) {
    primaryStage.setTitle("Image");

    src = new Image("http://www.gnu.org/graphics/gnu-head.jpg");
    width = (int) src.getWidth();
    height = (int) src.getHeight();

    srcView = new ImageView(src);
    srcView2 = new ImageView();
    srcView3 = new ImageView();

    ScrollPane scrollPane = new ScrollPane();
    ScrollPane scrollPane2 = new ScrollPane();
    ScrollPane scrollPane3 = new ScrollPane();

    scrollPane.setContent(srcView);
    scrollPane2.setContent(srcView2);
    scrollPane3.setContent(srcView3);

    SplitPane root = new SplitPane(scrollPane, scrollPane2, scrollPane3);

    primaryStage.setScene(new Scene(root, 800, 600));
    primaryStage.show();

    srcView2.setImage(writeToFile());
  }

  private WritableImage writeToFile() {
    WritableImage insert = new WritableImage(width, height);
    WritableImage newimage = new WritableImage(width, height);

    SnapshotParameters parameters = new SnapshotParameters();
    parameters.setFill(Color.TRANSPARENT);
    // make a snapshot
    srcView.snapshot(parameters, insert);

    PixelReader reader = insert.getPixelReader();
    PixelWriter writer = newimage.getPixelWriter();
    WritablePixelFormat<IntBuffer> format = WritablePixelFormat.getIntArgbInstance();

    System.out.println("WritablePixelFormat.getType(): " + format.getType());
    // the following normally creates an exact copy of the original
    for (int x = 0; x < width; x++) {
      for (int y = 0; y < height; y++) {
        int recWidth = 1;
        int recHeight = 1;

        int[] buffer = new int[recWidth * recHeight];
        reader.getPixels(x, y, recWidth, recHeight, format, buffer, 0, recWidth);

        int alpha = 0;
        int red = 0;
        int green = 0;
        int blue = 0;

        for (int color : buffer) {
          alpha += (color >>> 24);
          red += (color >>> 16 & 0xFF);
          green += (color >>> 8 & 0xFF);
          blue += (color & 0xFF);
        }
        alpha = alpha / recWidth / recHeight;
        red = red / recWidth / recHeight;
        green = green / recWidth / recHeight;
        blue = blue / recWidth / recHeight;

        int color = (alpha << 24) + (red << 16) + (green << 8) + blue;
        Arrays.fill(buffer, color);

        writer.setPixels(x, y, recWidth, recHeight, format, buffer, 0, recWidth);
      }

    }
    System.out.println("Reader Type: " + reader.getPixelFormat().getType());
    System.out.println("Writer Type: " + writer.getPixelFormat().getType());

    try {
      BufferedImage bf = SwingFXUtils.fromFXImage(newimage, null);
      ImageIO.write(bf, "jpg", new File("C:\\temp\\test1.jpg"));
    } catch (IOException ex) {
      Logger.getLogger(WritableImageDemo.class.getName()).
              log(Level.SEVERE, null, ex);
    }
    try {
      Image img = new Image(new FileInputStream(new File("C:\\temp\\test1.jpg")));
      srcView3.setImage(img);
    } catch (IOException ex) {
      Logger.getLogger(WritableImageDemo.class.getName()).log(Level.SEVERE, null, ex);
    }
    return newimage;
  }

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

If I run this code, I'll get this result. Showing in the left, the original read image, in middle the copied from the method and in the right the created one by the SwingFXUtils.fromFXImage method. So, this is to me a bug!

enter image description here

Update

On new comment at the bug:

It's probably not relevant to the problem in thie JIRA, but the following is a bug in your program:

srcView.snapshot(parameters, insert); 

You must not rely on snapshot actually using the passed in image. It will if it can, but for correctness you need to use the return value of the method. For example:

insert = srcView.snapshot(parameters, insert);

and they have scheduled the bug fix for version 9.

aw-think
  • 4,723
  • 2
  • 21
  • 42