Come salvare un’immagine in Swift preservando i dati EXIF

Recentemente ho dovuto “lottare” con i dati EXIF salvati nelle foto di iOS.

Personalmente era la prima volta che avevo la necessità di fare in modo che le immagini conservassero questi dati anche quando caricate tramite delle API. Pensavo che iOS fornisse dei metodi standard per queste operazioni, invece la questione si è rivelata più particolare del previsto.

In questo articolo troverete l’implementazione iniziale che utilizzavo, e le nuove modifiche che ho apportato per riuscire a risolvere il problema.

Situazione attuale

Gestisco da diverso tempo (probabilmente dal 2010) il progetto in questione. Una delle varie funzionalità è la possibilità dell’utente di allegare delle foto, che vengono caricate su un backend tramite un servizio apposito. Di recente, tuttavia, è emersa la necessità che le immagini caricate mantengano i dati EXIF associati, in particolare quelli relativi alla geolocalizzazione.

Facendo alcune prove, ci siamo accorti che le immagini caricate dall’app iOS erano prive di tali dati, quindi mi sono messo ad indagare la cusa del problema.

L’implementazione esistente è composta principalmente da tre fasi:

  1. selezione delle immagini, utilizzando il picker nativo UIImagePickerController
  2. salvataggio in locale (nella directory Cache) delle immagini selezionate, operazione propedeutica allo step 3
  3. upload delle immagini tramite URLSession.uploadTask

Dopo alcune verifiche, era chiaro che il problema fosse al punto 2: le immagini salvate temporaneamente in locale erano già prive dei dati EXIF.

The old way: jpegData(compressionQuality:)

Il codice esistente salvava le immagini ricavate dal rullino utilizzando il metodo jpegData(compressionQuality:) che UIImage mette a disposizione. Partendo dal PHAsset, veniva estratta l’immagine e poi salvata in locale, in modo da avere un file da trasformare poi in un oggetto di tipo Data.

Questo è un esempio (minimale) di come venivano salvate le immagini:

private func savePhotoWithoutExif(
    info: [UIImagePickerController.InfoKey : Any],
    localUrl: URL
) throws {
    guard let image = info[.originalImage] as? UIImage else {
        throw PhotoError.missingImageData
    }
        
    guard let data = image.jpegData(compressionQuality: 1.0) else {
        throw PhotoError.invalidImage
    }
        
    do {
        try data.write(to: localUrl)
    } catch {
        throw PhotoError.saveFailed
    }
}

Se andiamo ad analizzare l’immagine che salva questo metodo, ci accorgeremo che i dati EXIF non sono più presenti (in particolare le coordinate GPS):

Per analizzare le immagini, ho utilizzato il Simulatore di iOS e stampato in console il percorso completo del file generato. In questo modo ho potuto navigarci tramite il Finder ed aprire il file con Anteprima, che permette di visualizzare tutti gli EXIF contenuti nel file.

The new way: CIContext().writeJPEGRepresentation

Mi sono messo alla ricerca di una soluzione che permettesse di salvare una copia dell’immagine in locale, senza però perdere i dati EXIF. Dopo qualche ricerca e un po’ di studio della documentazione, ho trovato una soluzione che si basa sull’utilizzo di CIContext. In sostanza, si tratta di renderizzare ed esportare l’immagine in formato jpeg, utilizzando il metodo writeJPEGRepresentation(of:to:colorSpace:) presente in CIContext. In questo modo, i dati EXIF vengono mantenuti.

Il codice è leggermente più complesso rispetto al precedente, qui ve lo propongo ripulito di altri controlli che ho aggiunto nella mia implementazione:

// create request to get all asset metadata
let options = PHContentEditingInputRequestOptions()
// download from iCloud if necessary
options.isNetworkAccessAllowed = true
asset.requestContentEditingInput(with: options) { (contentEditingInput: PHContentEditingInput?, _) -> Void in
    guard let contentEditingInput else {
        print("[ERROR] Unable to get PHContentEditingInput")
        return
    }
    guard 
        let imageUrl = contentEditingInput.fullSizeImageURL,
	let fullImage = CIImage(contentsOf: imageUrl, options: [ .applyOrientationProperty: true ]),
	let colorSpace = fullImage.colorSpace else {
	   print("[ERROR] Unable to create required parameters")
           return
	}
             
    do {
        try CIContext().writeJPEGRepresentation(of: fullImage,
                                                to: localUrl,
                                                colorSpace: colorSpace)
    } catch {
        print("[ERROR] Unable to write image (\(error.localizedDescription))")
    }
}

Si tratta, in sostanza, di ricavare tutti i metadati associati al PHAsset che stiamo gestendo, per poi renderizzare l’immagine tramite il metodo writeJPEGRepresentation.

Se andiamo a comparare i dati EXIF della foto originale e di quella salvata, vedremo in questo caso che sono identici, non abbiamo perso nessuna informazione:

Conclusioni e progetto

La soluzione che vi ho presentato è molto ridotta rispetto a quella reale che ho implementato. Non sono supportati, ad esempio, i vari formati che potete trovare in un PHAsset (ad esempio le live photos). L’obbiettivo era focalizzarmi sul solo punto che effettivamente vi permette di salvare delle immagini conservando i metadati EXIF.

Ho creato un repository su GitHub con un semplice progetto d’esempio, con cui potete verificare le due diverse implementazioni. Per qualsiasi domanda, scrivetemi nei commenti o su Mastodon.

Per i più curiosi, questa è l’immagine che ho utilizzato per le prove di questo articolo, scattata a Tokyo il 26/01/2020 (sigh).

Ciao!

Ingegnere informatico e sviluppatore freelance, mi occupo da anni di sviluppo per iOS (ma non solo). Dal 2008 scrivo su questo piccolo blog (con qualche lunga pausa), in cui parlo di programmazione e di qualsiasi altra cosa che mi diverta.

Leave a reply:

Your email address will not be published.

Site Footer