Skip to content

fix(backend): keep GPS coordinates of 0 during metadata extraction - #1425

Open
prawnsgupta wants to merge 3 commits into
AOSSIE-Org:mainfrom
prawnsgupta:fix/1406-zero-gps-coordinates
Open

fix(backend): keep GPS coordinates of 0 during metadata extraction#1425
prawnsgupta wants to merge 3 commits into
AOSSIE-Org:mainfrom
prawnsgupta:fix/1406-zero-gps-coordinates

Conversation

@prawnsgupta

@prawnsgupta prawnsgupta commented Jul 27, 2026

Copy link
Copy Markdown

Fixes #1406

The problem

MetadataExtractor.extract_gps_coordinates() resolved its fallback sources with truthiness rather than an explicit None check:

lat = lat or gps.get("latitude")

0.0 is falsy in Python, so a latitude or longitude of exactly 0 — a real location on the equator or the prime meridian — was treated as missing and overwritten with None. By the time execution reached the correct if lat is not None and lon is not None: guard the value was already gone, so extract_all() returned (None, None) for perfectly valid coordinates.

extract_all() is called during image upload (app/utils/images.py:275) to populate the location fields, so photos taken in Kenya, Uganda, Ecuador, Indonesia, Brazil (equator) or the UK, France, Spain, Ghana, Algeria (prime meridian) lost their location entirely and never showed up in the location-based Memories feature.

Before / after

Running extract_all() on realistic metadata, on main and on this branch:

photo metadata before after
Nairobi NP, Kenya (equator) (None, None) (0.0, 36.8219)
Greenwich, UK (prime meridian) (None, None) (51.4779, 0.0)
Null Island (0, 0) (None, None) (0.0, 0.0)
Pontianak, Indonesia (nested exif.gps) (None, None) (0.0, 109.3425)
Quito, Ecuador (lat/lon aliases) (None, None) (0.0, -78.4678)
malformed top-level, valid exif.gps (None, None) (28.6, 77.2)
out-of-range top-level, valid exif.gps (None, None) (28.6, 77.2)
New Delhi, India (control) (28.6139, 77.209) (28.6139, 77.209)
no GPS at all (control) (None, None) (None, None)

The fix

Each coordinate is resolved through _resolve_coordinate(), which walks its candidate sources in order of preference and converts and range-checks each one in turn, returning the first that is actually usable. A candidate is skipped when it is:

  • absent (None, or a blank string),
  • unreadable as a number ("not-a-number", a list, a dict),
  • a boolbool subclasses int, so float(True) would otherwise sail through as latitude 1.0,
  • outside its valid range (±90 / ±180, which also rejects NaN and infinity).

0 is explicitly kept, which is the actual bug: it is a real location but falsy in Python, so an a or b chain discarded it.

Deliberately unchanged:

  • Source precedence — top-level, then nested exif.gps, then the lat/lon/Latitude/Longitude aliases, exactly as before.
  • Range validation — the ±90 / ±180 check still rejects genuinely invalid values. 0 is valid and was never what that check was meant to catch.
  • A lone coordinate is not a location — if either half cannot be resolved, the result is still (None, None).

Two things that fall out of resolving each coordinate independently: a top-level 0.0 can no longer be silently replaced by a different non-zero value from exif.gps (the wrong location rather than no location), and an unusable value in a preferred field no longer masks a good one further down the chain.

Also included

One line in app/utils/images.py — the upload path's GPS log used the same if latitude and longitude: truthiness test, so valid zero coordinates were extracted but omitted from the logs. Same root cause, log-only, no behavioural change beyond the log line.

Scope

The separate _extract_gps_coordinates() in app/utils/images.py parses GPS as DMS tuples and is not affected — a (0, 0, 0) DMS tuple is a non-empty tuple and therefore truthy — so this fix is correctly scoped to extract_location_metadata.py.

Tests

backend/tests/test_extract_location_metadata.py — 81 tests covering the equator, prime meridian, Null Island, integer/string/negative zeros, the nested and aliased field names, source precedence, and the existing missing / half-present / out-of-range / unparseable / malformed-exif cases.

Plus, per review, the resolver itself and the fallthrough behaviour: malformed, out-of-range, blank, boolean and non-finite values in a higher-priority field falling through to a valid exif.gps or alias, falling through more than one bad source, falling through to a valid 0, and each coordinate falling through independently.

Verified the tests catch the original bug: 19 of them fail on main.

Full backend suite: 567 passed. black and ruff clean on the changed files.

Summary by CodeRabbit

  • Bug Fixes
    • Improved GPS location metadata extraction across supported EXIF and alias fields.
    • Preserves coordinates with a value of 0 (including when nested in EXIF).
    • Correctly resolves latitude and longitude independently, combining valid values from different sources.
    • More reliably rejects incomplete, malformed, or invalid location data and returns null results when appropriate.
  • Tests
    • Expanded location metadata extraction test coverage, including zero preservation and GPS fallback/precedence cases.
    • Added validation for coordinate candidate handling, datetime parsing robustness, and safe handling of malformed metadata inputs.

extract_gps_coordinates() resolved its fallbacks with truthiness
(`lat or gps.get("latitude")`), so a latitude or longitude of exactly 0
-- a real location on the equator or the prime meridian -- was treated
as missing and overwritten with None before the `is not None` guard
could keep it. Photos from Kenya, Ecuador, Indonesia, the UK and
elsewhere lost their location entirely and never reached the
location-based Memories feature.

Each coordinate is now resolved through a _first_present() helper that
skips only None and blank strings, so 0 survives while genuinely absent
values still fall through to the next source. Source precedence
(top-level, then nested exif.gps, then the lat/lon aliases) is unchanged
and the -90..90 / -180..180 range validation is untouched.

The same truthiness test in the upload logging in images.py is corrected
too, since it otherwise omits valid zero coordinates from the log.

Adds tests covering the equator, the prime meridian, Null Island, the
nested and aliased field names, source precedence, and the existing
missing / out-of-range / malformed cases.

Fixes AOSSIE-Org#1406
@github-actions github-actions Bot added backend bug Something isn't working labels Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b6f64e2-6438-4828-a6d4-99bf3d81ef23

📥 Commits

Reviewing files that changed from the base of the PR and between 1c5e907 and 2a6f120.

📒 Files selected for processing (2)
  • backend/app/utils/extract_location_metadata.py
  • backend/tests/test_extract_location_metadata.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/app/utils/extract_location_metadata.py
  • backend/tests/test_extract_location_metadata.py

Walkthrough

GPS extraction now preserves valid zero coordinates, resolves latitude and longitude independently across metadata locations, normalizes malformed nested structures, and updates image logging checks. Tests cover precedence, validation, JSON and bytes input, invalid metadata, and datetime parsing.

Changes

GPS coordinate handling

Layer / File(s) Summary
Metadata source resolution
backend/app/utils/extract_location_metadata.py
Adds _resolve_coordinate, independently resolves coordinates across metadata sources, preserves zero values, handles malformed EXIF structures, and catches datetime overflow errors.
Image integration and coverage
backend/app/utils/images.py, backend/tests/test_extract_location_metadata.py
Uses explicit None checks for GPS logging and adds coverage for coordinate resolution, precedence, boundaries, malformed metadata, JSON/bytes input, and datetime extraction.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: Python

Poem

I’m a rabbit hopping where zero winds blow,
Keeping each coordinate safe in the snow.
From EXIF’s deep burrow to JSON’s bright trail,
No prime meridian point shall fail.
Hop, hop—Null Island stays on the map!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: preserving GPS coordinates of 0 during metadata extraction.
Linked Issues check ✅ Passed Fix preserves zero lat/lon across top-level, nested exif.gps, and aliases while keeping precedence, range checks, and lone-coordinate None,None behavior.
Out of Scope Changes check ✅ Passed Changes stay focused on GPS extraction, zero-coordinate logging, and tests; no unrelated feature work stands out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/utils/extract_location_metadata.py`:
- Around line 101-112: The coordinate extraction in the metadata parsing method
must fall through unusable higher-priority values instead of selecting them
before conversion. Replace the shared _first_present selection for latitude and
longitude with independent candidate iteration that converts and range-checks
each value, continuing to the next source when malformed or out of range; add
automated regressions covering malformed and out-of-range top-level coordinates
with valid exif.gps fallbacks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 963bf3b0-801c-4ca8-bf30-320485acc4c3

📥 Commits

Reviewing files that changed from the base of the PR and between 448c0d5 and ef7c2db.

📒 Files selected for processing (3)
  • backend/app/utils/extract_location_metadata.py
  • backend/app/utils/images.py
  • backend/tests/test_extract_location_metadata.py

Comment thread backend/app/utils/extract_location_metadata.py Outdated
Resolving a coordinate picked the first non-blank candidate and only then
converted it, so a malformed or out-of-range value in a preferred field
masked a valid value in a less preferred one: a top-level
"latitude": "not-a-number" discarded a perfectly good exif.gps latitude
and returned (None, None).

_first_present is replaced by _resolve_coordinate, which converts and
range checks each candidate in turn and continues past any that is
absent, unreadable or outside its range. Latitude and longitude keep
their existing precedence and are still resolved independently, and 0 is
still preserved.

Booleans are skipped explicitly, since bool subclasses int and float(True)
would otherwise be accepted as latitude 1.0. NaN and infinity are rejected
by the range check.

An exception midway through resolution now returns (None, None) rather
than a half-resolved pair.

Adds tests for the resolver itself and for falling through malformed,
out-of-range, blank and boolean values in a higher-priority field to a
valid exif.gps or lat/lon alias, including falling through to a valid 0.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/utils/extract_location_metadata.py`:
- Around line 59-82: Update the candidate conversion in _resolve_coordinate to
catch OverflowError alongside ValueError and TypeError, so oversized integer
candidates are ignored and resolution continues to lower-priority values. Add
regression coverage using an oversized candidate such as 10**400 followed by a
valid fallback, preserving the fallback guarantee.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 561cbd6b-e1e6-4814-963c-25a179afc0af

📥 Commits

Reviewing files that changed from the base of the PR and between ef7c2db and 1c5e907.

📒 Files selected for processing (2)
  • backend/app/utils/extract_location_metadata.py
  • backend/tests/test_extract_location_metadata.py

Comment thread backend/app/utils/extract_location_metadata.py
float() raises OverflowError, not ValueError, on an integer beyond the
float range, and JSON puts no size limit on integers. That escaped the
conversion guard in _resolve_coordinate and unwound to the outer handler,
so one oversized value abandoned the whole extraction and discarded a
valid coordinate in a lower-priority field along with it.

OverflowError now joins ValueError and TypeError, so an oversized integer
is skipped like any other unreadable candidate and resolution continues.

Adds coverage for oversized positive and negative integers, with and
without a usable fallback, and through the extract_all JSON entry point
where such a value would actually arrive.
@prawnsgupta

prawnsgupta commented Jul 27, 2026

Copy link
Copy Markdown
Author

@rohan-pandeyy — could you approve the CI workflow run on this PR when you have a moment?

PR Check is currently at action_required, so the linting and the frontend and backend test jobs have not executed yet. All other checks have reported successfully, and the branch is current with main with no conflicts.

Per the testing steps in CONTRIBUTING.md, run locally against this branch:

  • pytest — 572 passing
  • black and ruff — clean

The changes are limited to backend/app/utils/ and backend/tests/, with no frontend or Tauri files touched.

Let me know for any further changes. If no changes then feel free to merge the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Valid GPS coordinates on the equator / prime meridian (lat or lon == 0) are silently dropped during metadata extraction

1 participant