scaling/resizing an image to have power of two width and height

In many implementations of OpenGL, a two-dimensional texture must be a power of two in both width and height. The obvious way to rescale the image is to use gluScaleImage() appropriately, but in the current version of JOGL (JSR-231 1.1.1) there appears to be a bug that causes a crash.

So here is a code snippet that uses AWT’s AffineTransform and Graphics2D classes to rescale the image. It’s not particularly challenging, but I thought I could save some others a few minutes of coding. Warning: this code is only lightly tested. In particular, I haven’t loaded any images that preserve alpha, nor tried loading a variety of formats. Let me know if you find problems so we can post improved versions.

`
static class Texture2D {

    public static Texture2D create(String resourceName) {
        return Texture2D.create(resourceName, false, false);
    }

    public static Texture2D create(String resourceName, boolean powerOfTwo) {
        return Texture2D.create(resourceName, powerOfTwo, false);
    }

    /*
     * Create a texture object from an image file, suitable for use for
     * calls to glBindTexture and related functions in OpenGL. If powerOfTwo
     * is true, the width and height of the underlying pixel buffer will be
     * adusted to be a power of two. If perserveAlpha is true, the pixel
     * buffer will include the alpha channel from the original image.
     */
    public static Texture2D create(String resourceName,
            boolean powerOfTwo,
            boolean preserveAlpha) {
        Texture2D texture = null;
        try {
            InputStream resource = ResourceRetriever.getResourceAsStream(resourceName);
            BufferedImage bufferedImage = ImageIO.read(resource);
            if (powerOfTwo) {
                bufferedImage = resampleToPowerOfTwo(bufferedImage);
            }
            ByteBuffer byteBuffer = unpackImage(bufferedImage,
                    preserveAlpha);
            texture = new Texture2D(byteBuffer,
                    bufferedImage.getWidth(),
                    bufferedImage.getHeight());
        } catch (IOException e) {}
        return texture;
    }

    /*
     * Create a scaled image which has width and height that are powers 
     * of two, equal to or smaller than the width and height of srcImage. If
     * srcImage already has width and height that are exact powers of two,
     * it is returned unchanged.
     */
    private static BufferedImage resampleToPowerOfTwo(BufferedImage srcImage) {
        BufferedImage po2Image = srcImage;
        int w = srcImage.getWidth();
        int h = srcImage.getHeight();
        int w2 = powerOfTwoFloor(w);
        int h2 = powerOfTwoFloor(h);
        if ((w != w2) || (h != h2)) {
            // Need to scale srcImage down to nearest power of two.  Use an 
            // AWT affine transform and Graphics2D rendering for the job.
            double scaleW = (double) w2 / (double) w;
            double scaleH = (double) h2 / (double) h;
            po2Image = new BufferedImage(w2, h2, BufferedImage.TYPE_INT_RGB);
            Graphics2D g2 = po2Image.createGraphics();
            AffineTransform at = AffineTransform.getScaleInstance(scaleW, scaleH);
            g2.drawRenderedImage(srcImage, at);
            g2.dispose();
        }
        return po2Image;
    }

    /*
     * Return the largest power of two that is less than or equal to i.
     */
    private static int powerOfTwoFloor(int i) {
        double log2i = Math.log(i) / Math.log(2.0);
        return 1 << (int) (Math.floor(log2i));
    }

    /*
     * Unpack an INT_RGB or RGBA BufferedImage into a GL-compatible
     * ByteBuffer. If preserveAlpha is true, then the alpha channel is
     * extracted, otherwise it is omitted.
     */
    private static ByteBuffer unpackImage(BufferedImage image,
            boolean preserveAlpha) {
        int[] packedPixels = new int[image.getWidth() * image.getHeight()];

        PixelGrabber pixelgrabber = new PixelGrabber(image,
                0,
                0,
                image.getWidth(),
                image.getHeight(),
                packedPixels,
                0,
                image.getWidth());
        try {
            pixelgrabber.grabPixels();
        } catch (InterruptedException e) {
            throw new RuntimeException();
        }

        int bytesPerPixel = preserveAlpha ? 4 : 3;
        ByteBuffer unpackedPixels = BufferUtil.newByteBuffer(packedPixels.length
            * bytesPerPixel);

        for (int row = image.getHeight() - 1; row >= 0; row--) {
            for (int col = 0; col < image.getWidth(); col++) {
                int packedPixel = packedPixels[row * image.getWidth() + col];
                unpackedPixels.put((byte) ((packedPixel >> 16) & 0xFF));
                unpackedPixels.put((byte) ((packedPixel >> 8) & 0xFF));
                unpackedPixels.put((byte) ((packedPixel >> 0) & 0xFF));
                if (preserveAlpha) {
                    unpackedPixels.put((byte) ((packedPixel >> 24) & 0xFF));
                }
            }
        }
        unpackedPixels.flip();
        return unpackedPixels;
    }

    private ByteBuffer _pixels;

    private int _width;

    private int _height;

    public Texture2D(ByteBuffer pixels, int width, int height) {
        super();
        _pixels = pixels;
        _width = width;
        _height = height;
    }

    public Buffer getPixels() {
        return _pixels;
    }

    public int getHeight() {
        return _height;
    }

    public int getWidth() {
        return _width;
    }

}

`

If Java 5 is an option then you could save some code and use the Integer.highestOneBit() API instead of your powerOfTwoFloor().

mabraham:

Good call! I’m easing into Java 5 - thanks for the suggestion!

  • fearless