How to display images with rounded Corners / Borders

Bilder in Java darzustellen ist keine gro├če Kunst, dazu gibt es auch ein Tutorial von Sun. Etwas sch├Âner w├Ąren aber abgerundete Ecken. Und noch sch├Âner w├Ąre es, wenn wir einfach eine kleine Componente h├Ątten die das alles selber macht, die wiederverwendbar w├Ąre und die im GUI-Editor einfach zu bedienen w├Ąre. Gesagt getan.

Die Anforderung ist also eine Gui Componente, die im Gui Editor (NetBeans in meinem Falle) wiederverwendbar ist (also ein JavaBean), der man ein Bild ├╝bergeben kann und bei der die Ecken abgerundet sind. Wenn m├Âglich, wollen wir sogar noch einen Rahmen setzen k├Ânnen. Ein kurzer Blick auf die Seite “Painting in AWT and Swing” zeigt, dass wir paintComponent() ├╝berschreiben sollten. Die Componente soll sich der Einfachheit halber der Gr├Â├če des Bildes automatisch anpassen.

Die fertige Klasse sieht dann so aus:

public class ThumbPanel extends JPanel {

    protected BufferedImage image = null;
    public static final String PROP_IMAGE = "image";
    protected int roundness = 10;
    public static final String PROP_ROUNDNESS = "roundness";

    public ThumbPanel() {
        init();
    }

    private void init() {
        setOpaque(false);
    }

    protected void update() {
        if (image != null) {
            setSize(image.getWidth(), image.getHeight());
            setPreferredSize(new Dimension(image.getWidth(), image.getHeight()));
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        if (image == null) {
            return;
        }
        g.setClip(new RoundRectangle2D.Double(0, 0, image.getWidth(), image.getHeight(), roundness, roundness));
        g.drawImage(image, 0, 0, null);
        g.setClip(null);
    }

    public int getRoundness() {
        return roundness;
    }

    public void setRoundness(int roundness) {
        int oldRoundness = this.roundness;
        this.roundness = roundness;
        firePropertyChange(PROP_ROUNDNESS, oldRoundness, roundness);
        update();
    }

    public BufferedImage getImage() {
        return image;
    }

    public void setImage(BufferedImage image) {
        BufferedImage oldImage = this.image;
        this.image = image;
        firePropertyChange(PROP_IMAGE, oldImage, image);
        update();
    }
}

Getter und Setter sind selbsterkl├Ąrend. Update() passt die Komponente der aktuellen Gr├Â├če an. Ein Null-Images possiert in meinem Anwendungsfall nicht, w├Ąre aber offenbar kein Problem das anzupassen. PaintComponent() setzt die Clip-Eigenschaft und malt dann das Bild auf die Komponente – fertig! Das ganze sieht dann so aus:

rounded corners ISo weit so gut. Nun will man aber vielleicht noch einen Rahmen (=Border) dazuf├╝gen. Standard Borders sehen dabei nicht ganz so praktikabel aus, da sie an den Ecken abgeschnitten werden. Also muss wohl oder ├╝bel eine eigene Border her. Sch├Ân w├Ąre ein Rahmen, bei dem man Farbe, Dicke und Rundung einstellen kann. Der Versuch, die LineBorder zu extenden war nicht wirklich von Erfolg gekr├Ânt, da man dabei Dicke und Rundung nicht separat einstellen kann.

Also selber malen. Linien mit RoundRectangle2Ds zu zeichnen, wollte nie so wirklich sch├Ân werden. Insbesondere sobald die Dicke > 1 Pixel sein sollte. Alternative: Roundrect fill und innen wieder Clip’en.┬á Nur dumm, dass sich das Clip dann auch auf das Bild ├╝bertragen hatte. Also per AlphaComposite das Innere einfach ausschneiden. Damit auch bei mehrfachen repaints, nicht immer ein BufferedImage f├╝r den Rahmen erzeugt werden muss, wird das Ergebnis einfach gecached. Die ganze klasse sieht dann so aus:

public class MyRoundBorder implements Border {

    private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
    protected int roundness = 10;
    public static final String PROP_ROUNDNESS = "roundness";
    protected Color color = Color.BLACK;
    public static final String PROP_COLOR = "color";
    protected int thickness = 1;
    public static final String PROP_THICKNESS = "thickness";
    // buffer the created image because recreating a complete image could waste precious time
    protected SoftReference cache = new SoftReference(null);
    protected Rectangle oldR = new Rectangle();
    protected Rectangle oldC = new Rectangle();

    public MyRoundBorder() {
    }

    public MyRoundBorder(int roundness, Color color, int thickness) {
        this.roundness = roundness;
        this.color = color;
        this.thickness = thickness;
    }

    public int getThickness() {
        return thickness;
    }

    public void setThickness(int thickness) {
        int oldThickness = this.thickness;
        this.thickness = thickness;
        propertyChangeSupport.firePropertyChange(PROP_THICKNESS, oldThickness, thickness);
        cache = new SoftReference(null);
    }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        Color oldColor = this.color;
        this.color = color;
        propertyChangeSupport.firePropertyChange(PROP_COLOR, oldColor, color);
        cache = new SoftReference(null);
    }

    public int getRoundness() {
        return roundness;
    }

    public void setRoundness(int roundness) {
        int oldRoundness = this.roundness;
        this.roundness = roundness;
        propertyChangeSupport.firePropertyChange(PROP_ROUNDNESS, oldRoundness, roundness);
        cache = new SoftReference(null);
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(listener);
    }

    @Override
    public void paintBorder(Component c, Graphics g1, int x, int y, int width, int height) {
        Graphics2D g = (Graphics2D) g1;

        BufferedImage buffered = cache.get();
        Rectangle newR = new Rectangle(x, y, width, height);
        if (buffered == null || !c.getBounds().equals(oldC) || !oldR.equals(newR)) {
            buffered = new BufferedImage(c.getWidth(), c.getHeight(), BufferedImage.TYPE_INT_ARGB);
            Graphics2D tmpg = buffered.createGraphics();
            tmpg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            tmpg.setClip(g1.getClip());
            tmpg.setColor(color);
            // fill area
            tmpg.fillRoundRect(x, y, width, height, roundness, roundness);
            // cut out inner
            tmpg.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_OUT));
            tmpg.fillRoundRect(x + thickness, y + thickness, width - (2 * thickness), height - (2 * thickness), roundness, roundness);
            tmpg.dispose();

            cache = new SoftReference(buffered);
            c.getBounds(oldC);
            oldR = newR;
        }
        // draw border upon image
        g.drawImage(buffered, 0, 0, null);
    }

    @Override
    public Insets getBorderInsets(Component c) {
        return new Insets(thickness, thickness, thickness, thickness);
    }

    @Override
    public boolean isBorderOpaque() {
        return true;
    }
}

Die Getter/Setter sind wieder dazu da, eine sch├Âne JavaBean zu bauen, damit die Border auch im GUI Editor einfach zu verwenden ist.

Da normale Borders verwendet werden k├Ânnen, kann nat├╝rlich auch eine CompoundBorder verwendet werden – um zum Beispiel anzuzeigen, wenn ein Bild selektiert ist (dann zB mit zus├Ątzlichem wei├čen Innenrahmen), was dann so aussieht (links einfach, rechts Compond):

CompoundBorder II

erste Schritte mit Nasa World Wind

Photos auf einer Karte anzuzeigen kann so schwer nicht sein m├Âchte man meinen. Anbindung an Google Maps oder Google Earth und gut is.

Will man diese Kartenanzeige jetzt noch in ein Java-Programm integrieren, sieht’s schon anders aus. Google Maps w├Ąre kein Problem, wenn denn JWebPane schon fertig w├Ąre. Ist es aber nicht. Also bleiben derzeit nur noch 2 Methoden: Nasa WorldWind einbinden oder JXMapViewer benutzen.

Der erste Test mit Nasa WorldWind ging erheblich schneller als erwartet: Das NetBeansWiki beschreibt die wenigen n├Âtigen Schritte.

  1. Nasa Worldwind Java SDK herunterladen
  2. In Netbeans eine Library mit den Dateien worldwind.jar, jogl.jar und gluegen-rt.jar anlegen
  3. die Library zum Projekt hinzuf├╝gen
  4. Ein JFrame-Form erstellen
  5. (optional einige JavaBeans in die Palette des GUI Managers hinzuf├╝gen)
  6. WorldWindowGLCanvas in den JFrame ziehen
  7. folgende Imports hinzuf├╝gen:
    import gov.nasa.worldwind.*;
    import gov.nasa.worldwind.avlist.AVKey;
  8. und folgenden Code unter den initComponents() Aufruf des Konstruktors:
    Model m = (Model) WorldWind.createConfigurationComponent(AVKey.MODEL_CLASS_NAME);
    worldWindowGLCanvas1.setModel(m);
  9. In den Projekteigenschaften noch folgende JVM-Property setzen: -Djava.library.path=c:pfadzumnasaworldwindsdk
  10. fertig!

Die ganze Klasse sieht dann so aus:

import gov.nasa.worldwind.*;
import gov.nasa.worldwind.avlist.AVKey;

public class NWW extends javax.swing.JFrame {

    public NWW() {
        initComponents();
        Model m = (Model) WorldWind.createConfigurationComponent(AVKey.MODEL_CLASS_NAME);
        worldWindowGLCanvas1.setModel(m);
    }

    private void initComponents() {
        worldWindowGLCanvas1 = new gov.nasa.worldwind.awt.WorldWindowGLCanvas();
        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("Nasa World WInd");
        setMinimumSize(new java.awt.Dimension(640, 480));
        getContentPane().add(worldWindowGLCanvas1, java.awt.BorderLayout.CENTER);
        pack();
    }

    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new NWW().setVisible(true);
            }
        });
    }
    private gov.nasa.worldwind.awt.WorldWindowGLCanvas worldWindowGLCanvas1;
}

N├╝tzliche Links:

Embedding Swing in JavaFX vs. embedding JavaFX in Swing

“Insider’s Guide to Mixing Swing and JavaFX” lautet der Titel vom Amy Fowlers Blogeintrag. Ich habe mich schon gefreut, dass es mit JavaFX 1.2 endlich m├Âglich ist, JavaFX in Swing einzubetten um so die neuen features in bestehenden Swingapplikationen nutzen zu k├Ânnen, ohne das ganze ├╝ber den (in)offiziellen Hack┬á laufen zu lassen, der im JavaFX Blog unter “How to Use JavaFX in Your Swing Application” beschrieben ist.

Aber: “If you’re a Swing developer […], I’ve broken down the process into 10 steps for integrating your Swing components into a JavaFX application.”
Genau die andere Richtung w├Ąre f├╝r die meisten Swing-Entwickler mit bestehenden Applikationen die interessantere!

Aber 2: “Note: We also recognize the need for the inverse (embedding a JavaFX scene into a Swing app), however that is not supported with 1.2 as it requires a more formal mechanism for manipulating JavaFX objects from Java code.

Also abwarten und hoffen, Hack benutzen, oder derweil einfach sein lassen.

Bilder in Java schnell skalieren / Fast Image Scaling / Resizing in Java

I’m recognizing, that this article faced quite some hits – if you’re a non-german user and want this article to be translated, please leave me a comment – maybe I’m gonna translate it if that is what people want.
Bilder in Java skalieren ist ein Thema f├╝r sich…

Intro

Verf├╝hrerisch ist sie ja, die Image.getScaledImage()-Methode. Und ebenso fatal, denn sie ist eine hei├čer Kandidat den Code elends langsam zu machen. Wie geht’s schneller/besser? Ein wertvoller Link zum Einstieg ist The Perils of Image.getScaledInstance() und die dortigen Links.

Die obige Aussage (bzgl. langsam) ist ohne Zahlen quasi wertfrei. Gerade eben habe ich wieder ein St├╝ck Code vor mir, indem Bilder skaliert werden m├╝ssen – und das schnell, da der Benutzer wartet! Ich habe also ├╝ber den Daumen nicht mehr als 100-200ms Zeit, dem Benutzer┬á ein Ergebnis zu pr├Ąsentieren, bevor die Anwendung langsam wirkt. Also:┬á meinen eigenen ImageScaler verwenden, den ich vor Zeiten mal geschrieben habe oder auf JAI (Java Advanced Imaging) zur├╝ckgreifen?
Der Plan ist klar: eine Performance-Messung muss her (da ich noch wei├č, dass ich damals nicht mit JAI verglichen habe).

Messung / Ergebnisse

Input: Ein Jpeg 4008 x 2443 Pixel / 2,47 MB
Output: Das Bild soll in max. 400 x 400 Pixel eingebettet werden, Seitenverh├Ąltnis soll beibehalten werden (also ~400 x 243 Pixel).
Kandidaten:

  1. Image.getScaledInstance()
  2. mein eigener Scaler
  3. JAI

Setup: Bild ausserhalb der Zeitmessung einlesen, jeweils 20x skalieren und die ben├Âtigte Zeit ausgeben. JAI wird dabei mit nativen DLLs getestet ().

Ergebnis 1:

  1. Image.getScaledInstance(): ~30 ms
  2. mein eigener Scaler: ~234 ms
  3. JAI: ~500 ms

Moment – das ist NICHT was erwartet war.
Image img = src.getScaledInstance(400, -1, Image.SCALE_FAST);
Sieht aus als k├Ânnte man nicht viel falsch machen. – Aber wird da ├╝berhaupt etwas gemacht?
Ver├Ąndern wir den Aufruf:
img.getSource().startProduction(new ImageConsumer() { … leere Methodenr├╝mpfe… }

Ergebnis 2:

  1. Image.getScaledInstance(): 35781 ms
  2. mein Scaler:  ~234 ms
  3. JAI: ~500 ms

Aha. So sieht das aus wie erwartet. getScaledInstance() skaliert das Bild also erst bei Bedarf – leider sehr langsam.

Ergebnis 3 – 1x, 5x und 50x kleinskalieren:

  1. mein Scaler:  ~ 60 ms, ~ 90 ms, ~460 ms
  2. JAI: ~500 ms, 500 ms, 500 ms

Ahja – JAI cached offenbar. Interessant zu wissen – bei entsprechendem Szenario also sicher die bessere Wahl – diesmal gewinnt aber mein Scaler, da hier nur kleinskaliert werden soll und die restlichen JAI-Features eh ungenutzt bleiben.

Fazit

Das Key Feature meines Skalers ist die Essenz aus vielen Blogs und JavaOne-Folien:

  1. Solange das Bild gr├Â├čer als die doppelte Zielgr├Â├če ist: Bild mit Faktor 0.5 und Nearest Neighbor Interpolation skalieren. – Um nicht duzende Zwischenbilder erzeugen zu m├╝ssen (was bei gro├čen Eingangsbildern richtig viel Speicher kosten kann, da die Bilder ja im Speicher dekomprimiert werden m├╝ssen!), skaliere ich in einem Schritt auf das kleinste Bild, das noch gr├Â├čer als das Zielbild ist.
  2. letzten Skalierungsschritt mit Bilinearer Interpolation skalieren.

Code:

public class ImageScaler {

    public BufferedImage scaleImage(BufferedImage img, Dimension d) {
        img = scaleByHalf(img, d);
        img = scaleExact(img, d);
        return img;
    }

    private BufferedImage scaleByHalf(BufferedImage img, Dimension d) {
        int w = img.getWidth();
        int h = img.getHeight();
        float factor = getBinFactor(w, h, d);

        // make new size
        w *= factor;
        h *= factor;
        BufferedImage scaled = new BufferedImage(w, h,
                BufferedImage.TYPE_INT_RGB);
        Graphics2D g = scaled.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
        g.drawImage(img, 0, 0, w, h, null);
        g.dispose();
        return scaled;
    }

    private BufferedImage scaleExact(BufferedImage img, Dimension d) {
        float factor = getFactor(img.getWidth(), img.getHeight(), d);

        // create the image
        int w = (int) (img.getWidth() * factor);
        int h = (int) (img.getHeight() * factor);
        BufferedImage scaled = new BufferedImage(w, h,
                BufferedImage.TYPE_INT_RGB);

        Graphics2D g = scaled.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(img, 0, 0, w, h, null);
        g.dispose();
        return scaled;
    }

    float getBinFactor(int width, int height, Dimension dim) {
        float factor = 1;
        float target = getFactor(width, height, dim);
        if (target <= 1) { while (factor / 2 > target) { factor /= 2; }
        } else { while (factor * 2 < target) { factor *= 2; }         }
        return factor;
    }

    float getFactor(int width, int height, Dimension dim) {
        float sx = dim.width / (float) width;
        float sy = dim.height / (float) height;
        return Math.min(sx, sy);
    }
}

N├╝tzliche Links:
public class ImageScaler {public BufferedImage scaleImage(BufferedImage img, Dimension d) {
img = scaleByHalf(img, d);
img = scaleExact(img, d);
return img;
}

private BufferedImage scaleByHalf(BufferedImage img, Dimension d) {
int w = img.getWidth();
int h = img.getHeight();
float factor = getBinFactor(w, h, d);

// make new size
w *= factor;
h *= factor;
BufferedImage scaled = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g.drawImage(img, 0, 0, w, h, null);
g.dispose();
return scaled;
}

private BufferedImage scaleExact(BufferedImage img, Dimension d) {
float factor = getFactor(img.getWidth(), img.getHeight(), d);

// create the image
int w = (int) (img.getWidth() * factor);
int h = (int) (img.getHeight() * factor);
BufferedImage scaled = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);

Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(img, 0, 0, w, h, null);
g.dispose();
return scaled;
}

float getBinFactor(int width, int height, Dimension dim) {
float factor = 1;
float target = getFactor(width, height, dim);
if (target <= 1) { while (factor / 2 > target) { factor /= 2; }
} else { while (factor * 2 < target) { factor *= 2; }         }
return factor;
}

float getFactor(int width, int height, Dimension dim) {
float sx = dim.width / (float) width;
float sy = dim.height / (float) height;
return Math.min(sx, sy);
}
}

Nimbus Farbpalette / color palette

Im Nimbus L&F k├Ânnen Farben f├╝r das ganze L&F sehr flexibel eingestellt werden — wenn man die Namen der UIProperties kennt:

Die Farbpalette für Nimbus L&F kann man bei JasperPotts einsehen:  http://jasperpotts.com/blogfiles/nimbusdefaults/nimbus.html
Der zugeh├Ârige Blogpost: http://www.jasperpotts.com/blog/2008/08/nimbus-uimanager-uidefaults/
Diese Defaults kommen aus com.sun.java.swing.plaf.nimbus.NimbusDefaults#initializeDefaults(UIDefaults d).
Die zugeh├Ârigen Sourcen sind verf├╝gbar, wenn man das JDK heruntergeladen hat.

Create Table IF NOT EXISTS … in JavaDB/Derby

In MySQL gibt es das praktische Konstrukt “Create Table IF NOT EXISTS foo”.
M├Âchte man dieselbe Funktionalit├Ąt in Apache Derby/JavaDB, wird oft empfohlen, ein
Select auf die entsprechende Tabelle durchzuf├╝hren und die entsprechende Exception
abzufangen (siehe z.B. hier). – F├╝r ambitionierte Programmierer nur bedingt akzeptabel,
da Flu├čkontrolle durch Exceptions nur im Ausnahmefall eine sch├Âne L├Âsung darstellt (siehe z.B. hier).

Eine sch├Ânere L├Âsung ist es, zu pr├╝fen, ob die Tabelle (hier “Foo”) existiert, um dann ohne Exception
entsprechen reagieren zu k├Ânnen:

DatabaseMetaData dmd = conn.getMetaData();
ResultSet rs = dmd.getTables(null,”APP”, “FOO”,null);
if (!rs.next()) {
s.executeUpdate(“CREATE TABLE FOO (I INT)”);
}