Skip to content

8388450: ImageIO.write(SwingFXUtils.fromFXImage()) creates 0 length JPEG for some images - #2254

Open
prsadhuk wants to merge 2 commits into
openjdk:masterfrom
prsadhuk:JDK-8388450
Open

8388450: ImageIO.write(SwingFXUtils.fromFXImage()) creates 0 length JPEG for some images#2254
prsadhuk wants to merge 2 commits into
openjdk:masterfrom
prsadhuk:JDK-8388450

Conversation

@prsadhuk

@prsadhuk prsadhuk commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

If an image has RGBA encoding and all pixels are opaque, then SwingFXUtils.fromFXImage(image, null) selects TYPE_INT_ARGB_PRE pixel format but the JPEG ImageIO writer cannot encode this alpha-bearing image pixel format since it has no support for it and returns false; so an empty byte array is created.

Issue is when bimg (used to store the returned pixel data from fromFXImage) is null, fromFXImage uses the JavaFX PixelReader’s storage format, not the alpha values of individual pixels.
For an RGBA PNG, JavaFX commonly decodes the pixels into premultiplied ARGB/BGRA, so fxFormat.getType() is one of INT_ARGB_PRE/BYTE_BGRA_PRE so getBestBufferedImageType returns BufferedImage.TYPE_INT_ARGB_PRE because the pixel format has an alpha component. It does not inspect whether every alpha value happens to be 255.

The existing fromFXImage code only calls checkFXImageOpaque() when the caller supplies a non-null bImg BufferedImage.
If bImg is null, all-opaque RGBA PNG produces an alpha-capable BufferedImage i.e., BufferedImage.TYPE_INT_ARGB_PRE

if (bimg == null) {
    bimg = new BufferedImage(iw, ih, prefBimgType);
}

and since JPEG has no standard alpha channel so it cannot store transparency, so when ImageIO.write(image, "jpg", out) runs, ImageIO looks for a registered JPEG writer which can encode that pre-multiplied-alpha image type,
but the JPEG writer rejects an alpha-bearing BufferedImage, so ImageIO.write finds no suitable writer and returns false
so OutputStream is not written into and have 0 bytes
[Basically the JPEG writer does not check whether the alpha values are all 255; it only sees that the input image has an alpha channel and declines to write it]

The proposed JavaFX change avoids the rejection for an RGBA-formatted but fully opaque image by returning TYPE_INT_RGB, which JPEG can encode.
ie., for a JavaFX image with an alpha-capable format but only opaque pixels, fromFXImage(image, null) is made to choose RGB format rather than ARGB.
Additionally, checkFXImageOpaque is improved to scan one row at a time instead of costly full-image scan so that unnecessary Color object for each pixels is not created just to inspect alpha.

A regression subtest is added to existing testcase



Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (2 reviews required, with at least 1 Reviewer, 1 Author)

Issue

  • JDK-8388450: ImageIO.write(SwingFXUtils.fromFXImage()) creates 0 length JPEG for some images (Bug - P3)

Reviewers

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/2254/head:pull/2254
$ git checkout pull/2254

Update a local copy of the PR:
$ git checkout pull/2254
$ git pull https://git.openjdk.org/jfx.git pull/2254/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 2254

View PR using the GUI difftool:
$ git pr show -t 2254

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/2254.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper

bridgekeeper Bot commented Aug 10, 2026

Copy link
Copy Markdown

👋 Welcome back psadhukhan! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk

openjdk Bot commented Aug 10, 2026

Copy link
Copy Markdown

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk openjdk Bot added the rfr Ready for review label Aug 10, 2026
@openjdk

openjdk Bot commented Aug 10, 2026

Copy link
Copy Markdown

The total number of required reviews for this PR has been set to 2 based on the presence of this label: rfr. This can be overridden with the /reviewers command.

@mlbridge

mlbridge Bot commented Aug 10, 2026

Copy link
Copy Markdown

Webrevs

for (int y = 0; y < ih; y++) {
pr.getPixels(0, y, iw, 1, format, pixels, 0, iw);
for (int pixel : pixels) {
if ((pixel >>> 24) != 0xff) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have done
((pixel & 0xff000000) != 0xff000000)

but I think there is no difference in performance whatsoever

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then let it remain same :-)

if (color.getOpacity() != 1.0) {
int[] pixels = new int[iw];
WritablePixelFormat<IntBuffer> format =
PixelFormat.getIntArgbPreInstance();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious: why is this line broken? it fits in 120 columns just fine. time to update the formatting rules?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated..it was just to keep line consistent with the javadoc beneath

@@ -250,16 +256,15 @@ public static BufferedImage fromFXImage(Image img, BufferedImage bimg) {
int ih = (int) img.getHeight();
PixelFormat<?> fxFormat = pr.getPixelFormat();
boolean srcPixelsAreOpaque = false;
boolean opacityMatters = bimg == null ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: this is calculated even when it's not needed. could it be moved to L267?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok..modified

if (bimg != null &&
(bimg.getType() == BufferedImage.TYPE_INT_BGR ||
bimg.getType() == BufferedImage.TYPE_INT_RGB)) {
case BYTE_INDEXED:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: this switch statement is missing BYTE_BGRA. is this a problem?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, missed...added

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to iterate over every PixelFormat.Type using WritableImage(PixelBuffer) constructor to make sure we are getting a meaningful result in each case?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess PixelBuffer supports only INT_ARGB_PRE and BYTE_BGRA_PRE

Pixel data should be stored either in an IntBuffer using a PixelFormat of type INT_ARGB_PRE or in a ByteBuffer using a PixelFormat of type BYTE_BGRA_PRE.

INT_ARGB_PRE is already being tested..there's not much test coverage to iterate so I guess let it
stay small and target the reported behavior

@andy-goryachev-oracle

Copy link
Copy Markdown
Contributor

Something is wrong: the reproducer fails to render the PNG image attached to the ticket,
ImageIO.getWriter() L1558 returns an empty iterator, so ImageIO.write() fails

    private static byte[] writeImage(Image im, String format) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(65536);
        try {
            // using disk cache slows things down
            boolean old = ImageIO.getUseCache();
            ImageIO.setUseCache(false);
            try {
                var bi = ImgUtil.fromFXImage(im, null);
                ImageIO.write(bi, format, out);
            } finally {
                ImageIO.setUseCache(old);
            }
        } finally {
            out.close();
        }
        return out.toByteArray();
    }

@prsadhuk

Copy link
Copy Markdown
Collaborator Author

Something is wrong: the reproducer fails to render the PNG image attached to the ticket, ImageIO.getWriter() L1558 returns an empty iterator, so ImageIO.write() fails

    private static byte[] writeImage(Image im, String format) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(65536);
        try {
            // using disk cache slows things down
            boolean old = ImageIO.getUseCache();
            ImageIO.setUseCache(false);
            try {
                var bi = ImgUtil.fromFXImage(im, null);
                ImageIO.write(bi, format, out);
            } finally {
                ImageIO.setUseCache(old);
            }
        } finally {
            out.close();
        }
        return out.toByteArray();
    }

I guess it will fail as ImgUtil class which is copied implementation of SwingFXUtils is not having this PR fix..
If I change ImageWriteTest_8388450.java to call SwingFXUtils.fromFXImage it passes

./jdk/bin/java @C:/Users/Prasantas/dev/javafx/jfx/rt/build/run.args ImageWriteTest_8388450.java
OK eclipse-key-mappings.png test

private static byte[] writeImage(Image im, String format) throws IOException {
       ByteArrayOutputStream out = new ByteArrayOutputStream(65536);
       try {
           // using disk cache slows things down
           boolean old = ImageIO.getUseCache();
           ImageIO.setUseCache(false);
           try {
               var bi = SwingFXUtils.fromFXImage(im, null);
               ImageIO.write(bi, format, out);
           } finally {
               ImageIO.setUseCache(old);
           }
       } finally {
           out.close();
       }
       return out.toByteArray();
   }

@andy-goryachev-oracle

Copy link
Copy Markdown
Contributor

ImgUtil class which is copied implementation of SwingFXUtils is not having this PR fix.

you are right, I am sorry.

this also means the RichTextArea incubator module should use SwingFXUtils directly, adding dependency on javafx.swing.

@kevinrushforth

Copy link
Copy Markdown
Member

ImgUtil class which is copied implementation of SwingFXUtils is not having this PR fix.

you are right, I am sorry.

this also means the RichTextArea incubator module should use SwingFXUtils directly, adding dependency on javafx.swing.

Hmm. Maybe as a stop-gap, but it would not be acceptable for any javafx.* modules to depend on javafx.swing, so this would have to be replaced before we could ever finalize RichTextArea.

@andy-goryachev-oracle

Copy link
Copy Markdown
Contributor

it would not be acceptable for any javafx.* modules to depend on javafx.swing

What can we do?

We could provide a utility in javafx.graphics (?) to read/write a subset of formats natively (PNG/JPG should be sufficient) that works directly with Image instead of BufferedImage and does not involve ImageIO.

Or we can keep duplicating the code and potentially risk tripping over the same situation in the future.

@andy-goryachev-oracle

Copy link
Copy Markdown
Contributor

or we could port ImageIO parts to work with JPG/PNG in javafx.

@andy-goryachev-oracle

Copy link
Copy Markdown
Contributor

Applied these changes to ImgUtil in #2224.

@andy-goryachev-oracle

Copy link
Copy Markdown
Contributor

@Ziad-Mid could you be the second reviewer please?

@andy-goryachev-oracle

Copy link
Copy Markdown
Contributor

I've filed https://bugs.openjdk.org/browse/JDK-8390345 to avoid duplicating image i/o code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rfr Ready for review

Development

Successfully merging this pull request may close these issues.

3 participants