Wie kann man Rotationen, Skalierungen, etc vornehmen?

Mit der Klasse AffineTransform lassen sich graphische Transformationen wie Rotationen, Scherungen, Translationen (=Verschiebungen) und Skalierungen vornehmen.

Unter einer affinen Transformation versteht man die geometrische Transformation eines Koordinatensystems in ein anderes. Es werden hier zwei Beispiele vorgestellt, die auf unterschiedliche Weise diese Problematik beleuchten.
Script 1 lädt zum Experimentieren ein. Durch einfaches Ein- und Auskommentieren kleiner, nummerierter Scriptbereiche können einfache geometrische Grundformen (ein Rechteck und eine Linie) modifiziert werden.
Script 2 stellt eine kleine Applikation dar, die einige Möglichkeiten der Klasse AffineTransform anhand eines Bildes aufzeigt und die Ergebnisse in einem GUI (Graphical User Interface = Bildschirmfenster) präsentiert.

Das erste Beispiel besteht aus zwei Klassen. Die Hauptklasse mit der main-Methode ist von JFrame abgeleitet und stellt den größten Teil des GUI in einem BorderLayout dar. Ins Zentrum wird ein Objekt der zweiten, von JPanel abgeleiteten Klasse, AffineTransformationPanel, als Zeichenfläche geladen, darunter befindet sich ein JSlider, der, je nach Auswahl, eine schrittweise Transformation ermöglicht. Der Slider ist bei einem ChangeListener angemeldet, der seinen eingestellten Wert an eine Variable des Panel-Objektes übergibt. Sie dient als Stellgröße für die Modifikation des gezeichneten Objektes. Dies wird bei Aktivierung des Sliders entsprechend modifiziert. Anschließend wird das JPanel neu gezeichnet.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class AffineTransformation extends JFrame implements ChangeListener {
    
    AffineTransformationPanel panel;
    
    public AffineTransformation(){
        panel = new AffineTransformationPanel();
        setLayout(new BorderLayout());
        add(panel, BorderLayout.CENTER);
        
        JSlider slider = new JSlider(0, 10, 0);
        slider.addChangeListener(this);
        add(slider, BorderLayout.SOUTH);

        this.setSize(300, 300);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }

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

    public void stateChanged(ChangeEvent e) {
        panel.setNum((double)((JSlider)e.getSource()).getValue()/5);
        panel.repaint();
    }
}

class AffineTransformationPanel extends JPanel {
    
    double num;
    
    public void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;
        g2d.setColor(Color.WHITE);
        g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
        g2d.setColor(Color.BLACK);
        int x = 15;
        int y = 10;
        int h = 100;
        int b = 200;
        Rectangle2D.Double rect = new Rectangle2D.Double(x, y, b, h);
        g2d.setColor(Color.RED);
        g2d.draw(rect);
        
        AffineTransform at = new AffineTransform();
        // (1)Scherung und zusätzliche Translation
        //at.setToShear(num, num/5);
        //at.translate(num*30, num*40);
        
        // (2)Translation (Verschiebung) und zusätzliche Scherung
        //at.setToTranslation(num*30, num*40);
        //at.shear(num, num/5);
        
        // (3)Skalierung
        //at.setToScale(num, num);
        
        // (4)Rotation um den Punkt x/y
        //at.setToRotation(num, x, y);
        
        // (5)Rotation um den Punkt 0/0
        //at.setToRotation(num);
        
        Shape s = at.createTransformedShape(rect);
        g2d.setColor(Color.BLACK);
        g2d.draw(s);
        
        // Verbindung der affinen Transformation mit dem gesamten Grafics-Kontext
        // bewirkt eine Transformation aller nachfolgender Objekte
        g2d.transform(at);
        Line2D.Double line = new Line2D.Double(100, 0, 100, 300);
        g2d.draw(line);
    }
    
    void setNum(double num){
        this.num = num;
    }
}

Diese Variable num ist die einzige, die die Klasse AffineTransformationPanel definiert. In der von JPanel abgeleiteten Klasse sind zudem noch zwei Methoden deklariert. Eine Getter-Methode für die Variable und die überschriebene Methode paintComponent(), die die Zeichenarbeit übernimmt.
Hier wird zunächst das als Parameter übergebene Graphics-Objekt in ein Objekt des Typs Graphics2D gecastet und mit diesem ein weißes gefülltes Rechteck in der Größe des Panels gezeichnet, um eine weiße Grundfläche zu erhalten. In der Folge werden vier Variablen deklariert, die den linken oberen Initialisierungspunkt und die Breite und Höhe eines Rechtecks angeben. Sie werden mit den entsprechenden Werten initialisiert und mit diesen Werten ein Objekt der Klasse Rectangle2D.Double erzeugt. Die Wandlung der int-Werte in die notwendigen double-Werte erfolgt selbstständig. Das Objekt stellt ein als Umriss gezeichnetes Rechteck dar, das auf dem Graphics2D-Objekt durch die Mehtode draw() erzeugt wird. Es dient als Gegenstand der folgenden Manipulationen.

Hierzu wird als erstes ein Objekt der Klasse AffineTransform erzeugt. Auf ihm werden die geometrischen Transformationen vorgenommen. Einige sind in den folgenden fünf Abschnitten dargestellt und können einzeln auskommentiert werden, um ihre Auswirkungen zu erkunden. Allen gemeinsam ist die Tatsache, dass auf dem AffineTransform-Objekt eine Methode setToXXX() ausgeführt wird, wobei XXX angibt, welche Transformation ausgeführt wird. Die vom JSlider belegte Variable num wird hierbei als Parameter übergeben. Eine zusätzliche Transformation kann wie in (1) und (2) gezeigt über die Methoden shear(), translate(), rotate() und scale() durchgeführt werden. Wichtig zu wissen sind drei Dinge:

Die Zeichnung selbst wird nach Anpassung der Vordergrundfarbe durch die Methode draw() vorgenommen, die auf dem Graphics2D-Objekt ausgeführt wird.
Möchte man nicht für jedes zu transformierende Objekt eine andere AffineTransform-Methode aufrufen, sondern verschiedene Objekte auf gleiche Weise manipulieren, so lässt sich das AffineTransform-Objekt auch direkt dem Graphik-Kontext mit der Methode transform() übergeben. Als Beispiel dient die im letzten Abschnitt der Methode erzeugte Linie.

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
 * Die Klasse demonstriert verschiedene grafische Transformationen
 * anhand eines Bildes (Pfad anpassen!)
 */
public class ImageDemo extends JFrame implements ChangeListener, ActionListener {

    private final String bildPfad = "test.jpg";

    private BufferedImage[] img;

    private JRadioButton scaleRB, rotateRB, transRB, scherRB;

    private JLabel origLabel, transLabel;

    private JTextField xScaleField, yScaleField,
            xPosRotateField, yPosRotateField, xTransField, yTransField,
            xScherField, yScherField;

    private JButton button;
    
    JSlider rotateSlider;

    private CardLayout cl;

    private JPanel cardPanel;

    private Box scalePanel, rotatePanel, transPanel, scherPanel;

    private static final long serialVersionUID = 1L;

    public ImageDemo() {

        JPanel rechtsPanel = initRB();

        img = getImages();

        if (img == null) {
            System.out.println("Image-Import fehlgeschlagen!");
            System.exit(1);
        }

        ImageIcon origIcon = new ImageIcon(img[0]);
        origLabel = new JLabel(origIcon);
        origLabel.setBorder(createBorder("Original"));

        ImageIcon transIcon = new ImageIcon();
        transLabel = new JLabel(transIcon);
        transLabel.setBorder(createBorder("Transformation"));

        JPanel centerPanel = new JPanel();
        centerPanel.setLayout(new GridLayout(2, 1));
        centerPanel.setBackground(Color.DARK_GRAY);
        centerPanel.add(origLabel);
        centerPanel.add(transLabel);

        getContentPane().add(centerPanel, BorderLayout.CENTER);
        getContentPane().add(rechtsPanel, BorderLayout.EAST);
        Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
        setSize(dim.width, dim.height);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }

    private Border createBorder(String title) {
        return new TitledBorder(new LineBorder(Color.WHITE), title,
                TitledBorder.ABOVE_BOTTOM, TitledBorder.LEADING, new Font(
                        "Verdana", Font.PLAIN, 12), Color.WHITE);
    }

    private JPanel initRB() {
        scaleRB = new JRadioButton("Skalierung", true);
        scaleRB.addChangeListener(this);
        rotateRB = new JRadioButton("Rotation");
        rotateRB.addChangeListener(this);
        transRB = new JRadioButton("Translation");
        transRB.addChangeListener(this);
        scherRB = new JRadioButton("Scherung");
        scherRB.addChangeListener(this);
        ButtonGroup buttonGroup = new ButtonGroup();
        buttonGroup.add(scaleRB);
        buttonGroup.add(rotateRB);
        buttonGroup.add(transRB);
        buttonGroup.add(scherRB);
        JPanel rBPanel = new JPanel(new GridLayout(4, 1, 0, 5));
        rBPanel.add(scaleRB);
        rBPanel.add(rotateRB);
        rBPanel.add(transRB);
        rBPanel.add(scherRB);

        JLabel xScaleLabel, yScaleLabel, faktorRotateLabel, xPosRotateLabel, yPosRotateLabel, xTransLabel, yTransLabel, xScherLabel, yScherLabel;
        xScaleLabel = new JLabel("X-Wert:");
        yScaleLabel = new JLabel("Y-Wert:");
        faktorRotateLabel = new JLabel("Winkel");
        xPosRotateLabel = new JLabel("X-Wert:");
        yPosRotateLabel = new JLabel("Y-Wert:");
        xTransLabel = new JLabel("X-Wert:");
        yTransLabel = new JLabel("Y-Wert:");
        xScherLabel = new JLabel("X-Wert:");
        yScherLabel = new JLabel("Y-Wert:");

        xScaleField = new JTextField(4);
        xScaleField.setPreferredSize(new Dimension(100, 20));
        yScaleField = new JTextField(4);
        yScaleField.setPreferredSize(new Dimension(100, 20));
        // Bereich 0 - 2*PI
        // wird im Listener durch 100 dividiert
        rotateSlider = new JSlider(0, 628, 0);
        rotateSlider.setMajorTickSpacing(150);
        rotateSlider.setPaintTicks(true);
        xPosRotateField = new JTextField(4);
        yPosRotateField = new JTextField(4);
        xTransField = new JTextField(4);
        yTransField = new JTextField(4);
        xScherField = new JTextField(4);
        yScherField = new JTextField(4);

        Dimension fillDim = new Dimension(10, Short.MAX_VALUE);

        scalePanel = new Box(BoxLayout.Y_AXIS);
        scalePanel.add(xScaleLabel);
        scalePanel.add(xScaleField);
        scalePanel.add(yScaleLabel);
        scalePanel.add(yScaleField);
        scalePanel.add(new Box.Filler(fillDim, fillDim, fillDim));

        rotatePanel = new Box(BoxLayout.Y_AXIS);
        rotatePanel.add(faktorRotateLabel);
        rotatePanel.add(rotateSlider);
        rotatePanel.add(xPosRotateLabel);
        rotatePanel.add(xPosRotateField);
        rotatePanel.add(yPosRotateLabel);
        rotatePanel.add(yPosRotateField);
        rotatePanel.add(new Box.Filler(fillDim, fillDim, fillDim));

        transPanel = new Box(BoxLayout.Y_AXIS);
        transPanel.add(xTransLabel);
        transPanel.add(xTransField);
        transPanel.add(yTransLabel);
        transPanel.add(yTransField);
        transPanel.add(new Box.Filler(fillDim, fillDim, fillDim));

        scherPanel = new Box(BoxLayout.Y_AXIS);
        scherPanel.add(xScherLabel);
        scherPanel.add(xScherField);
        scherPanel.add(yScherLabel);
        scherPanel.add(yScherField);
        scherPanel.add(new Box.Filler(fillDim, fillDim, fillDim));

        cl = new CardLayout(0, 20);
        cardPanel = new JPanel(cl);
        cardPanel.add(scalePanel, "Skalierung");
        cardPanel.add(rotatePanel, "Rotation");
        cardPanel.add(transPanel, "Translation (Verschieben)");
        cardPanel.add(scherPanel, "Scherung");

        button = new JButton("Transformieren");
        button.addActionListener(this);

        JPanel mainPanel = new JPanel(new BorderLayout(5, 10));
        mainPanel.add(rBPanel, BorderLayout.NORTH);
        mainPanel.add(cardPanel, BorderLayout.CENTER);
        mainPanel.add(button, BorderLayout.SOUTH);
        return mainPanel;
    }

    /**
     * skaliert ein BufferedImage
     */
    private BufferedImage[] scaleImage(String xStr, String yStr) {
        BufferedImage[] iArr = getImages();
        if (iArr == null)
            return null;

        // 1.0 ist Originalgroesse
        double x = str2double(xStr);
        double y = str2double(yStr);
        if(x == 0 || y == 0) return null;
        AffineTransform trans = AffineTransform.getScaleInstance(x, y);
        AffineTransformOp op = new AffineTransformOp(trans,
                AffineTransformOp.TYPE_BILINEAR);
        op.filter(iArr[0], iArr[1]);
        return iArr;
    }

    /**
     * dreht ein BufferedImage
     */
    private BufferedImage[] rotateImage(String fStr, String xStr, String yStr) {
        BufferedImage[] iArr = getImages();
        if (iArr == null)
            return null;
        double x = str2double(xStr);
        double y = str2double(yStr);
        if(xStr.equals("-1")) x = iArr[0].getWidth()/2;
        if(yStr.equals("-1")) y = iArr[0].getHeight()/2;

        // um die Mitte rotieren
        // f = 6.284 - volle Drehung
        double f = str2double(fStr);
        AffineTransform trans = AffineTransform.getRotateInstance(f, x, y);
        AffineTransformOp op = new AffineTransformOp(trans,
                AffineTransformOp.TYPE_BILINEAR);
        op.filter(iArr[0], iArr[1]);
        return iArr;
    }

    /**
     * transformiert den Ursprungspunkt eines BufferedImage
     */
    private BufferedImage[] transImage(String xStr, String yStr) {
        BufferedImage[] iArr = getImages();
        if (iArr == null)
            return null;

        // Translation
        double x = str2double(xStr);
        double y = str2double(yStr);
        AffineTransform trans = AffineTransform.getTranslateInstance(x, y);
        AffineTransformOp op = new AffineTransformOp(trans,
                AffineTransformOp.TYPE_BILINEAR);
        op.filter(iArr[0], iArr[1]);
        return iArr;
    }

    /**
     * schert ein BufferedImage
     */
    private BufferedImage[] scherImage(String xStr, String yStr) {
        BufferedImage[] iArr = getImages();
        if (iArr == null)
            return null;

        // scheren
        double x = str2double(xStr);
        double y = str2double(yStr);
        AffineTransform trans = AffineTransform.getShearInstance(x, y);
        AffineTransformOp op = new AffineTransformOp(trans,
                AffineTransformOp.TYPE_BILINEAR);
        op.filter(iArr[0], iArr[1]);
        return iArr;
    }

    /**
     * 
     * @param fileName
     *            String des Originalbildes
     * @return BufferedImage[] Array mit zwei BufferdImages: das erste ist das
     *         Original-Bild, das zweite ist das Zielbild für die
     *         Transformation
     */
    private BufferedImage[] getImages() {
        File file = new File(bildPfad);
        BufferedImage origImg = null;
        try {
            origImg = ImageIO.read(file);
        } catch (IOException ex) {
            System.err.println("Image-Datei kann nicht gelesen werden!");
            System.exit(1);
        }
        if (origImg != null) {
            BufferedImage destImg = new BufferedImage(origImg.getWidth(),
                    origImg.getHeight(), origImg.getType());
            BufferedImage[] i = { origImg, destImg };
            return i;
        }
        return null;
    }

    private void setImg(BufferedImage[] images) {
        if (images == null) {
            System.out.println("fehlgeschlaagen");
            return;
        }
        ImageIcon origIcon = new ImageIcon(images[0]);
        ImageIcon transIcon = new ImageIcon(images[1]);
        origLabel.setIcon(origIcon);
        transLabel.setIcon(transIcon);
    }

    private double str2double(String dStr) {
        if (dStr == null)
            return 1;
        dStr = dStr.replace(',', '.');
        try {
            return new Double(dStr).doubleValue();
        } catch (NumberFormatException nfe) {
            System.out.println("falsches Zahlformat");
        }
        return 0;
    }

    public void stateChanged(ChangeEvent e) {
        if (e.getSource() == scaleRB) {
            cl.show(cardPanel, "Skalierung");
        }
        if (e.getSource() == rotateRB) {
            cl.show(cardPanel, "Rotation");
        }
        if (e.getSource() == transRB) {
            cl.show(cardPanel, "Translation (Verschieben)");
        }
        if (e.getSource() == scherRB) {
            cl.show(cardPanel, "Scherung");
        }
    }

    public void actionPerformed(ActionEvent e) {
        if (scaleRB.isSelected()) {
            if(xScaleField.getText().equals("") || yScaleField.getText().equals("")) return;
            img = scaleImage(xScaleField.getText(), yScaleField.getText());
            setImg(img);
        }
        if (rotateRB.isSelected()) {
            String xFaktor = "-1", yFaktor = "-1";
            if(!xPosRotateField.getText().equals("") && !yPosRotateField.getText().equals("")){
                xFaktor = xPosRotateField.getText();
                yFaktor = yPosRotateField.getText();
            }
            double faktor = (double)rotateSlider.getValue()/100;
            img = rotateImage(new Double(faktor).toString(), xFaktor, yFaktor);
            setImg(img);
        }
        if (transRB.isSelected()) {
            if(xTransField.getText().equals("") || yTransField.getText().equals("")) return;
            img = transImage(xTransField.getText(), yTransField.getText());
            setImg(img);
        }
        if (scherRB.isSelected()) {
            if(xScherField.getText().equals("") || yScherField.getText().equals("")) return;
            img = scherImage(xScherField.getText(), yTransField.getText());
            setImg(img);
        }
    }

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

Affine Transformation