react-native-simple-heic2jpg
v0.4.0
Published
React Native Component for converts HEIC files on Android and iOS
Maintainers
Readme
react-native-simple-heic2jpg
React Native native module for converting local HEIC/HEIF images to JPEG on Android and iOS.
Installation
npm install react-native-simple-heic2jpg
# or
yarn add react-native-simple-heic2jpg
# For iOS
cd ios && pod installFeatures
Image format conversion
- Converts local HEIC/HEIF images to JPEG.
- JPEG and PNG inputs are passed through without conversion.
EXIF metadata preservation
- Copies supported metadata such as GPS, camera, orientation, and date fields from the source image to the converted JPEG.
- Exact metadata preservation depends on platform image/EXIF support.
iOS and Android support
- iOS implementation: Objective-C++ using ImageIO/CoreImage.
- Android implementation: Kotlin using
BitmapFactoryand AndroidX ExifInterface. - Supports React Native old architecture and TurboModule/new architecture wiring.
Simple JavaScript API
- One exported helper:
convertImage(imagePath, options?).
- One exported helper:
Usage
import { convertImage } from 'react-native-simple-heic2jpg';
const result = await convertImage(path);To receive raw base64 instead of a file URI:
const base64 = await convertImage(path, { returnBase64: true });JPEG quality
Pass quality to control the JPEG encode quality of converted HEIC/HEIF images:
// Smaller file, default quality
await convertImage(path);
// Higher quality, larger file
await convertImage(path, { quality: 95 });qualityis an integer from0(smallest) to100(best). It defaults to80.- Out-of-range values are clamped to
0–100; fractional values are rounded. - Quality only applies to HEIC/HEIF inputs that are converted. JPEG and PNG inputs pass through without re-encoding, so
qualityhas no effect on them.
Note: this release standardizes the JPEG quality across platforms. Previously Android encoded at
100and iOS used the system default; both now default to80, so converted JPEGs are noticeably smaller. Passquality: 100for the maximum-quality (largest) output.
Input path contract
convertImage accepts local image files as either:
- raw local file paths, for example
/var/mobile/.../IMG_0001.HEICor/storage/emulated/0/.../IMG_0001.HEIC - local
file://URIs, for examplefile:///var/mobile/.../IMG_0001.HEIC
content:// URIs are not supported by this release. Resolve them to a local file path before calling convertImage.
Stripping metadata
Pass stripExif or stripGps to drop metadata from the converted JPEG:
// Remove GPS only (keeps camera info, dates, orientation)
await convertImage(path, { stripGps: true });
// Remove all EXIF/GPS except the orientation tag
await convertImage(path, { stripExif: true });stripGps: trueremoves only GPS tags.stripExif: trueremoves all EXIF and GPS metadata except the orientation tag (which is kept so the image still renders upright).stripExifimpliesstripGps.- Both default to
false.
Stripping only applies to HEIC/HEIF inputs that are converted. JPEG and PNG inputs are passed through without re-encoding, so their metadata is returned untouched — convertImage(jpgPath, { stripGps: true }) does not modify the original JPEG.
Injecting GPS coordinates
Pass gps to write coordinates into the converted JPEG:
await convertImage(path, {
gps: { latitude: 37.5665, longitude: 126.978 },
});- The injected coordinates replace whatever GPS the source carried.
- Injection wins over
stripGps/stripExiffor the GPS block only: strip removes the source GPS first, then the provided coordinates are written. Other EXIF is unaffected. - Signed decimal degrees; the hemisphere refs (N/S, E/W) are derived from the sign.
- HEIC/HEIF input: coordinates are written onto the converted JPEG.
- JPEG input: normally a pass-through, but when
gpsis provided the JPEG is copied (metadata rewritten, pixels not re-encoded) and the returned path points to the injected copy — the original file is never mutated. Camera captures, which arrive as JPEG, get GPS this way. - PNG input: always a pass-through; PNG has no reliable EXIF container, so
gpsis ignored.
This pairs with the photo-picker section below: when Android's system picker hands you a file with zeroed GPS, your app can re-attach coordinates it knows from another source (its own location fix, MediaStore query, or user input).
Platform note: on iOS,
stripExifremoves the EXIF and GPS metadata blocks but may retain camera make/model fields stored in the TIFF block. GPS is removed on both platforms; Android'sstripExifalso drops make/model. Full parity is deferred to a future major release.
GPS metadata and the Android photo picker
If the converted output has no GPS tags even though the original photo has them, check how the input file reached your app before suspecting this library. On Android, files returned by the system photo picker (PICK_IMAGES / PickVisualMedia) have their GPS EXIF values zeroed out by the OS before your app ever sees the bytes. This is Android's scoped-storage location redaction (the system hides location information by default), and it cannot be bypassed for picker URIs — MediaStore.setRequireOriginal() is rejected for them even with the ACCESS_MEDIA_LOCATION permission (AOSP PickerUriResolver: "Require Original is not supported for Picker URI").
This library preserves GPS whenever the input file actually contains it. To feed it GPS-bearing inputs on Android, use a path the OS does not redact:
- files your app already owns on disk (app storage, downloads you wrote, camera output)
- the Storage Access Framework / documents picker (
ACTION_OPEN_DOCUMENT) - a MediaStore URI resolved with
ACCESS_MEDIA_LOCATION+MediaStore.setRequireOriginal(), copied to a local file first (content://is not accepted directly — see the input path contract)
EXIF orientation policy
Both platforms preserve the EXIF orientation tag but do not rotate the pixels:
- The converted JPEG carries the source's
Orientationtag unchanged. - The pixel data is written unrotated (Android decodes raw pixels with
BitmapFactory; iOS keeps theCIImageunrotated). - Your app — or the image component you render with — is responsible for interpreting the orientation tag, exactly as it would for the original HEIC.
This behavior is identical on iOS and Android.
Return value
By default, the JavaScript API resolves to a file:// URI string.
- HEIC/HEIF input: returns the converted JPEG file URI.
- JPEG input: returns the original file URI — unless
gpsis provided, in which case it returns a new injected-copy URI (the original is left untouched). - PNG input: returns the original file URI (
gpsis ignored).
When returnBase64 is true, the JavaScript API resolves to a raw base64 string.
- HEIC/HEIF input: returns the converted JPEG bytes as base64.
- JPEG/JPG input: returns the original file bytes as base64 — unless
gpsis provided, in which case it returns the injected copy's bytes. - PNG input: returns the original file bytes as base64 (
gpsis ignored). - The returned string does not include a
file://prefix. - The returned string does not include a
data:image/...;base64,prefix. Add one in your app if your target component requires a data URI. - HEIC/HEIF base64 mode may use temporary/cache files internally so the returned base64 comes from finalized JPEG bytes with preserved metadata. Generated temporary/cache files are cleaned after encoding.
- Base64 increases memory usage compared with URI mode, so URI mode remains the default and is recommended for large images.
Error handling
The promise rejects on failure. New failure paths carry a stable error.code you can branch on:
| error.code | Platform | When |
| --- | --- | --- |
| E_HEIF_UNSUPPORTED_OS | Android | HEIC/HEIF decode attempted below Android 9 (API 28), where the platform has no HEIF decoder. |
| E_HEIF_DECODE_FAILED | Android | The input could not be decoded (corrupt or not a real HEIC/HEIF). |
| E_UNSUPPORTED_URI | Android | A content:// URI was passed. Resolve it to a local file path first. |
Platform differences
content://rejection happens on both platforms, but the code differs: Android rejects withE_UNSUPPORTED_URI, while iOS rejects with its existingUnsupported URIcode. The codes are intentionally left unaligned in this release to avoid changing the established iOS code (a breaking change); full alignment is deferred to a future major.- Pass-through inputs (JPEG/PNG) are never re-encoded, so
stripExif/stripGpshave no effect on them on either platform. - Existing iOS reject codes (for example
Unsupported Image Format) are unchanged in this release.
License
MIT
