Search code examples
javamethodsbufferedimage

Editing the Border of a BufferedImage in Java


I am trying to create a method that will add a border to a BufferedImage where both border thickness and border color can be configurable by the parameters of the method.

Border thickness will be a non-negative integer (0, 1, 2, etc.) and corresponds to how many pixels thick the border should be on all four sides inside the perimeter of the image.

Border color is a length 3 integer array, corresponding to the RGB color value that the border should be.

This is the original image: original image

This is what the image should look like with a border of thickness 10 and RGB values 235, 64, 52: resulting image

This is the code I have but I keep running into an out-of-bounds error. error

I would greatly appreciate any help. I also realize I can use Java's graphics tools to edit on top of the image but I'm trying to edit the image itself and not create any kind of overlays.

public static void applyBorder(BufferedImage img, int borderThickness, int[] borderColor) {
    int width = img.getWidth();
    int height = img.getHeight();
    int borderRgb = (borderColor[0] << 16) | (borderColor[1] << 8) | borderColor[2];

    for (int x = 0; x < img.getWidth(); x++) {
      for (int y = 0; y < borderThickness; y++) {
        img.setRGB(x, y, borderRgb); // Top border
        img.setRGB(x, img.getHeight() - 1 - y, borderRgb); // Bottom border
      }
    }

    for (int y = borderThickness; y < img.getHeight() - borderThickness; y++) {
      for (int x = 0; x < borderThickness; x++) {
        img.setRGB(x, y, borderRgb); // Left border
        img.setRGB(img.getWidth() - 1 - x, y, borderRgb); // Right border
      }
    }

    for (int x = 0; x < width; x++) {
      for (int y = 0; y < height; y++) {
        img.setRGB(x + borderThickness, y + borderThickness, img.getRGB(x, y));
      }
    }
  }

This is how I am calling the method in the main class:

int borderThickness = 10;
int[] borderColor = {255, 0, 0};
applyBorder(img, borderThickness, borderColor);
File fileBorder = new File("dog_border.png");
ImageIO.write(img, "png", fileBorder);

Solution

  • There's a few ways you might do this, personally I might consider making use of Area, as it allows you to subtract (or add) shapes together, for example...

    enter image description here

    import java.awt.Color;
    import java.awt.EventQueue;
    import java.awt.Graphics2D;
    import java.awt.GridLayout;
    import java.awt.Rectangle;
    import java.awt.geom.Area;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import javax.imageio.ImageIO;
    import javax.swing.ImageIcon;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    
    public class Main {
        public static void main(String[] args) {
            new Main();
        }
    
        public Main() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        JFrame frame = new JFrame();
                        frame.add(new TestPane());
                        frame.pack();
                        frame.setLocationRelativeTo(null);
                        frame.setVisible(true);
                    } catch (IOException ex) {
                        Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            });
        }
    
        public static BufferedImage applyBorder(BufferedImage img, int borderThickness, Color borderColor) {
            if (borderThickness * 2 > img.getWidth() || borderThickness * 2 > img.getHeight()) {
                return null;
            }
            Area border = new Area(new Rectangle(0, 0, img.getWidth(), img.getHeight()));
            border.subtract(new Area(new Rectangle(
                    borderThickness, 
                    borderThickness, 
                    img.getWidth() - (borderThickness * 2), 
                    img.getHeight() - (borderThickness * 2)
            )));
            BufferedImage targetImg = new BufferedImage(img.getWidth(), img.getHeight(), img.getType());
            Graphics2D g2d = targetImg.createGraphics();
            g2d.drawImage(img, 0, 0, null);
            g2d.setColor(borderColor);
            g2d.fill(border);
            g2d.dispose();
            return targetImg;
        }
    
        public class TestPane extends JPanel {
    
            public TestPane() throws IOException {
                BufferedImage master = ImageIO.read(getClass().getResource("/images/MegaTokyo.png"));
                BufferedImage bordered = applyBorder(master, 10, Color.RED);
                setLayout(new GridLayout(0, 2));
    
                add(new JLabel(new ImageIcon(master)));
                add(new JLabel(new ImageIcon(bordered)));
            }
        }
    }
    

    You're probably scratching your head trying to figure out why this would be of benefit.

    Consider for a second, what if you wanted the border to have a rounded inner edge!

    Well, that's pretty easy to achieve, simply modify the applyBorder method to subtract a different shape, for example...

    border.subtract(new Area(new RoundRectangle2D.Double(
            borderThickness, 
            borderThickness, 
            img.getWidth() - (borderThickness * 2), 
            img.getHeight() - (borderThickness * 2),
            32,
            32
    )));
    

    which generates...

    enter image description here

    You could also make use of the existing Border API if you really wanted to.

    The "problem"...

    The reason you're getting a java.lang.ArrayIndexOutOfBoundsException is because the code which, I think, for some reason I don't understand, fills the image with the pixels of the image...I'm assuming you're trying to "insert" the image into the border, rather then having the border drawn on top of the image???

    Anyway, your problem is here...

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                targetImg.setRGB(x + borderThickness, y + borderThickness, img.getRGB(x, y));
            }
        }
    

    When x is width - 1, you're then adding borderThickness to it, which attempts to place a pixel outside of the image range. Same thing goes for y position.

    Assuming you want to add the border around the image, as apposed to adding the border over the image, you need to modify your code to create a new image which is large enough to contain the original image and the border, for example...

    enter image description here

    public static BufferedImage applySlowBorder(BufferedImage img, int borderThickness, Color borderColor) {
        int width = img.getWidth();
        int height = img.getHeight();
        int borderRgb = borderColor.getRGB();//(borderColor[0] << 16) | (borderColor[1] << 8) | borderColor[2];
        
        BufferedImage targetImg = new BufferedImage(
                img.getWidth() + (borderThickness * 2), 
                img.getHeight() + (borderThickness * 2), 
                img.getType()
        );
    
        for (int x = 0; x < targetImg.getWidth(); x++) {
            for (int y = 0; y < borderThickness; y++) {
                targetImg.setRGB(x, y, borderRgb); // Top border
                targetImg.setRGB(x, targetImg.getHeight() - 1 - y, borderRgb); // Bottom border
            }
        }
    
        for (int y = borderThickness; y < targetImg.getHeight() - borderThickness; y++) {
            for (int x = 0; x < borderThickness; x++) {
                targetImg.setRGB(x, y, borderRgb); // Left border
                targetImg.setRGB(targetImg.getWidth() - 1 - x, y, borderRgb); // Right border
            }
        }
    
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                targetImg.setRGB(x + borderThickness, y + borderThickness, img.getRGB(x, y));
            }
        }
        return targetImg;
    }
    

    If, however, you want to have the border painted over the image, then you need to paint the original image first, for example...

    enter image description here

    public static BufferedImage applySlowBorder(BufferedImage img, int borderThickness, Color borderColor) {
        int width = img.getWidth();
        int height = img.getHeight();
        int borderRgb = borderColor.getRGB();//(borderColor[0] << 16) | (borderColor[1] << 8) | borderColor[2];
        
        BufferedImage targetImg = new BufferedImage(
                img.getWidth(),
                img.getHeight(),
                img.getType()
        );
    
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                targetImg.setRGB(x, y, img.getRGB(x, y));
            }
        }
    
        for (int x = 0; x < targetImg.getWidth(); x++) {
            for (int y = 0; y < borderThickness; y++) {
                targetImg.setRGB(x, y, borderRgb); // Top border
                targetImg.setRGB(x, targetImg.getHeight() - 1 - y, borderRgb); // Bottom border
            }
        }
    
        for (int y = borderThickness; y < targetImg.getHeight() - borderThickness; y++) {
            for (int x = 0; x < borderThickness; x++) {
                targetImg.setRGB(x, y, borderRgb); // Left border
                targetImg.setRGB(targetImg.getWidth() - 1 - x, y, borderRgb); // Right border
            }
        }
        return targetImg;
    }
    

    fyi: Using my super accurate, scientific measurement tools (insert sarcasm 🤣), the Graphics2D approach is about 10 milliseconds faster then the get/setRGB approach - while not a massive difference, the size of the image will have an increasingly large impact on these results - for example, using a 1920x1080 source image, the Graphics2D approach took about 9 milliseconds, where as your code took about 203 milliseconds - again, these are "observational" differences, but get/setRGB is known for been slow