Documentation

EXIF Gallery Documentation

Complete guide to integrating GPS-based photo filtering into your Capacitor app.

Overview

Capacitor EXIF Gallery is the only Capacitor plugin that provides native GPS-based photo filtering on both iOS and Android. It reads EXIF metadata directly on the native layer — in Swift and Kotlin — to filter device photos by GPS location, route polylines, and date ranges before the user makes a selection.

Unlike standard gallery plugins that simply open the photo picker, EXIF Gallery adds an intelligent filtering layer. This is essential for applications that need location-verified photo evidence, such as insurance claims, construction documentation, real estate listings, and travel journals.

Onboarding and welcome screen of the EXIF Gallery pluginLocation and time filter interfaceCode examples and integration patterns

How It Compares

Feature@capacitor/camera@capacitor-community/mediaEXIF Gallery
Pick images from gallery
Read EXIF metadataPartial
Filter by GPS location
Filter by time range
Route/polyline corridor filter
Multi-language native UI
Intelligent fallback logic

At a Glance

  • Filter photos by GPS radius or route polyline
  • Filter by date/time range
  • Combine location + time filters (AND logic)
  • Extract EXIF metadata (GPS, timestamps)
  • Intelligent fallback when too few results
  • Native UI in English, German, French, Spanish
  • Custom UI text overrides
  • iOS 15+ & Android 7.0+ (API 24)
  • Capacitor 8 compatible
  • Free for debug builds — license required for production

Platform Support

iOS

Minimum VersioniOS 15.0+
LanguageSwift 5.5+
FrameworkCapacitor 8.0+
Tested OniOS 15.x, 16.x, 17.x, 18.x — iPhone SE, iPhone 14, iPhone 15 Pro Max, iPad (all sizes)

Android

Minimum VersionAndroid 7.0+ (API 24)
LanguageKotlin 1.9+
FrameworkCapacitor 8.0+
Tested OnAndroid 7.0+, 10, 11, 12, 13, 14 — Pixel 4–7, Samsung Galaxy S21/S22, Galaxy Tab S8

Quick Start

1. Install

Install the plugin via npm and sync your Capacitor project.

bash
1npm install @kesbyte/capacitor-exif-gallery
2npx cap sync

2. Configure License Key

Required for production builds only. Debug builds work without a license for testing. Purchase your license key at https://plugins.kesbyte-digital.com/exif-gallery

iOS Configuration (Info.plist)

Open your ios/App/App/Info.plist and add:

xml
1<key>KBExifGalleryLicense</key>
2<string>YOUR_LICENSE_KEY_HERE</string>

Android Configuration (AndroidManifest.xml)

Open your android/app/src/main/AndroidManifest.xml and add inside the '<'application'>' tag:

xml
1<application>
2 <!-- Other configuration... -->
3
4 <meta-data
5 android:name="com.kesbytedigital.exifgallery.LICENSE_KEY"
6 android:value="YOUR_LICENSE_KEY_HERE" />
7
8</application>

Validation Behavior

Debug Builds: License validation is skipped — full functionality available for testing.

Production Builds: License is validated when pick() is called.

  • Valid license: Gallery opens normally
  • Invalid/missing license: pick() throws an error immediately

Error codes: LICENSE_MISSING, LICENSE_INVALID, LICENSE_BUNDLE_MISMATCH

The license is validated at the moment you call pick(), not during initialize(). This ensures fast app startup while still enforcing licensing before the plugin is actually used.

Troubleshooting

"License key not found" error:

  • Verify the key name matches exactly: KBExifGalleryLicense (iOS) or com.kesbytedigital.exifgallery.LICENSE_KEY (Android)
  • Ensure you ran npx cap sync after adding the license
  • Check that the license key has no extra whitespace or line breaks

"Bundle ID mismatch" error:

  • Your license is tied to a specific bundle ID (e.g., com.example.myapp)
  • Verify your app's bundle ID matches the license
  • iOS: Check CFBundleIdentifier in Info.plist
  • Android: Check applicationId in build.gradle

3. Configure Permissions

iOS Permissions (Info.plist)

Add the following to your ios/App/App/Info.plist:

xml
1<key>NSPhotoLibraryUsageDescription</key>
2<string>This app needs access to your photo library to filter and select images</string>
3
4<key>NSLocationWhenInUseUsageDescription</key>
5<string>This app uses your location to enhance image filtering capabilities</string>
  • NSPhotoLibraryUsageDescription: Required for reading photos from the library
  • NSLocationWhenInUseUsageDescription: Required for location-based filtering (only when app is in use)

Android Permissions (AndroidManifest.xml)

Add the following to your android/app/src/main/AndroidManifest.xml:

xml
1<!-- Photo Library Permissions -->
2<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
3<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
4
5<!-- Location Permission -->
6<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  • READ_MEDIA_IMAGES: For Android 13+ (API 33+), granular image access
  • READ_EXTERNAL_STORAGE: For Android 12 and below (API 32 or less), legacy storage access
  • ACCESS_FINE_LOCATION: Required for location-based filtering

4. Initialize

Call initialize() once at app startup before using pick().

typescript
1import { ExifGallery } from '@kesbyte/capacitor-exif-gallery';
2
3// During app initialization
4await ExifGallery.initialize();

5. Open Gallery with Filters

Call pick() with optional filter configuration to open the native gallery.

typescript
1const result = await ExifGallery.pick({
2 filter: {
3 location: {
4 coordinates: [{ lat: 52.52, lng: 13.40 }],
5 radius: 5000 // 5km radius
6 }
7 }
8});
9
10// Access selected images
11if (!result.cancelled) {
12 result.images.forEach(image => {
13 console.log(`Selected: ${image.uri}`);
14 console.log(`Location: ${image.exif?.lat}, ${image.exif?.lng}`);
15 console.log(`Timestamp: ${image.exif?.timestamp}`);
16 });
17}

Features in Detail

Location-Based Filtering

Filter images by geographic location with two flexible input formats. You can specify individual GPS coordinates or use encoded polylines from services like Google Maps.

Using coordinates

Pass an array of GPS coordinates to filter images within a specified radius of each point.

typescript
1const result = await ExifGallery.pick({
2 filter: {
3 location: {
4 coordinates: [
5 { lat: 52.5163, lng: 13.3777 }, // Berlin
6 { lat: 48.1374, lng: 11.5755 } // Munich
7 ],
8 radius: 10000 // 10km around each point
9 }
10 }
11});

Using encoded polylines (Google Maps format)

Pass an encoded polyline string from Google Directions API or similar services to filter images along a route.

typescript
1const result = await ExifGallery.pick({
2 filter: {
3 location: {
4 polyline: route, // Encoded string like "_p~iF~ps|U_ulLnnqC"
5 radius: 5000 // 5km corridor around route
6 }
7 }
8});

Why encoded polylines?

  • 92% smaller payload (5KB to 400 bytes for 100 points)
  • URL-safe format
  • Direct compatibility with Google Maps, Mapbox, OpenStreetMap

Time Range Filtering

Select images taken within a specific date/time window. The plugin reads the EXIF DateTimeOriginal timestamp — the actual capture moment, not the file modification date.

typescript
1const result = await ExifGallery.pick({
2 filter: {
3 timeRange: {
4 start: new Date('2025-01-01'),
5 end: new Date('2025-12-31')
6 }
7 }
8});

Combined Filters (Location AND Time)

Apply multiple filters simultaneously. When both location and time filters are provided, images must match ALL criteria (AND logic).

typescript
1const result = await ExifGallery.pick({
2 filter: {
3 location: {
4 coordinates: [{ lat: 52.52, lng: 13.40 }],
5 radius: 5000
6 },
7 timeRange: {
8 start: new Date('2025-01-01'),
9 end: new Date('2025-12-31')
10 }
11 }
12});

EXIF Metadata Extraction

Access GPS coordinates and timestamp information from every selected image. The EXIF data is read natively without copying files to temporary directories.

typescript
1const result = await ExifGallery.pick();
2
3result.images.forEach(image => {
4 const exif = image.exif;
5 if (exif) {
6 console.log(`Latitude: ${exif.lat}`);
7 console.log(`Longitude: ${exif.lng}`);
8 console.log(`Taken at: ${exif.timestamp}`);
9 }
10});

Custom UI Text

Override any default UI string in any language. Translations are passed at initialization and merged with the built-in defaults.

typescript
1await ExifGallery.initialize({
2 locale: 'en',
3 customTexts: {
4 galleryTitle: 'Select Your Photos',
5 selectButton: 'Pick Images',
6 cancelButton: 'Close',
7 filterDialogTitle: 'Advanced Filters',
8 emptyMessage: 'No photos found matching your criteria'
9 }
10});

Automatic Permission Handling

The plugin requests photo library and location permissions just-in-time when pick() is called. You can optionally request permissions upfront during onboarding by setting requestPermissionsUpfront to true.

typescript
1await ExifGallery.initialize({
2 requestPermissionsUpfront: true
3});

Usage Examples

Basic Gallery

Open the gallery without filters. Users can manually select any images from their library.

typescript
1import { ExifGallery } from '@kesbyte/capacitor-exif-gallery';
2
3const result = await ExifGallery.pick();
4
5if (!result.cancelled) {
6 const fileUris = result.images.map(img => img.uri);
7 console.log(`Selected ${fileUris.length} images`);
8}

Location Filter with Fallback

Automatically switches to time-based filtering if the location filter returns fewer results than the fallbackThreshold.

typescript
1const result = await ExifGallery.pick({
2 filter: {
3 location: {
4 coordinates: [{ lat: 52.52, lng: 13.40 }],
5 radius: 1000 // 1km - strict radius
6 }
7 },
8 fallbackThreshold: 5 // Switch to time filter if < 5 images found
9});

Pre-configured Filters (Read-only)

Enforce specific filters without allowing users to adjust them. Set allowManualAdjustment to false to lock the filter configuration.

typescript
1const result = await ExifGallery.pick({
2 filter: {
3 location: {
4 coordinates: [{ lat: 48.85, lng: 2.29 }], // Paris
5 radius: 20000
6 }
7 },
8 allowManualAdjustment: false // User cannot change filters
9});

Interactive Filter UI

Allow users to adjust filters in the native gallery interface. This is the default behavior (allowManualAdjustment defaults to true).

typescript
1const result = await ExifGallery.pick({
2 filter: {
3 timeRange: {
4 start: new Date('2025-01-01'),
5 end: new Date('2025-12-31')
6 }
7 },
8 allowManualAdjustment: true // Default - user can adjust
9});

Multi-language Support

Switch the native UI language at runtime by re-initializing with a different locale. Built-in support for English, German, French, and Spanish.

typescript
1// German UI
2await ExifGallery.initialize({ locale: 'de' });
3const resultDE = await ExifGallery.pick();
4
5// French UI
6await ExifGallery.initialize({ locale: 'fr' });
7const resultFR = await ExifGallery.pick();
8
9// Spanish UI
10await ExifGallery.initialize({ locale: 'es' });
11const resultES = await ExifGallery.pick();

Route-Based Filtering

Filter images along a recorded hiking or travel route using an encoded polyline. The radius parameter defines the corridor width on each side of the route.

typescript
1// Your recorded GPS points from a Strava activity or hiking app
2const routePolyline = "_p~iF~ps|U_ulLnnqC_seK`xwE";
3
4const result = await ExifGallery.pick({
5 filter: {
6 location: {
7 polyline: routePolyline,
8 radius: 2000 // 2km corridor on each side of route
9 }
10 }
11});
12
13console.log(`Found ${result.images.length} photos along your route`);

API Reference

Methods

initialize(config?: InitConfig)

Initialize the plugin with optional configuration. Must be called before pick(). Can be called multiple times to update configuration.

Default behavior (no config):

  • Detects system language automatically
  • Uses built-in English/German/French/Spanish translations
  • Requests permissions just-in-time (when pick() is called)

config (optional) — InitConfig object

pick(options?: PickOptions)

Open the native gallery with optional filters and return selected images. Must call initialize() first, otherwise throws initialization_required error.

Filter behavior:

  • If filter provided: Gallery opens with pre-configured filters
  • If no filter: User can manually set filters in gallery UI
  • Auto-fallback: If location filter returns fewer images than fallbackThreshold, falls back to time filter

options (optional) — PickOptions object

Promise'<'PickResult'>'

Interfaces

InitConfig

Plugin initialization configuration. All properties are optional.

PropertyTypeDescription
locale'en' | 'de' | 'fr' | 'es'Optional locale for UI text. Auto-detects system language if not provided. Falls back to English if system language not supported.
customTextsPartial<TranslationSet>Optional custom text overrides. Merges with default translations. Override only the keys you need.
requestPermissionsUpfrontbooleanRequest photo library permissions during initialization. Default: false (permissions requested just-in-time when pick() is called).

PickOptions

Options for the pick() method.

PropertyTypeDescription
filterFilterConfigOptional filter configuration to pre-configure the gallery. If not provided, user can manually set filters in the gallery UI.
fallbackThresholdnumberMinimum number of results before automatic fallback to time filter. Default: 5.
allowManualAdjustmentbooleanAllow user to manually adjust filters in the gallery UI. Default: true. Set to false to enforce the provided filter configuration.

PickResult

Result from the pick() method.

PropertyTypeDescription
imagesImageResult[]Array of selected images. Empty if user cancelled or no images matched filters.
cancelledbooleantrue if user explicitly cancelled. false if user confirmed selection (even if empty).

ImageResult

Single image result from pick().

PropertyTypeDescription
uristringFile URI for the image (file:// path). Can be used to display or upload the image.
exifImageExif | undefinedEXIF metadata if available. May be undefined if image has no EXIF data.
filteredBy'time' | 'location'How this image was filtered: 'location' = matched location filter, 'time' = matched time filter (or fallback).

ImageExif

EXIF metadata extracted from an image.

PropertyTypeDescription
latnumber | undefinedLatitude from GPS EXIF data (if available).
lngnumber | undefinedLongitude from GPS EXIF data (if available).
timestampDate | undefinedTimestamp from EXIF DateTimeOriginal (if available).

FilterConfig

Combined filter configuration for location and/or time.

PropertyTypeDescription
locationLocationFilterOptional location-based filter. If provided with timeRange, both filters are applied (AND condition).
timeRangeTimeRangeFilterOptional time range filter. If provided with location, both filters are applied (AND condition).

LocationFilter

Location-based filter configuration.

PropertyTypeDescription
polylineLatLng[] | stringGPS track as array of coordinates or encoded polyline string. Images within radius meters of any point on the polyline will match.
coordinatesLatLng[]Individual coordinate points. Images within radius meters of any coordinate will match.
radiusnumberSearch radius in meters. Default: 100.

LatLng

Geographic coordinate with latitude and longitude.

PropertyTypeDescription
latnumberLatitude in decimal degrees (-90 to +90).
lngnumberLongitude in decimal degrees (-180 to +180).

TimeRangeFilter

Time range filter configuration.

PropertyTypeDescription
startDateStart date/time for the filter. Images taken at or after this time will match.
endDateEnd date/time for the filter. Images taken at or before this time will match.

TranslationSet

Complete set of UI text keys used by the native gallery interface. All keys are available for customization via customTexts in InitConfig.

KeyDescription
galleryTitleGallery screen title
selectButton"Select" button text
cancelButton"Cancel" button text
selectAllButton"Select All" button text
deselectAllButton"Deselect All" button text
selectionCounterSelection counter (supports {count} and {total} placeholders)
confirmButton"Confirm" button text
filterDialogTitleFilter dialog title
radiusLabel"Radius (meters)" label
startDateLabel"Start Date" label
endDateLabel"End Date" label
loadingMessage"Loading images..." message
emptyMessage"No images found" message
errorMessage"An error occurred" message
retryButton"Retry" button text
initializationError"Plugin not initialized" error
permissionError"Permission denied" error
filterError"Invalid filter parameters" error

SupportedLocale

Supported languages for built-in translations: 'en' (English), 'de' (German), 'fr' (French), 'es' (Spanish).

typescript
1type SupportedLocale = 'en' | 'de' | 'fr' | 'es';

Licensing

Capacitor EXIF Gallery uses a commercial license model. The plugin is free for debug/development builds, and requires a valid license key for production builds.

Debug Builds (Free)

  • No license key required for debug/development builds
  • Full functionality available for integration and testing
  • Integrate the plugin into your app and test all features freely

Production Builds (License Required)

  • !License key REQUIRED for production/release builds
  • !Production builds will fail validation without a valid license
  • !Purchase a license at https://plugins.kesbyte-digital.com/exif-gallery

How It Works

  1. 1Development: Install and test the plugin freely in debug builds
  2. 2Production: Purchase a license key before releasing your app
  3. 3Integration: Add the license key to your app configuration (Info.plist / AndroidManifest.xml)
  4. 4Build: Production builds validate the license automatically when pick() is called

Frequently Asked Questions

Support

Need help integrating the EXIF Gallery plugin? Here are the available support channels: