How to how gps coordinates are encoded in exif data
- Step 1Find the GPS IFD via the pointer in IFD0 — EXIF lives in a TIFF structure inside the JPEG's APP1 segment. IFD0 contains tag
0x8825(GPSInfoIFDPointer), whose value is the byte offset (from the TIFF header) of the GPS sub-IFD. Libraries likepiexifjsexpose this as theGPSgroup. - Step 2Read the hemisphere reference tags first — Tag
0x0001(GPSLatitudeRef) is the ASCII"N"or"S"; tag0x0003(GPSLongitudeRef) is"E"or"W". These carry the sign — the coordinate numbers themselves are always non-negative. - Step 3Read the latitude RATIONAL triplet — Tag
0x0002(GPSLatitude) is three RATIONAL values:[degNum,degDen] [minNum,minDen] [secNum,secDen]. Each RATIONAL is two uint32 (numerator then denominator), so the triplet is 24 bytes. Divide each pair to get degrees, minutes, seconds. - Step 4Read the longitude RATIONAL triplet — Tag
0x0004(GPSLongitude) has the same three-RATIONAL structure as latitude. Together with the ref tags, tags 1–4 are everything the previewer needs. - Step 5Convert to decimal degrees — Compute
decimal = degrees + minutes/60 + seconds/3600. Then apply the ref: ifGPSLatitudeRefis"S"(orGPSLongitudeRefis"W"), negate. This is the exact formula the previewer runs. - Step 6Guard the failure paths — If the coordinate array has fewer than three pairs, or a denominator is zero, the result is
NaN. The previewer treats any non-finite latitude or longitude as 'no GPS' and shows the no-data notice rather than a wrong pin.
GPS sub-IFD tags relevant to coordinate decoding
The previewer reads tags 0x0001-0x0004. Altitude and timestamp tags are listed for completeness but are not decoded by this tool.
| Tag (hex) | Name | Type | Used by previewer? |
|---|---|---|---|
| 0x8825 | GPSInfoIFDPointer (in IFD0) | LONG (offset) | Yes — locates the GPS IFD |
| 0x0001 | GPSLatitudeRef | ASCII ("N"/"S") | Yes — sign for latitude |
| 0x0002 | GPSLatitude | RATIONAL x3 (D, M, S) | Yes — the latitude triplet |
| 0x0003 | GPSLongitudeRef | ASCII ("E"/"W") | Yes — sign for longitude |
| 0x0004 | GPSLongitude | RATIONAL x3 (D, M, S) | Yes — the longitude triplet |
| 0x0005 / 0x0006 | GPSAltitudeRef / GPSAltitude | BYTE / RATIONAL (metres) | No — not decoded |
| 0x0007 / 0x001D | GPSTimeStamp / GPSDateStamp | RATIONAL x3 / ASCII | No — not decoded |
Worked DMS-to-decimal conversions
Each row shows the stored rationals, the ref, and the decimal the previewer produces.
| Stored triplet + ref | D / M / S | Decimal degrees |
|---|---|---|
| [51,1] [30,1] [26640,1000] ref N | 51 / 30 / 26.64 | 51.50740 |
| [33,1] [52,1] [4080,1000] ref S | 33 / 52 / 4.08 | -33.86780 |
| [2,1] [17,1] [24120,1000] ref E | 2 / 17 / 24.12 | 2.29003 |
| [0,1] [7,1] [40080,1000] ref W | 0 / 7 / 40.08 | -0.12780 |
| [40,1] [44520,1000] ref N (2 pairs) | incomplete | NaN -> no GPS |
Cookbook
Byte-level walk-throughs of the encoding and the conversion. The final snippet is functionally the previewer's own decode.
A full latitude/longitude block decoded
How a Paris coordinate sits in the GPS IFD and what it converts to. The numbers are always positive; the refs carry the sign.
GPS IFD:
0x0001 GPSLatitudeRef = "N"
0x0002 GPSLatitude = [48,1] [51,1] [30240,1000]
0x0003 GPSLongitudeRef = "E"
0x0004 GPSLongitude = [2,1] [17,1] [24120,1000]
Convert:
lat = 48 + 51/60 + 30.24/3600 = 48.85840 (N -> +)
lon = 2 + 17/60 + 24.12/3600 = 2.29003 (E -> +)
Result: {"hasGps": true, "lat": 48.85840, "lon": 2.29003}Why the RATIONAL denominator matters
Seconds are commonly stored as a fraction over 1000 (or 100) to keep sub-second precision in integers. Dividing numerator by denominator recovers the real value.
Seconds stored as [26640, 1000]: 26640 / 1000 = 26.64 seconds If a parser ignores the denominator and reads 26640 as seconds: 48 + 51/60 + 26640/3600 = 56.25... <- badly wrong Always divide each pair. The previewer does: coords.map(pair => pair[0] / pair[1])
Applying the hemisphere reference
The single most common bug in hand-rolled parsers is forgetting the ref tag, which leaves every southern/western coordinate in the wrong hemisphere.
GPSLatitudeRef = "S", GPSLatitude = [33,1][52,1][4080,1000] decimal = 33 + 52/60 + 4.08/3600 = 33.86780 ref is "S" -> negate -> -33.86780 Without the negate, the pin lands in Lebanon, not Sydney.
The previewer's exact decode, in JavaScript
This is the conversion the tool runs after piexifjs returns the GPS group. A non-finite result means no usable coordinate.
function gpsToDecimal(coords, ref) {
if (!Array.isArray(coords) || coords.length < 3) return NaN;
const [d, m, s] = coords.map(pair => pair[0] / pair[1]);
let dec = d + m / 60 + s / 3600;
if (ref === "S" || ref === "W") dec = -dec;
return dec;
}
const lat = gpsToDecimal(gps[2], gps[1]); // 0x0002, 0x0001
const lon = gpsToDecimal(gps[4], gps[3]); // 0x0004, 0x0003
const hasGps = Number.isFinite(lat) && Number.isFinite(lon);An incomplete triplet decodes to nothing
If a camera wrote only degrees and minutes (two pairs) the guard clause returns NaN, and the previewer shows the no-GPS notice instead of an off-by-a-lot pin.
GPSLatitude = [40,1] [44520,1000] // only 2 RATIONALs coords.length < 3 -> return NaN hasGps = false -> "No GPS data found in EXIF" This is intentional: a partial coordinate is not a coordinate.
Edge cases and what actually happens
Coordinate array has fewer than three rationals
NaN -> no GPSThe decode requires a full degrees/minutes/seconds triplet. If only two pairs (or one) are present, the conversion returns NaN and the previewer reports no GPS. Some cameras encode coordinates differently; a strictly DMS-triplet parser like this one will not read those.
Sign baked into the numbers instead of the ref tag
Encoding errorEXIF coordinate rationals are defined as non-negative, with direction in the ref tag. A file that wrote a negative degree value is non-conformant; a triplet-and-ref parser may double-negate or misplace it. Conformant files keep the numbers positive and the sign in GPSLatitudeRef/GPSLongitudeRef.
Zero or missing denominator in a RATIONAL
Invalid rationalA denominator of 0 makes numerator/denominator non-finite, propagating to a NaN decimal. The previewer treats the whole coordinate as absent rather than rendering a pin from a corrupt value. This usually signals a malformed or truncated EXIF block.
Missing GPSLatitudeRef / GPSLongitudeRef
Sign undefinedWithout the ref tag the hemisphere is unknown. The previewer's check is ref === "S" / ref === "W", so a missing ref defaults to the positive hemisphere — which is wrong for southern/western locations. Conformant cameras always write the ref alongside the coordinate.
Altitude or timestamp present but you expected them in the output
Not decodedThe GPS IFD may contain GPSAltitude (0x0006) and GPSTimeStamp (0x0007), but the previewer reads only tags 1–4 and returns {hasGps, lat, lon}. Those fields exist in the file; this tool simply does not surface them. Use a full EXIF reader if you need altitude or time.
Wrong TIFF byte order (endianness) assumed
Garbled offsetsThe TIFF header starts with II (little-endian) or MM (big-endian); every multi-byte value must be read in that order. A parser that hard-codes one endianness mangles the IFD offsets and the rationals. piexifjs handles both; hand-rolled parsers must read the byte-order mark first.
HEIC or other non-JPEG/TIFF container
Unsupported formatEven though HEIC can carry the same GPS IFD structure, piexifjs parses JPEG and TIFF EXIF, so the previewer will not extract coordinates from HEIC. The encoding is the same; the container is not supported by this code path.
Coordinates resolve to exactly 0,0
Likely zeroed fieldA decode of 0.00000, 0.00000 is far more often a zeroed/initialised GPS field than a genuine photo at null island. The math is technically valid, so the tool will pin it — interpret near-zero coordinates with suspicion and inspect the raw rationals.
Frequently asked questions
Why is GPS stored as degrees/minutes/seconds rather than decimal?
EXIF predates ubiquitous floating-point conventions and uses the RATIONAL type (two uint32) for precision without floats. DMS maps cleanly onto three RATIONALs, and storing seconds as, say, numerator over 1000 preserves sub-second precision in integers. You convert to decimal degrees when you need to plot it.
What exactly is the RATIONAL type?
Two consecutive unsigned 32-bit integers: a numerator followed by a denominator, 8 bytes total. The value is numerator/denominator. A DMS coordinate is three RATIONALs (24 bytes). Values are non-negative; sign lives in the ref tag.
How do I convert the triplet to decimal degrees?
Divide each pair to get degrees, minutes, and seconds, then compute degrees + minutes/60 + seconds/3600. Finally apply the hemisphere: negate if GPSLatitudeRef is "S" or GPSLongitudeRef is "W". That is precisely the formula the previewer uses.
Where does the sign of the coordinate come from?
From the reference tags, not the numbers. GPSLatitudeRef is "N" or "S"; GPSLongitudeRef is "E" or "W". S and W negate the decimal value. Forgetting this is the classic bug that lands every southern-hemisphere photo in the wrong place.
How is altitude stored, and does the previewer show it?
GPSAltitude (0x0006) is a single RATIONAL in metres, with GPSAltitudeRef (0x0005) as 0 for above sea level or 1 for below. The previewer does not decode it — it reads latitude and longitude only. Use a full EXIF tool for altitude.
Why does my coordinate come out wildly wrong?
Two usual causes: ignoring the RATIONAL denominator (treating 26640 as 26640 seconds instead of 26.64), or skipping the ref-tag negation. Divide every pair and always apply the hemisphere sign. The worked-conversion table above shows correct results to compare against.
What makes a parse return no coordinates?
A coordinate array with fewer than three RATIONALs, a zero denominator, or a non-finite result. The previewer's guard returns NaN in those cases, sets hasGps to false, and shows the no-GPS notice instead of a misleading pin.
Does endianness affect parsing?
Yes. The TIFF header's II/MM mark sets little- or big-endian, and all offsets and rationals must be read accordingly. piexifjs handles both transparently; if you write your own parser, read the byte-order mark first or your offsets will be garbage.
Can I get GPS from HEIC with this encoding knowledge?
The GPS IFD structure can appear in HEIC, but the previewer's piexifjs path supports JPEG and TIFF only, so it will not extract HEIC coordinates. Convert HEIC to JPEG first, or use a parser with HEIF support, then apply the same DMS-to-decimal math.
Which JavaScript library should I use to read EXIF GPS?
This tool uses piexifjs, a widely-used pure-JS library for reading and writing JPEG/TIFF EXIF. exifr is a modern alternative with async streaming and broader format coverage. Whichever you use, you still apply the d + m/60 + s/3600 conversion and the ref-tag sign yourself, or via the library's helper.
Is the conversion lossy?
The DMS-to-decimal math is exact arithmetic on the stored rationals; any 'loss' is just the precision the camera chose when writing the seconds denominator. The previewer carries full precision internally and displays five decimal places in the marker popup, which is roughly metre-level.
Where can I see this decode in action?
The EXIF Map Previewer runs exactly this conversion in your browser and plots the result. To strip the GPS afterwards, use the EXIF Scrubber, and to confirm a file is a genuine JPEG/TIFF before parsing, the Magic-Byte Validator.
Privacy first
Every JAD Security operation runs entirely in your browser. Files, passwords, and PGP private keys never leave your device — verified by zero outbound network requests during processing.