Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8357034

GifImageDecoder can produce wrong transparent pixels

XMLWordPrintable

    • generic
    • generic

      ADDITIONAL SYSTEM INFORMATION :
      Observed on Mac 15.4.1 using Java 25

      A DESCRIPTION OF THE PROBLEM :
      When gif frames are set to disposal method 1 (DISPOSAL_SAVE), and the transparent index changes across frames: sometimes the GifImageDecoder mistakenly replaces opaque pixels with transparent pixels.

      (This error is only triggered when two consecutive frames use identical color palettes, and the transparent pixel index in that palette is a pixel color a previous frame used as an opaque color.)

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the attached test file with "ukraine-flag.gif".

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The test should pass. Although the gif has a color palette with only 2 colors, the resulting BufferedImages should actually use 3 colors (blue, yellow, and transparent)
      ACTUAL -
      The test fails.

      The color palette has 2 colors:
      0 = blue
      1 = yellow

      The last two frames use color index 0 as the transparent pixel index. When GifImageDecoder processes the last (third) frame: it overwrites blue pixels as transparent pixels and it makes the transparent border opaque yellow.

      So the core problem here is some clever gif encoders have figured out how to squeeze (n+1)-many colors into an n-many color palette (if you include the transparent pixel as a color). In this image, for ex, the BufferedImage should have 3 colors: blue, yellow, transparent.

      (This is of course a simplified version of the problem; the original gif that made me notice this issue used all 256-colors, which means the solution may (?) require changing byte-based data to int (or short?) based data.)

      ---------- BEGIN SOURCE ----------
      import java.awt.Graphics2D;
      import java.awt.Image;
      import java.awt.Toolkit;
      import java.awt.image.BufferedImage;
      import java.awt.image.ColorModel;
      import java.awt.image.ImageConsumer;
      import java.awt.image.IndexColorModel;
      import java.net.URL;
      import java.util.ArrayList;
      import java.util.Hashtable;
      import java.util.concurrent.Semaphore;

      public class GifReuseFormerlyTransPixelTest {
          public static void main(String[] args) throws Exception {
              URL srcURL = GifReuseFormerlyTransPixelTest.class.getResource("ukraine-flag.gif");
              BufferedImage[] frames = getFrames(srcURL, 3);

              boolean pass = true;

              // these first two conditions have never failed. The second frame is always fine
              if ( !Integer.toUnsignedString(frames[1].getRGB(100, 50), 16).equals("ff005bbb") ) {
                  System.err.println("On frame #2, the top stripe should be opaque blue");
                  pass = false;
              }

              if ( !Integer.toUnsignedString(frames[1].getRGB(5, 5), 16).equals("0") ) {
                  System.err.println("On frame #2, the top-left corner should be transparent");
                  pass = false;
              }

              // these two conditions failed when we start looking at frame 3
              if ( !Integer.toUnsignedString(frames[2].getRGB(100, 50), 16).equals("ff005bbb") ) {
                  System.err.println("On frame #3, the top stripe should be opaque blue");
                  pass = false;
              }

              if ( !Integer.toUnsignedString(frames[2].getRGB(5, 5), 16).equals("0") ) {
                  System.err.println("On frame #3, the top-left corner should be transparent");
                  pass = false;
              }

              if (!pass) {
                  throw new Error("See log for details");
              }
          }

          private static BufferedImage[] getFrames(URL gifURL, int numberOfFrames) {
              Image image = Toolkit.getDefaultToolkit().createImage(gifURL);
              ArrayList<BufferedImage> returnValue = new ArrayList<>(numberOfFrames);

              Semaphore semaphore = new Semaphore(1);
              semaphore.acquireUninterruptibly();
              image.getSource().startProduction(new ImageConsumer() {
                  BufferedImage bi;
                  int frameCtr = 0;

                  @Override
                  public void setDimensions(int width, int height) {
                      bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
                  }

                  @Override
                  public void setProperties(Hashtable<?, ?> props) {}

                  @Override
                  public void setColorModel(ColorModel model) {}

                  @Override
                  public void setHints(int hintflags) {}

                  @Override
                  public void setPixels(int x, int y, int w, int h, ColorModel model, byte[] pixels, int off, int scansize) {
                      try {
                          final int yMax = y + h;
                          final int xMax = x + w;

                          IndexColorModel icm = (IndexColorModel) model;
                          int[] colorModelRGBs = new int[icm.getMapSize()];
                          icm.getRGBs(colorModelRGBs);
                          int[] argbRow = new int[bi.getWidth()];

                          for (int y_ = y; y_ < yMax; y_++) {
                              int i = y_ * scansize + off;
                              for (int x_ = x; x_ < xMax; x_++, i++) {
                                  int pixel = pixels[i] & 0xff;
                                  argbRow[x_ - x] = colorModelRGBs[pixel];
                              }
                              bi.getRaster().setDataElements(x, y_, w, 1, argbRow);
                          }
                      } catch (RuntimeException e) {
                          // we don't expect this to happen, but if something goes
                          // wrong nobody else will print our stacktrace for us:
                          e.printStackTrace();
                          throw e;
                      }
                  }

                  @Override
                  public void setPixels(int x, int y, int w, int h, ColorModel model, int[] pixels, int off, int scansize) {}

                  @Override
                  public void imageComplete(int status) {
                      try {
                          frameCtr++;

                          BufferedImage copy = new BufferedImage(bi.getWidth(),
                                  bi.getHeight(), BufferedImage.TYPE_INT_ARGB);
                          Graphics2D g = copy.createGraphics();
                          g.drawImage(bi, 0, 0, null);
                          g.dispose();
                          returnValue.add(copy);

                          if (frameCtr == numberOfFrames) {
                              semaphore.release();
                              // if we don't detach this consumer the producer will
                              // loop forever
                              image.getSource().removeConsumer(this);
                              image.flush();
                          }
                      } catch(Exception e) {
                          e.printStackTrace();
                          throw new RuntimeException(e);
                      }
                  }
              });

              semaphore.acquireUninterruptibly();

              return returnValue.toArray(new BufferedImage[0]);
          }
      }

      ---------- END SOURCE ----------

            jdv Jayathirth D V
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: