Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

To mirror a screenshot read with glReadPixels, reverse the pixel columns within each row: destination column x gets source column width - 1 - x. For an int[] containing tightly packed pixels, that means copying each row from right to left. If the screenshot is upside down rather than mirrored, reverse the rows instead; OpenGL’s lower-left readback origin is a separate orientation issue.

Flip columns, not rows

A horizontal flip mirrors an image around its vertical centerline. It changes each pixel’s x-coordinate but leaves its y-coordinate unchanged:

destination(x, y) = source(width - 1 - x, y)

For a row-major array with exactly width * height packed pixels, the linear source index for (x, y) is y * width + x. The flipped destination therefore reads from y * width + (width - 1 - x).

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
static int[] flipHorizontally(int[] src, int width, int height) {
    if (src == null) throw new IllegalArgumentException("src must not be null");
    if (width <= 0 || height <= 0) {
        throw new IllegalArgumentException("width and height must be positive");
    }
    if (src.length != width * height) {
        throw new IllegalArgumentException("src must contain width * height pixels");
    }

    int[] dst = new int[src.length];
    for (int y = 0; y < height; y++) {
        int row = y * width;
        for (int x = 0; x < width; x++) {
            dst[row + x] = src[row + (width - 1 - x)];
        }
    }
    return dst;
}

The length check assumes the product fits in an int, as it ordinarily must for a Java array. For very large or untrusted dimensions, validate the product with long arithmetic before allocating: (long) width * height.

In-place alternative

If you can modify the array rather than preserve the original, swap opposite columns in each row. Stop halfway across each row so that every pair is swapped exactly once.

static void flipHorizontallyInPlace(int[] pixels, int width, int height) {
    if (pixels == null) throw new IllegalArgumentException("pixels must not be null");
    if (width <= 0 || height <= 0 || pixels.length != width * height) {
        throw new IllegalArgumentException("dimensions must match the packed pixel array");
    }

    for (int y = 0; y < height; y++) {
        int row = y * width;
        for (int x = 0; x < width / 2; x++) {
            int left = row + x;
            int right = row + width - 1 - x;
            int tmp = pixels[left];
            pixels[left] = pixels[right];
            pixels[right] = tmp;
        }
    }
}

For an odd width, the center pixel has no partner and remains where it is. For a width of one, there is nothing to swap.

Apply the flip to a glReadPixels result

The indexing operation is independent of the OpenGL binding, but the meaning of each array element is not. LWJGL exposes glReadPixels overloads for Java buffers and primitive arrays; choose one consistent with the format and type requested by your call. Do not assume every result is one Java integer per pixel.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For example, if your call and subsequent conversion produce one packed Java int per pixel in row-major order, pass those pixels to flipHorizontally. If your readback is instead RGBA unsigned bytes, each pixel occupies four bytes, so reverse four-byte pixel groups rather than reversing individual bytes.

static void flipRgbaBytesInPlace(byte[] pixels, int width, int height, int rowStride) {
    int bytesPerPixel = 4;
    if (pixels == null) throw new IllegalArgumentException("pixels must not be null");
    if (width <= 0 || height <= 0 || rowStride < width * bytesPerPixel) {
        throw new IllegalArgumentException("invalid dimensions or row stride");
    }
    if ((long) rowStride * height > pixels.length) {
        throw new IllegalArgumentException("buffer is too small for dimensions and stride");
    }

    byte[] temp = new byte[bytesPerPixel];
    for (int y = 0; y < height; y++) {
        int rowStart = y * rowStride;
        for (int x = 0; x < width / 2; x++) {
            int left = rowStart + x * bytesPerPixel;
            int right = rowStart + (width - 1 - x) * bytesPerPixel;
            for (int b = 0; b < bytesPerPixel; b++) {
                temp[b] = pixels[left + b];
                pixels[left + b] = pixels[right + b];
                pixels[right + b] = temp[b];
            }
        }
    }
}

This byte-array example assumes four bytes per pixel and a known stride in bytes. Its row starts are calculated from rowStride, not width * 4, so it can accommodate padding at the end of rows. For another format, change the bytes-per-pixel assumption and preserve each complete pixel’s bytes together. If you work directly with a buffer, also account for its current position and limit; indexing from zero is correct only when that is where the image data begins.

Writing into a BufferedImage

If you already have a BufferedImage whose pixels are interpreted correctly, you can write each source pixel into the opposite destination column:

static BufferedImage flippedImage(BufferedImage source) {
    int width = source.getWidth();
    int height = source.getHeight();
    BufferedImage destination = new BufferedImage(
        width, height, BufferedImage.TYPE_INT_ARGB
    );

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            destination.setRGB(width - 1 - x, y, source.getRGB(x, y));
        }
    }
    return destination;
}

This is a clear way to express the coordinate mapping, but calling getRGB and setRGB for every pixel may be less efficient than copying through bulk pixel storage. It also operates on the image’s logical colors; it does not correct a mistaken channel-order or alpha interpretation made when creating the image from the OpenGL bytes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why a glReadPixels screenshot may be upside down

Khronos documents glReadPixels as reading from the selected read framebuffer beginning at the pixel whose lower-left corner is at the requested (x, y). That lower-left origin explains why data can appear vertically inverted when treated as a top-left-origin image. It does not, by itself, mean the image needs a horizontal mirror.

Keep the two transformations distinct:

  • Upside down: reverse row order. Destination row y reads source row height - 1 - y.
  • Mirrored left-to-right: reverse columns within each row, using width - 1 - x.
  • Both problems: apply both mappings; the operations are independent.

Some screenshot helpers handle the vertical conversion for you. JOGL’s documented screenshot utility says it flips scanlines vertically when converting OpenGL contents to a BufferedImage. Avoid reversing rows a second time if your chosen helper already did so. That documentation is useful for understanding the behavior, but it is from an archived JSR-231 API; check the API and version used by your project before treating it as a current JOGL recommendation.

Check dimensions, layout, and image interpretation

A correct mirror formula can still produce corrupted output if its assumptions do not match the readback buffer. Before changing the loop, check these details:

  • Framebuffer and region: confirm that the intended read framebuffer is selected and that the x, y, width, and height passed to glReadPixels describe the area you want.
  • Format and type: derive bytes per pixel and channel representation from the requested OpenGL format and type. A Java int destination alone does not say whether the logical image is RGBA, BGRA, or another arrangement.
  • Stride: use the actual number of bytes between row starts. Do not use y * width as a byte offset unless rows are tightly packed and the formula is in pixels.
  • Buffer position: start at the first pixel of the image in the buffer, not automatically at index zero. Respect the buffer’s current position and bounds.
  • Image coordinate convention: determine whether your destination image treats row zero as the top or bottom. Handle that vertical conversion once, at a clearly defined stage.
  • Alpha and channels: preserve alpha when copying and ensure your image-construction code interprets channel bytes in the order actually requested.

For a packed integer array with a row stride measured in pixels, the general destination-to-source calculation is srcRowStart + (width - 1 - x), where srcRowStart = y * rowStridePixels. For bytes, it is srcRowStart + (width - 1 - x) * bytesPerPixel. Keeping pixel coordinates separate from byte offsets makes accidental per-channel reversal less likely.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshoot common symptoms

Symptom Likely cause What to check or change
Image is mirrored, but colors look scrambled Individual bytes or channels were reversed instead of complete pixels, or channel interpretation is inconsistent. Swap whole pixel groups and verify the readback format, type, channel order, and alpha handling.
Image is upside down, not mirrored Rows are being interpreted with a top-left origin although readback begins at the lower-left. Reverse row order once. Check whether an image helper already flips scanlines.
Only some rows or columns look wrong Stride, dimensions, buffer bounds, or buffer position does not match the indexing formula. Verify row starts and ensure the buffer contains at least the required rows and pixel groups.
Part of the image is correct but edges are clipped The read region or destination dimensions do not match the intended framebuffer area. Check the selected framebuffer and the coordinates and dimensions passed to readback.
Mirror operation has no visible effect The image may be symmetric, only one pixel wide, or the flipped result may not be the image subsequently displayed or saved. Use an asymmetric image with distinct corner labels and inspect the actual output object.

An asymmetric test image with different labels or colors in each corner is a practical way to tell column reversal from row reversal. This is a debugging technique, not a claim that a particular image or implementation has been tested here.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and reliability considerations

A separate destination array requires one additional image-sized allocation and one read plus one write per pixel. In-place swapping avoids allocating a second full pixel array, at the cost of modifying the original readback data. If another stage still needs the unmodified pixels, use a separate destination or retain a copy.

For byte buffers, the work per swapped pair scales with bytes per pixel. Keep each pixel’s channels together, and avoid creating a temporary object for every pixel. If you also need vertical correction, decide whether to combine the two coordinate mappings in one destination-copy pass or to perform one operation in place; the right choice depends on whether preserving the original data matters.

The flip does not fix failed readback, a wrong framebuffer, an incorrect pixel format, or a buffer that is too small. Treat those as separate capture and conversion problems. Validate dimensions and storage before indexing so an error is diagnosed at the boundary rather than appearing as a malformed image later.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

If your goal is a clean browser screenshot rather than an OpenGL framebuffer readback, ScreenshotNeo provides a one-request screenshot API. It does not apply the Java horizontal-flip transformation above; use the Java method when you need to mirror pixel data from your own OpenGL capture. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, and cache hits are not billed, with response headers indicating the page verdict and billing status. Its MCP server provides screenshot tools for Claude, Cursor, and other MCP clients.

For example, the one-call cURL request saves a WebP screenshot; see the ScreenshotNeo documentation for API options:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The free plan includes 1,000 shots per month with no card required; paid plans start at $5 for 3,000 shots. Sign up for ScreenshotNeo’s free plan.

Frequently Asked Questions

Does flipping horizontally also fix an upside-down screenshot?

No. Horizontal flipping reverses columns; correcting upside-down output reverses rows. Apply only the transformation that matches the visible problem.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I flip a ByteBuffer or IntBuffer instead of copying to an array?

Yes. Use the same coordinate mapping, but account for the buffer position, limit, pixel representation, and row stride. Swap complete pixel groups for byte-oriented data.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.