●MAX — Rork Max generates native Swift for every Apple platform, from iPhone to Vision Pro●NATIVE — It reaches native capabilities like AR/LiDAR, Metal 3D, Dynamic Island, Live Activities, and HealthKit●PUBLISH — Publish to the App Store in two clicks; Rork Max is $200/month●EXPO — Standard Rork builds iOS and Android together via React Native (Expo) and is free to start●PROMPT — Describe your app idea in plain English and Rork generates deployable, store-ready code●PRICE — Standard Rork's paid plans start at $25/month: build with it first, then consider Max for native features●MAX — Rork Max generates native Swift for every Apple platform, from iPhone to Vision Pro●NATIVE — It reaches native capabilities like AR/LiDAR, Metal 3D, Dynamic Island, Live Activities, and HealthKit●PUBLISH — Publish to the App Store in two clicks; Rork Max is $200/month●EXPO — Standard Rork builds iOS and Android together via React Native (Expo) and is free to start●PROMPT — Describe your app idea in plain English and Rork generates deployable, store-ready code●PRICE — Standard Rork's paid plans start at $25/month: build with it first, then consider Max for native features
Your Rork App's Photos Look Sideways Only After Upload — Normalizing EXIF Orientation and Stripping Location Metadata
A photo that looks upright in your app rotates 90 degrees once it hits your server. The cause is the EXIF Orientation tag. Here's how to bake the rotation into pixels and strip location metadata with expo-image-manipulator before uploading.
A user uploads a photo. On their own phone it looks perfectly upright, but the version stored on your server — or shown to another user — is rotated 90 degrees. As an indie developer running image-heavy apps, I have hit this more than once. The first time, a user told me "the photo I uploaded shows up sideways on my friend's screen," and I spent half a day chasing it. The short version: the pixels were never rotated. The rotation lived in a separate place, the EXIF Orientation tag, and the two sides of the pipeline disagreed about whether to read it.
The same structure shows up in apps you build with Rork. React Native's <Image> respects the Orientation tag and renders correctly, but the server you upload to, the image library that resizes it, or a third-party preview often ignore the tag entirely. Today we'll build the piece that prevents all of this: bake the rotation into the pixels and drop the EXIF — including location — before the file is uploaded.
It Looks Right in the App but Tilts Only After Transfer
Start by isolating the symptom. Three observations almost always confirm an Orientation issue:
The same file looks upright inside your app's <Image>
Uploaded to a server and opened in a browser or another library, it is rotated
Portrait shots break far more often than landscape ones
When all three line up, it is the Orientation tag. An iPhone camera, even when held vertically, stores the raw landscape pixels from the sensor and writes "rotate this 90 degrees clockwise when displaying" into the Orientation tag. <Image> reads the tag and shows it correctly; a server that ignores the tag shows the raw, sideways pixels. That is the whole disagreement.
The Cause Is EXIF Orientation — the Pixels Never Moved
The Orientation tag is an integer from 1 to 8 describing a rotation-and-flip combination. In practice you'll meet four of them.
Value
Meaning
Typical source
1
No rotation (upright)
Landscape capture, already normalized
3
Rotated 180 degrees
Phone held upside down
6
Upright after 90 degrees clockwise
Portrait capture (most common)
8
Upright after 90 degrees counter-clockwise
Portrait, opposite way
Value 6 dominates, and every case I was reported was a 6. The key point: when the value isn't 1, the pixels and the display instruction are out of sync. Since your upload target ignores the tag, the most portable fix is to physically rotate the pixels on your side and reset the tag to 1.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can finally diagnose why a photo looks upright on your device but rotates 90 degrees on your server or a friend's screen, reading it straight from the EXIF Orientation value (1-8)
✦You'll get a working expo-image-picker + expo-image-manipulator implementation that bakes rotation into pixels and removes EXIF before upload
✦You'll be able to stop leaking the GPS location embedded in user photos before the file ever leaves the device
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Understand What expo-image-picker Actually Returns
expo-image-picker returns metadata when you pass exif: true. To observe the cause first, let's inspect the Orientation of the selected photo.
import * as ImagePicker from 'expo-image-picker';// Diagnostic helper: read the EXIF Orientation of the chosen imageasync function pickAndInspect() { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], exif: true, // request EXIF (not included by default) quality: 1, }); if (result.canceled) return; const asset = result.assets[0]; // iOS uses "Orientation"; Android may nest the key differently const orientation = asset.exif?.Orientation ?? asset.exif?.['{TIFF}']?.Orientation ?? 1; console.log('orientation =', orientation); // portrait shots often print 6 console.log('size =', asset.width, 'x', asset.height); return asset;}
One behavior is worth remembering. On iOS, if you let the picker re-encode by setting quality below 1, it sometimes bakes the rotation in and normalizes Orientation to 1. With quality: 1, the original Orientation tends to survive. Because the outcome depends on the quality value, I prefer not to rely on the picker and to normalize explicitly on my side.
Bake the Rotation Into the Pixels
The normalization itself uses expo-image-manipulator. Decoding and re-encoding the image writes out upright pixels that already account for the Orientation tag. EXIF is not carried across the re-encode, which also handles the location stripping below.
import * as ImageManipulator from 'expo-image-manipulator';// Re-bake the photo into upright pixels and return a JPEG with no EXIFasync function normalizeForUpload(uri: string) { const manipulated = await ImageManipulator.manipulateAsync( uri, [], // an empty op list is fine — the re-encode normalizes { compress: 0.8, // 0-1; 0.8 is visually near-lossless for uploads format: ImageManipulator.SaveFormat.JPEG, } ); // manipulated.uri points to a new file with Orientation=1 and no EXIF return manipulated;}
manipulateAsync interprets the Orientation before drawing, so even an empty op list produces an upright bitmap that reflects the tag's instruction. The new URI has Orientation 1, so a server that never reads the tag still stores the correct orientation. A HEIC photo gets unified into a widely compatible JPEG by specifying format: JPEG.
Strip EXIF to Stop the Location Leak
Rotation is only half of it. EXIF also carries the user's GPS coordinates, capture time, and device model. Upload a photo taken at home untouched, and you've handed its latitude and longitude to an external service. The re-encode drops EXIF, but it's worth building the habit of verifying that it's actually gone.
import * as ImageManipulator from 'expo-image-manipulator';import * as FileSystem from 'expo-file-system';// Normalize + verify: log original vs. new file size after processingasync function safeProcess(originalUri: string) { const before = await FileSystem.getInfoAsync(originalUri, { size: true }); const out = await ImageManipulator.manipulateAsync( originalUri, [{ resize: { width: 1600 } }], // long edge to 1600px; plenty for posts { compress: 0.8, format: ImageManipulator.SaveFormat.JPEG } ); const after = await FileSystem.getInfoAsync(out.uri, { size: true }); const ratio = (1 - (after.size ?? 0) / (before.size ?? 1)) * 100; console.log(`size: ${before.size} -> ${after.size} bytes (-${ratio.toFixed(0)}%)`); // open the output JPEG in a viewer and confirm the location fields are empty return out.uri;}
In my own checks, an iPhone portrait photo (around 3.2MB, Orientation 6) processed at a 1600px long edge with compress: 0.8 shrank to roughly 280-420KB. That is more than 85% less to transfer, upright pixels, and stripped location metadata — three wins in one pass. App Store privacy guidance rewards data minimization, and stripping EXIF before upload is exactly the kind of "don't collect what you don't need" implementation you can point to.
Funnel Every Upload Path Through Normalization
Finally, consolidate this at the single doorway to upload. Even with several entry points — capture, library pick, drag-and-drop — routing everything through normalizeForUpload right before the request structurally prevents both orientation and location accidents.
async function uploadPhoto(pickedUri: string, endpoint: string) { // 1) Whatever path the image came from, normalize it here const safeUri = await safeProcess(pickedUri); // 2) Send only the upright, EXIF-free file to the server const form = new FormData(); form.append('file', { uri: safeUri, name: 'photo.jpg', type: 'image/jpeg', } as any); const res = await fetch(endpoint, { method: 'POST', body: form }); if (!res.ok) throw new Error(`upload failed: ${res.status}`); return res.json();}
When you ask Rork to implement this, state the condition explicitly: "re-encode the image with expo-image-manipulator right before upload to normalize Orientation and strip EXIF." Generated code converges on this shape when you say so. A vague "upload the image" tends to send the picker URI as-is, so that one extra sentence is what changes the quality.
What Bit Me in Production, and How to Verify
A few traps from real operation:
Fetching an Android content:// URI directly fails: manipulateAsync outputs a file:// URI, so inserting normalization also sidesteps this.
Thumbnails and the full image processed separately end up rotated differently: build the list thumbnail from the same normalization function and the mismatch disappears.
Setting compress: 1 to avoid re-encode loss means the file never shrinks: for posting use cases I recommend around 0.8. Full-size storage is the only case that needs lossless.
As a first step, pick a single portrait photo, run it through safeProcess, upload the output, and check both orientation and location. Once that passes, the rest is dropping normalizeForUpload into your existing upload path. Photos quietly carry a slice of someone's life. Fixing the orientation is a good moment to also stop holding on to a location you never needed — and that quiet care is part of what makes an app worth keeping.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.