How to Flip an Image
Flipping an image — horizontal (left–right mirror) or vertical (top–bottom) — is one of the most common image operations. It sounds trivial, but EXIF orientation tags, alpha channel handling, and library-specific direction conventions introduce real gotchas. This guide covers both flip axes with working implementations in JavaScript, Python, Go, and PHP.
Horizontal vs Vertical Flip
| Flip | Also called | Effect |
|---|---|---|
| Horizontal | Mirror, flip-X | Left side becomes right side |
| Vertical | Flip-Y, upside-down | Top becomes bottom |
Original Horizontal flip Vertical flip
┌───────┐ ┌───────┐ ┌───────┐
│ A B │ → │ B A │ → │ C D │
│ C D │ │ D C │ │ A B │
└───────┘ └───────┘ └───────┘
Output dimensions never change — flipping is a pixel rearrangement, not a resize.
EXIF Orientation — Apply Before Flipping
Smartphone photos often carry an EXIF orientation tag. If you flip a JPEG without first correcting its EXIF orientation, the result will be wrong (the image appears to flip on the wrong axis).
Raw JPEG bytes: landscape (3024 × 4032)
EXIF tag 6 : "rotate 90° CW for display"
You flip : horizontally
Result : appears flipped vertically in viewers that auto-apply EXIF
Fix: strip or apply EXIF orientation before flipping — use Sharp's .rotate() (no argument), Pillow's ImageOps.exif_transpose(), or Imagick's autoOrient().
JavaScript
Browser: Canvas API
/**
* Flip an image element horizontally or vertically.
* @param {HTMLImageElement} img
* @param {'horizontal' | 'vertical'} direction
* @returns {HTMLCanvasElement}
*/
function flipImage(img, direction = 'horizontal') {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
if (direction === 'horizontal') {
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
} else {
ctx.translate(0, canvas.height);
ctx.scale(1, -1);
}
ctx.drawImage(img, 0, 0);
return canvas;
}
// Usage
const img = document.getElementById('myImage');
const flipped = flipImage(img, 'horizontal');
document.body.appendChild(flipped);
// Download the result
flipped.toBlob(blob => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'flipped.png';
a.click();
}, 'image/png');
Node.js: Sharp
import sharp from 'sharp';
// Horizontal flip (mirror)
await sharp('input.jpg')
.rotate() // auto-apply EXIF orientation first
.flop() // horizontal flip (left–right)
.toFile('flipped-h.jpg');
// Vertical flip (upside-down)
await sharp('input.jpg')
.rotate()
.flip() // vertical flip (top–bottom)
.toFile('flipped-v.jpg');
// Both axes at once (same as 180° rotation)
await sharp('input.jpg')
.rotate()
.flip()
.flop()
.toFile('flipped-both.jpg');
Sharp naming:
.flip()= vertical (Y axis),.flop()= horizontal (X axis). Counterintuitive — remember: "flop" sounds like flipping sideways.
Python
Pillow
from PIL import Image, ImageOps
def flip_image(input_path: str, output_path: str, direction: str = 'horizontal') -> None:
"""
Flip an image horizontally or vertically.
Applies EXIF orientation before flipping.
"""
with Image.open(input_path) as img:
# Apply EXIF orientation first (critical for smartphone photos)
img = ImageOps.exif_transpose(img)
if direction == 'horizontal':
flipped = ImageOps.mirror(img) # left–right
elif direction == 'vertical':
flipped = ImageOps.flip(img) # top–bottom
else:
raise ValueError("direction must be 'horizontal' or 'vertical'")
flipped.save(output_path)
# Examples
flip_image('photo.jpg', 'mirrored.jpg', 'horizontal')
flip_image('photo.jpg', 'upside-down.jpg', 'vertical')
Pillow naming:
ImageOps.mirror()→ horizontal flip (left–right mirror)ImageOps.flip()→ vertical flip (upside-down)
# Alternative using the transpose method directly
from PIL import Image
with Image.open('photo.jpg') as img:
# TRANSPOSE constants
h_flip = img.transpose(Image.FLIP_LEFT_RIGHT)
v_flip = img.transpose(Image.FLIP_TOP_BOTTOM)
h_flip.save('h-flipped.jpg')
v_flip.save('v-flipped.jpg')
Go
Go's standard library has no built-in flip, but it is straightforward to implement:
package main
import (
"image"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"
)
// FlipHorizontal mirrors an image left-to-right.
func FlipHorizontal(src image.Image) *image.NRGBA {
bounds := src.Bounds()
dst := image.NewNRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
// Source pixel comes from the mirrored X position
srcX := bounds.Max.X - 1 - (x - bounds.Min.X) + bounds.Min.X
dst.Set(x, y, src.At(srcX, y))
}
}
return dst
}
// FlipVertical mirrors an image top-to-bottom.
func FlipVertical(src image.Image) *image.NRGBA {
bounds := src.Bounds()
dst := image.NewNRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
srcY := bounds.Max.Y - 1 - (y - bounds.Min.Y) + bounds.Min.Y
for x := bounds.Min.X; x < bounds.Max.X; x++ {
dst.Set(x, y, src.At(x, srcY))
}
}
return dst
}
func main() {
f, _ := os.Open("photo.jpg")
defer f.Close()
src, _, _ := image.Decode(f)
flipped := FlipHorizontal(src)
out, _ := os.Create("flipped.jpg")
defer out.Close()
jpeg.Encode(out, flipped, &jpeg.Options{Quality: 90})
}
For production use, consider the
disintegration/imaginglibrary which providesimaging.FlipH()andimaging.FlipV().
// With imaging library
import "github.com/disintegration/imaging"
src, _ := imaging.Open("photo.jpg", imaging.AutoOrientation(true))
flippedH := imaging.FlipH(src) // horizontal
flippedV := imaging.FlipV(src) // vertical
imaging.Save(flippedH, "flipped.jpg")
PHP
GD
<?php
/**
* Flip an image file horizontally or vertically using GD.
* Preserves PNG transparency.
*/
function flipImage(
string $inputPath,
string $outputPath,
string $direction = 'horizontal'
): void {
$info = getimagesize($inputPath);
$mime = $info['mime'];
// Load source image
$src = match ($mime) {
'image/jpeg' => imagecreatefromjpeg($inputPath),
'image/png' => imagecreatefrompng($inputPath),
'image/webp' => imagecreatefromwebp($inputPath),
default => throw new InvalidArgumentException("Unsupported: $mime"),
};
// Preserve PNG alpha
imagesavealpha($src, true);
// GD 5.5+ has imageflip() built in
$mode = $direction === 'vertical' ? IMG_FLIP_VERTICAL : IMG_FLIP_HORIZONTAL;
imageflip($src, $mode);
// Save result
match ($mime) {
'image/jpeg' => imagejpeg($src, $outputPath, 90),
'image/png' => imagepng($src, $outputPath),
'image/webp' => imagewebp($src, $outputPath, 90),
};
imagedestroy($src);
}
flipImage('photo.jpg', 'mirrored.jpg', 'horizontal');
flipImage('photo.png', 'upside-down.png', 'vertical');
Imagick
<?php
$imagick = new Imagick('photo.jpg');
$imagick->autoOrient(); // apply EXIF orientation first
// Horizontal flip
$imagick->flopImage(); // left–right mirror
// Or vertical flip
// $imagick->flipImage(); // top–bottom
$imagick->writeImage('flipped.jpg');
$imagick->clear();
Imagick naming matches Pillow:
flipImage()= vertical,flopImage()= horizontal.
Quick Reference
| Library | Horizontal flip | Vertical flip | Auto EXIF |
|---|---|---|---|
| Canvas API | scale(-1, 1) |
scale(1, -1) |
No — manual |
| Sharp (Node) | .flop() |
.flip() |
.rotate() |
| Pillow | ImageOps.mirror() |
ImageOps.flip() |
exif_transpose() |
| Go imaging | imaging.FlipH() |
imaging.FlipV() |
AutoOrientation(true) |
| PHP GD | imageflip(IMG_FLIP_HORIZONTAL) |
imageflip(IMG_FLIP_VERTICAL) |
No |
| PHP Imagick | flopImage() |
flipImage() |
autoOrient() |
6 Common Mistakes
Skipping EXIF correction on JPEGs — the most frequent bug. Always call
exif_transpose()/.rotate()/autoOrient()before flipping smartphone photos.Confusing Sharp's
.flip()and.flop()— Sharp's.flip()is vertical (top–bottom),.flop()is horizontal (left–right). The opposite of what many developers expect.Forgetting to preserve PNG alpha in PHP GD — call
imagesavealpha($src, true)beforeimageflip(), otherwise transparent areas turn black.Re-encoding a JPEG unnecessarily — each save-as-JPEG introduces generation loss. If the source is JPEG and you only need to flip, consider lossless JPEG flip tools (
jpegtran -flip horizontal) that manipulate DCT coefficients directly without full decode/encode.Applying both flips when you wanted 180° rotation — flipping both axes produces the same pixel result as a 180° rotation, but uses two passes instead of one. Use
.rotate(180)orimg.transpose(Image.ROTATE_180)instead.Not testing with non-square images — flip logic is correct for any aspect ratio, but a common manual pixel-loop bug is using
widthwhereheightshould be used in the vertical case. Always test with a rectangular image.
Frequently Asked Questions
Does flipping change the image file size? Slightly. The pixel content changes, which affects compression ratios. A JPEG of a mirrored photo will be very close to the original size, but not byte-identical.
Is horizontal flip the same as a 180° rotation? No. A horizontal flip mirrors left-to-right; a 180° rotation also flips upside-down. Applying both a horizontal and a vertical flip equals a 180° rotation.
How do I create a CSS mirror effect without processing the image?
Use transform: scaleX(-1) for horizontal or scaleY(-1) for vertical. This is GPU-accelerated and requires no server-side processing.
.mirror { transform: scaleX(-1); }
.upside-down { transform: scaleY(-1); }
Can I flip a GIF without losing animation?
Not with the techniques above — they work on a single frame. For animated GIFs, use FFmpeg: ffmpeg -i in.gif -vf hflip out.gif.
Why does my flipped selfie look wrong? Front-facing cameras mirror the preview so you see yourself as in a mirror. The saved photo is already corrected (un-mirrored). If you flip the saved photo, it becomes a mirror image again, which can look unnatural. This is expected behaviour.
Does lossless JPEG flip exist?
Yes. jpegtran -flip horizontal input.jpg -outfile output.jpg manipulates DCT blocks directly without decoding, so there is zero quality loss. Only supported for images whose dimensions are multiples of the DCT block size (usually 8 or 16 pixels).