AffineTransform totally breaks drawing

I didn’t believe how broken this was until I saw it with my own eyes.

With an Affine Transfrom drawImage does not work. Period. It is completely broken such that I can’t possibly imagine how it passes any tests that Sun might have. (Tested on Windows XP)
I’ve tested with 1.4.2_04 and 1.5.0 beta 3 build 56
same results on both.
Try this code, drag the windows over each other, off the screen edge etc. Note how the ‘stretched’ window simply doesn’t paint properly if at all, while the un-stretched window is fine.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;

/*
 * Created on 22-Jun-2004
 *
 * @author scott.palmer
 */
public class TransformTest
{
      public static void main(String[] args)
      {
            JFrame frameNT = new JFrame("Transform Test - Plain");
            MenuScreen screenNT = new MenuScreen(720,480,false);
            frameNT.getContentPane().add( screenNT, BorderLayout.CENTER );
            
            JFrame frameT = new JFrame("Transform Test - With Transform");
            MenuScreen screenT = new MenuScreen(720,480,true);
            frameT.getContentPane().add( screenT, BorderLayout.CENTER );

            frameNT.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frameT.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frameNT.pack();
            frameT.pack();
            frameNT.show();
            frameT.show();
      }
}

class MenuScreen extends JPanel
{
      public MenuScreen(int width, int height, boolean anamorphic)
      {
            compositeImage = new BufferedImage(width, height,
                        BufferedImage.TYPE_INT_ARGB);
            size.width = width;
            size.height = height;
            this.anamorphic = anamorphic;
            
            setDoubleBuffered(false);
            setOpaque(true);
      }

      protected void paintComponent(Graphics g)
      {
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g;
            AffineTransform savedAT = null;
            if(anamorphic)
            {
                  // 4:3 -> 16:9
                  savedAT = g2.getTransform();
                  g2.setTransform(AffineTransform.getScaleInstance(4.0 / 3.0, 1.0));
                  g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            }
            if( dirty )
                  rebuildCompositeImage(0);
            g2.drawImage(compositeImage, 0, 0, null);
            if(savedAT != null)
                  g2.setTransform(savedAT);
      }

      protected void rebuildCompositeImage(int fromItem)
      {
            // TODO account for insets
            Graphics2D g2 = (Graphics2D) compositeImage.getGraphics();
            g2.setBackground(getBackground());
            g2.clearRect(0, 0, getWidth(), getHeight());

            // leave some space around the edges
            int safeTitleX = (int) ((size.width - (size.width* 0.8 )) / 2.0 + 0.5);
            int safeTitleY = (int) ((size.height - (size.height* 0.8 )) / 2.0 + 0.5);
            int safeWidth = size.width - safeTitleX * 2;
            int safeHeight = size.height - safeTitleY * 2;
            int titleLines = 1;

            g2.setColor( textColor );
            g2.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON );
            g2.setRenderingHint( RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON );
            g2.setRenderingHint( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY );
            // draw title
            FontMetrics fm = g2.getFontMetrics(titleFont);
            int tHeight = fm.getHeight();
            if( title != null )
            {
                  int tWidth = fm.stringWidth(title);
                  g2.setFont(titleFont);
                  g2.drawString(title, safeTitleX + (safeWidth - tWidth) / 2, safeTitleY + fm.getAscent());
            }
            // draw Menu items
            fm = g2.getFontMetrics(chapterFont);
            int menuThumbHeight = 60;
            int menuRowHeight = fm.getHeight() + menuThumbHeight;
            int chapsPerRow = 4;
            int chapWidth = (safeWidth/chapsPerRow);
            int zz = fromItem + 1;
            for(int j = 0; j < 4; j++)
            {
                  for(int i = 0; i < 4; i++)
                  {
                        String str = "Menu Item "+zz;
                        int ctPos = (chapWidth - fm.stringWidth(str))/2; // center chapter title
                        int x = safeTitleX + i * chapWidth + ctPos;
                        int y = safeTitleY + (tHeight * titleLines) + menuThumbHeight + fm.getAscent() + j * menuRowHeight;
                        g2.drawString(str,x,y);
                        zz++;
                  }
            }
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2.dispose();
            dirty = false;
      }
      
      public Dimension getPreferredSize()
      {
            return anamorphic ? anamorphicSize : size;
      }

      public Dimension getMinimumSize() {      return getPreferredSize(); }
      public Dimension getMaximumSize() { return getPreferredSize(); }

      private boolean dirty = true; // need to rebuild the image
      private Dimension size = new Dimension(720, 480);
      private Dimension anamorphicSize = new Dimension(720 * 4 / 3, 480);
      private boolean anamorphic;
      private BufferedImage compositeImage;
      private Color textColor = Color.BLACK;
      
      private String title = "Painting Bugs with AffineTransform";
      private Font titleFont = new Font("SansSerif", Font.PLAIN, 20);
      private Font chapterFont = new Font("SansSerif", Font.PLAIN, 18);
}

I think I know where your problem lies.

You should have concatenated your scaled AffineTransform with the Graphics context current AffineTransform object before calling setTransform(). More conveniently, you can just save a copy of the current AffineTransform and call g2d.scale(4.0/3.0, 1.0), do your drawing, then restore the saved copy.

I believe that the Swing renderer would have translated the Graphics context prior to any repainting of the dirty areas of the window/frame when it was obscured.

I was thinking “That can’t be it.” If the Swing renderer ever translated the origin prior to calling paintComponent it would screw up the coordinate system for painting - how would the code possibly know what to paint if the origin was secretly moved? It will set the clipping region, sure… but it can’t move the origin.

Then I went “Ahhhhhhh” the true origin of the graphics context is that of the clip rect, and it is translating the origin back to where it should be for the whole component… I must be undoing that correction

Is that behaviour documented???

[quote]Then I went “Ahhhhhhh” the true origin of the graphics context is that of the clip rect, and it is translating the origin back to where it should be for the whole component… I must be undoing that correction
[/quote]
That is what I THINK is happening, since if you try moving another (heavyweight) window over the stretched window, and then click/focus on the stretched window to bring it to the front, you would notice that the transformed image is actually drawn with a translation similar in size to the distance between the left edges of the two windows.

Again, my guess is that the Swing renderer:

  1. Is notified of a paint() request by the native paint sub-system to repaint the dirty regions

  2. calls Graphics.create(int x, int y, int width, int height) to get a “sub” Graphics object with a new clip and origin (and adjusts the translation to reflect the original Graphics origin) and propagates that “sub” Graphics object down the paint() heirachy instead.

I’m very curious as to why they didn’t just clip the original Graphics object if that’s the case ::slight_smile: .

I haven’t seen any myself, therefore I can only make reasonable speculations. Hopefully someone else can confirm.

Maybe it’s only a Swing quirk. Does AWT work differently?

[quote]I’m very curious as to why they didn’t just clip the original Graphics object if that’s the case ::slight_smile: .
[/quote]
Well that does appear to be the case, so I’m curious as well… I had assumed that clipping of the original graphics object is what would have happened. I’m guessing that using the smaller graphics context saves memory and can lead to better native support for clipping.

Anyway my program is fixed, and I should appologise for jumping to conclusions. draing with an AffineTransform is obviously not broken - my brain is :). Thanks for cluing me in.

[edit] oops, misread… sorry