A product shot taken on a desk, phone tilted forward over the box: the vertical edges lean towards each other, the label on the front is a trapezoid, and the top face that is a rectangle in the real world comes out as a wedge. Nothing in the photograph is wrong. The camera was not parallel to the thing it was pointed at.
The same happens to a delivery note photographed instead of scanned, a screen shot off a colleague’s monitor, a sign taken from the pavement, a framed print on a wall. Long edges stay straight, but pairs of edges that are parallel in the world stop being parallel in the file, and the closer edge is always the longer one.
The obvious repairs fail. Rotate the picture and the trapezoid rotates with it. Crop it and you are cutting a rectangle out of a shape that is not one. Stretch it wider and the near edge grows as much as the far edge. WordPress core is honest about the limit: WP_Image_Editor documents resize(), multi_resize(), crop(), rotate() and flip(), and nothing that could do this job.
What it takes to fix perspective distortion in photo files is a different kind of transform and four points to pin it down. The tool below does that in this browser tab. Your file is read through an object URL inside the page: nothing is uploaded, nothing is fetched, no request of any kind is made. The customer address on that delivery note never leaves your machine.
Perspective and keystone corrector
Drag the four corners onto a rectangle that was photographed at an angle and get it back flat, square and in its true proportions. The homography is solved here, the pixels are resampled here, and the picture never leaves this browser tab.
Sample photograph, drawn in this browser.
That file is not a picture this browser can open.
The corners go clockwise from the top left. Pick one and the arrow keys nudge it a pixel at a time, with shift ten. While you drag, the loupe shows the pixels under the pointer, because a corner you cannot see is a corner you cannot hit.
The automatic estimate is geometry, not a guess: the two vanishing points of the opposite edge pairs are the images of two directions that are at right angles on the real object, and that one fact pins down the focal length and with it the true ratio. It needs perspective to work on. Once a vanishing point sits further than twenty image diagonals from the centre, one pixel of slip at a corner is enough to throw the answer right off, so the tool refuses it and says so instead.
Source pixels in, output pixels out, normalised so that h₃₃ is one. The resampler never uses it in this direction: for every destination pixel it applies the inverse, built from the adjugate of the same three by three, divides through by w and reads the source between its pixels.
That is the same mapping as a CSS transform. Put it on the original photograph with transform-origin: 0 0 and the browser straightens it for you, on the compositor, with no canvas at all.
Rotate, crop and scale are the wrong family
Every transform in a normal image editor belongs to the affine family: translate, rotate, scale, shear and combinations of them. An affine map of a plane has six free numbers and one property that matters here above all: parallel lines stay parallel. Two edges that converge in the source converge in the result, whatever you do with crop, rotate and scale.
A camera pointed at a flat object from an angle performs a projective map instead, and that has eight free numbers rather than six. It still keeps straight lines straight, which is why the edges of your box are clean in the photo, but it is allowed to let two parallel lines meet. The point where they meet is the vanishing point, and it is what the two extra numbers carry.
The name for that map is a homography: a 3×3 matrix acting on homogeneous coordinates. Nine entries, but the matrix is defined only up to a common scale factor, so eight are free. Eight unknowns need eight equations, and four point pairs give exactly eight. That is why the interface asks for four corners and not three or five.
Four corners, eight equations
You drag four numbered handles onto the corners of the rectangle as it appears in the photograph. The tool knows where they should end up: the corners of a clean rectangle in the target ratio. Each source and destination pair gives two linear equations, one from x and one from y, once the projective division is cleared away. Fixing the bottom right entry at 1 removes the free scale factor and leaves eight unknowns.
source (x, y) -> destination (u, v)
x*h11 + y*h12 + h13 - u*x*h31 - u*y*h32 = u
x*h21 + y*h22 + h23 - v*x*h31 - v*y*h32 = v
four point pairs -> 8 rows, 8 unknowns, h33 fixed at 1
That system is solved by Gauss elimination with partial pivoting, and the pivoting is not decoration. Without the row swap the solver divides by an almost zero pivot as soon as two corners sit on one horizontal, the ordinary case of a poster on a level shelf photographed with only a forward tilt. The unpivoted version does not warn you. It returns enormous numbers and an output that flies off the canvas.
The sampling runs backwards
This is the most useful idea in the subject. The naive way to apply the matrix is to walk the source pixels, push each through the homography and paint it where it lands. That is forward mapping, and it is broken: the landing points are fractional and unevenly spaced, so parts of the destination receive several source pixels stacked on each other and other parts receive none. The empty parts are holes, and now you need a second algorithm to fill them.
The correct way runs in the other direction. Walk the destination pixels instead, and apply the inverse matrix to ask where in the source each one came from. Every output pixel is visited once and written once, so no holes and no collisions, and the interpolation happens in the source where the grid is still complete and evenly spaced.
None of that is exclusive to this tool. ImageMagick describes its own distortion operators in the same terms, mapping “the coordinate of each pixel in the destination image to the corresponding location in the source image” and reading the colour there. Its -distort Perspective takes four control point pairs, and GIMP’s Perspective tool works from four corner handles too, though GIMP’s manual notes it “is not actually a perspective tool, as it doesn’t impose perspective rules”. Comparable tools exist. What differs is where the pixels are processed and what the tool tells you.
The inverse matrix comes from the adjugate of the same 3×3, with no need to divide by the determinant: the result is used in homogeneous coordinates and the common factor cancels in the division that follows. That division is the projective part. The matrix gives three numbers, and the source coordinates are the first two divided by the third. When the third is constant you are back to an affine map.
All three numbers are affine along a destination scanline, so each changes by a fixed amount from one pixel to the next: the inner loop adds three constants and does one division instead of recomputing a matrix product, then samples by nearest neighbour, bilinear or Catmull-Rom bicubic. Anything landing outside the source is written fully transparent rather than clamped to the nearest edge pixel, because clamping produces the smeared border streaks that give a bad warp away instantly.
The true aspect ratio is hiding in the vanishing points
Here is the question that separates a straightening tool from a guessing one. Four corners tell you the shape of the quadrilateral. They do not tell you whether the original was A4, a square or a 16:9 screen, and if you pick wrong the output is flat, square and still the wrong proportions. Most tools ask you to choose. The geometry can often answer instead.
Extend the two edges that were parallel in the world and they meet at a vanishing point; the other pair gives a second. A vanishing point is the image of a direction rather than of a place, so those two points are the images of two directions that meet at a right angle on the real object. Assume a pinhole camera with square pixels and the principal point at the centre of the frame, back-project both vanishing points, and demand that the two direction vectors have a dot product of zero. That is one equation in one unknown, and the unknown is the square of the focal length.
vanishing points V1 = (u1, v1) V2 = (u2, v2)
principal point (cx, cy) = centre of the frame
f^2 = -( (u1 - cx)*(u2 - cx) + (v1 - cy)*(v2 - cy) )
K = [ f 0 cx ; 0 f cy ; 0 0 1 ]
columns 1 and 2 of inv(K) * H -> the two real side lengths
ratio = |column 1| / |column 2|
With the focal length known the camera matrix is known, and the homography that maps the unit square onto your four corners can be pulled back through it. Its first two columns are then the two side directions scaled by the two real side lengths, so the ratio of their lengths is the aspect ratio of the original. No guessing about paper sizes.
The sample image built into the tool exists to prove the arithmetic closes. It is a 600 by 900 poster projected through a camera whose parameters are known because they were chosen: 22 degrees of pitch, 18 of yaw, focal length 1100 pixels. The estimate hands back exactly those 1100 pixels and a ratio of 1.500. That is the test to ask of any estimator.
When the estimate degenerates, and how you find out
The method has a failure mode, and it is the common case rather than an exotic one. On a nearly frontal shot the converging edges barely converge, the vanishing points run off towards infinity, and the expression for the square of the focal length goes negative. An imaginary focal length is the arithmetic saying the picture does not contain enough perspective to measure perspective with.
Three conditions are tested and named: an imaginary focal length, vanishing points further out than twenty image diagonals, and a quadrilateral that is not a simple one. When one fires, the tool says which, visibly falls back on the fallback ratio you selected (edge lengths by default, or 1:1, 3:2, 4:3, or A series portrait), and marks the result as a fallback in the meta line and in data-fell. A tool that substitutes a guess for a measurement and presents both the same way is worse than one that refuses. The twenty diagonals were measured, not picked: at three degrees of tilt a one pixel slip at a corner still leaves roughly 0.2 percent of error in the ratio, and at two degrees the same arithmetic tips into an imaginary focal length.
Two more limits. Dragging one corner past another turns the quadrilateral into a bow tie, and a self-intersecting quadrilateral is refused outright, with the previous good result left on screen. And the estimate assumes square pixels with the principal point at the centre, so a heavily cropped picture or one shot with a shift lens breaks that assumption invisibly. For those, set the ratio by hand: 1:1, 3:2, 2:3, 4:3, 3:4, A series landscape or portrait, or typed in as 16:9, 16/9 or a plain number.
What it deliberately does not do
A homography maps straight lines to straight lines, and that sets the hard boundary of what four corners can repair. Lens distortion breaks the assumption before you start: the OpenCV calibration documentation puts it in one line, “radial distortion causes straight lines to appear curved”, and models it with the coefficients k1, k2, p1, p2 and k3. Shoot the sign on a wide phone lens and the edges are already bowed. Pin the four corners and they land perfectly while the middle of each edge still bulges. Undistort with a lens profile first, then bring that file here.
The file also has to be one the browser can decode, because the decoding is the browser’s. The MDN guide to image file types for the web lists APNG, AVIF, GIF, JPEG, PNG, SVG and WebP as supported, with BMP, ICO and TIFF as ones to avoid. HEIC is not on that list, which is why a photo copied straight off an iPhone may refuse to open here, and why HEIC uploads hit the same wall in WordPress. Convert to JPEG or PNG first.
Three smaller omissions. Alpha is interpolated straight rather than premultiplied, so a PNG with hard transparent regions can fringe by about a pixel there, which never shows on a photograph. Two copies of the tool on one page are not supported, the documented limit for the whole plugin. And it warps pixels and nothing else: no exposure correction, no removing the shadow your hand cast across the page, no text sharpening, no reading the text at all. There is no fetch, no XHR, no innerHTML, no external script or font and no Math.random, so the whole thing behaves identically offline.
Stretched corners lose sharpness
Straightening evens out the geometry. It cannot even out the information. The far end of the object occupied fewer source pixels than the near end, and afterwards both occupy the same number of output pixels, so the far end has been enlarged and the near end reduced in one pass. Text along the far edge comes back soft, and that softness is real: the detail was never recorded. Catmull-Rom bicubic holds edge contrast better than bilinear, but invents nothing and can overshoot into a light halo. Nearest neighbour is for cases where any blending is wrong.
So shoot as square-on as the shelf allows and fill the frame, so the far edge still gets pixels. Then set the output width slider, 240 to 1600 pixels with a live readout, close to the width you will publish at, because scaling up afterwards is the worst of both worlds and the size question is better settled before export. What the resampler and the export setting actually cost is measurable rather than a matter of taste.
The source is worked on at no more than 1400 pixels on the long side, and while a corner is under the pointer the output drops to 420 pixels wide and four taps so the preview keeps up, with the full pass running again on release. Arrow keys nudge a corner one pixel and shift ten, a loupe follows the handle, and corner snap and the perspective grid switch off when they get in the way.
The matrix, and getting the result into a page
Three things come out: the straightened PNG, the 3×3 in numbers, and the same mapping as a CSS matrix3d. The last is for a warp you want live on an element rather than baked into a file, a mockup sitting at an angle in a hero section for instance. MDN states that the sixteen values of matrix3d() “are described in the column-major order”, which is where most hand written attempts go wrong.
H = [ a b c ; d e f ; g h 1 ]
transform: matrix3d(
a, d, 0, g,
b, e, 0, h,
0, 0, 1, 0,
c, f, 0, 1
);
Note the direction. That CSS transform is the forward map, source to destination, applied to a live element by the compositor, while the resampler uses its inverse. Confusing the two is the classic reason a CSS experiment and an exported file disagree.
Once the PNG is straight, everything downstream gets simpler. A catalogue in which every box front is a true rectangle crops predictably and sits on a grid without visual wobble, half the work described in making product photos that do not look fake. From there it is an ordinary upload, and the built in image editor handles the crop and scale that follow. To do the crop, the scale and the replacement without leaving the media library, that is what WunderPaint is for.
Four points, eight numbers
This feels unfixable in a normal editor because the editor offers only the affine family while the damage was done by a projective map. Once you see that the missing piece is two extra numbers, and that four corners supply exactly the eight equations that pin them down, the rest is bookkeeping: solve carefully enough that a level shelf does not blow up the pivot, and sample backwards so every output pixel is written once.
The aspect ratio is the part worth being sceptical about in any tool. Recovering it from two vanishing points is real geometry with a real answer, and on a picture with enough tilt it returns the true proportions of an object you never measured. On a nearly frontal shot it collapses, and the only thing separating a trustworthy tool from an untrustworthy one is whether it says so. When you see the fallback marker, either reshoot with more angle or set the ratio yourself, because you know what the object was and the arithmetic does not.
Keep two hard walls in mind before blaming the tool. A bowed edge is lens distortion and needs undistorting first, since no homography straightens a curve. A soft far edge is missing information and no resampler puts it back. Everything between those walls is four handles and a matrix, computed on your own machine, on a file that never went anywhere.