1

I save an image from the simulator photo library.

Original image from stock photo library

I then save the image as a string and when the user clicks Save, it is reconverted from the string into an image and placed in a table view in another controller. For some reason, only certain stock photos are being reconverted upside down.

Upside down image after reconverted back to image from string

Code for converting image to string:

extension UIImage
{
    func toString() -> String?
    {
        let data: Data? = self.pngData()
        return data?.base64EncodedString(options: .lineLength64Characters)
    }
}

Code to reconvert string back to image:

extension String
{
    func toImage() -> UIImage?
    {
        if let data = Data(base64Encoded: self, options: .ignoreUnknownCharacters)
        {
            return UIImage(data: data)
        }
    
        return nil
   }
}

The conversion is a derivation based on another StackOverflow page: Convert between UIImage and Base64 string

Database store code for entire 'family':

    /**
INSERT operation prepared statement for family

- Parameters
    - family: Contains the data for each child and parents' name and image
*/
func insertFamily(family: Family)
{
    var insertStatement: OpaquePointer? = nil
    let result = sqlite3_prepare_v2(self.db, self.insertFamilyQuery, -1, &insertStatement, nil)
    
    if result == SQLITE_OK
    {
        // Bind values to insert statement
        sqlite3_bind_text(insertStatement, 1, (family.childName! as NSString).utf8String, -1, nil)
        sqlite3_bind_text(insertStatement, 2, (family.childImage! as NSString).utf8String, -1, nil)
        sqlite3_bind_text(insertStatement, 3, (family.parentOneName! as NSString).utf8String, -1, nil)
        sqlite3_bind_text(insertStatement, 4, (family.parentOneImage! as NSString).utf8String, -1, nil)
        sqlite3_bind_text(insertStatement, 5, (family.parentTwoName! as NSString).utf8String, -1, nil)
        sqlite3_bind_text(insertStatement, 6, (family.parentTwoImage! as NSString).utf8String, -1, nil)
        
        // Execute statement
        let result = sqlite3_step(insertStatement)
        sqlite3_finalize(insertStatement)   // Destroy statement to avoid memory leak
        
        if result == SQLITE_DONE
        {
            os_log("Inserted family for %s", log: self.oslog, type: .info, family.childName!)
        }
        else
        {
            os_log("Could not insert family", log: self.oslog, type: .error)
            os_log("Expected: %s Received: %s", log: self.oslog, type: .error, SQLITE_DONE, result)
        }
    }
    else 
    {
        os_log("INSERT statement could not be prepared", log: self.oslog, type: .error)
        os_log("Expected: %s Received: %s", log: self.oslog, type: .error, SQLITE_DONE, result)
    }
}

Database read code for entire 'family':

    /**
Pull the family for the given childName if it exists

- Parameters
    - childName: The child's name associated with the family

- Returns: The family found
*/
func pullFamily(childName: String) -> Family?
{
    var family = Family()
    var readStatement: OpaquePointer? = nil
    let selectStatement = self.pullFamilyQuery + childName + "'"
    
    // Read
    if sqlite3_prepare_v2(self.db, selectStatement, -1, &readStatement, nil) == SQLITE_OK
    {
        if sqlite3_step(readStatement) == SQLITE_ROW
        {
            let childName = String(describing: String(cString: sqlite3_column_text(readStatement, 0)))
            let childImage = String(describing: String(cString: sqlite3_column_text(readStatement, 1)))
            let parentOneName = String(describing: String(cString: sqlite3_column_text(readStatement, 2)))
            let parentOneImage = String(describing: String(cString: sqlite3_column_text(readStatement, 3)))
            let parentTwoName = String(describing: String(cString: sqlite3_column_text(readStatement, 4)))
            let parentTwoImage = String(describing: String(cString: sqlite3_column_text(readStatement, 5)))
            
            family = Family(childName: childName, 
                                childImage: childImage, 
                                parentOneName: parentOneName, 
                                parentOneImage: parentOneImage, 
                                parentTwoName: parentTwoName, 
                                parentTwoImage: parentTwoImage)
        }
    }
    else 
    {
        os_log("Could not pull family by child name ", log: self.oslog, type: .error, childName)
    }
    
    sqlite3_finalize(readStatement) // Destroy statement to avoid memory leak
    return family
}

Family Initializer

init(childName: String?, childImage: UIImage?,
     parentOneName: String?, parentOneImage: UIImage?,
     parentTwoName: String?, parentTwoImage: UIImage?)
    {
        self.childName = childName
        self.childImage = childImage
        self.parentOneName = parentOneName
        self.parentOneImage = parentOneImage
        self.parentTwoName = parentTwoName
        self.parentTwoImage = parentTwoImage
    }

Test Code for Blob Write

// Child Image
result = (family.childImage!.unrotatedPngData()?.withUnsafeBytes { bufferPointer -> Int32 in
        sqlite3_bind_blob(insertStatement, 1, bufferPointer.baseAddress, Int32(bufferPointer.count), SQLITE_TRANSIENT) })!

guard result == SQLITE_OK else {
    logger.error("[\(#function); \(#line)] ERROR: Could not bind child image")
    logger.error("[\(#function); \(#line)] ERROR: Expected - \(SQLITE_OK), Received - \(result)")
    logger.error("[\(#function); \(#line)] [ERROR: \(self.getDatabaseError())")
    return

Test Code for Blob Read

while sqlite3_step(readStatement) == SQLITE_ROW
{
    guard
        let childName = sqlite3_column_text(readStatement, 0).flatMap({ String(cString: $0) }),
        let childImageBlob = sqlite3_column_blob(readStatement, 1)
        //let parentOneName = sqlite3_column_text(readStatement, 2).flatMap({ String(cString: $0) }),
        //let parentOneImageBlob = sqlite3_column_blob(readStatement, 3),
        //let parentTwoName = sqlite3_column_text(readStatement, 4).flatMap({ String(cString: $0) }),
        //let parentTwoImageBlob = sqlite3_column_blob(readStatement, 5)
    else {
        logger.error("[\(#function); \(#line)] Could not pull family")
        logger.error("\(String(cString: sqlite3_errmsg(self.db)))")
        return families
    }
            
    // Convert child image data to child image
    let childData = Data(bytes: childImageBlob, count: Int(sqlite3_column_bytes(readStatement, 1)))
    guard let childImage = UIImage(data: childData) else {
        logger.error("Could not convert child image for child name \(childName)")
        return families
    }

    ...
}
Nick08
  • 157
  • 1
  • 2
  • 10
  • 1
    I prefer to avoid converting Data to String altogether... – aheze Jan 14 '21 at 20:36
  • 2
    why would you want it convert to String in the first place ? – Chris Jan 14 '21 at 21:05
  • 1
    Just a guess, but something that "newbies" to CoreImage can get confused with is the the origin (value of (0,0)) is actually the *lower* left, not the *upper*. The behavior you have looks similar. –  Jan 14 '21 at 23:20
  • @Chris seemed an easier way to store it in a database – Nick08 Jan 15 '21 at 01:14
  • @dfd I'm unfamiliar with CoreImage but I'm not explicitly setting the origin myself. Is there a way to set that? – Nick08 Jan 15 '21 at 01:21
  • Please provide a complete reproducible example, including a link to the image file if needed. – matt Jan 15 '21 at 02:23
  • No. It was just a guess. I see you edited the question with some database storage code. New thought - have you tried eliminating using a database to single out the actual issue? What if you went UIImage -> String -> UIImage without storing the value? Yeah, I know that's not close to what you are want, but at least it narrows down what is working versus what isn't. –  Jan 15 '21 at 03:38
  • 1
    By the way, all of those `NSString` casts can be eliminated. E.g. `sqlite3_bind_text(insertStatement, 1, (family.childName! as NSString).utf8String, -1, nil)` can be simplified to `sqlite3_bind_text(insertStatement, 1, family.childName!, -1, nil)`. And I'd suggest not using `nil` for that last parameter, but rather `SQLITE_TRANSIENT` as defined [here](https://stackoverflow.com/questions/24102775/accessing-an-sqlite-database-in-swift/28642293#28642293). – Rob Mar 22 '21 at 03:45
  • Can you show your `Family` declaration? Also, given that your question was about the images, what precisely are the `parentOneImage` and `parentTwoImage`? Hopefully not base64-encoded string representations of the `pngData`... – Rob Mar 22 '21 at 03:48
  • So the images thus far that I'm messing with are just the stock ones from the simulator. – Nick08 Apr 14 '21 at 10:10

1 Answers1

2

The issue is not with the base-64 encoding to a string (nor the database). The problem is that pngData does not preserve image orientation. If I take a photo with my phone in portrait mode, the imageOrientation of the resulting UIImage is UIImage.Orientation.right and if I take it in landscape mode with the volume buttons at the top, the orientation is .down, etc. If I use those images directly, or grab their jpgData, they are fine. But if I extract the Data for those images using pngData, the resulting asset will be incorrectly rotated 90˚ or 180˚, respectively.

So you have a few options:

  1. You could use jpgData, which does honor the orientation of the image. While compression values like 0.7 or 0.8 look pretty good with decent compression, be aware that this is a lossy format.

  2. You can re-render the image (when necessary) before extracting the pngData. E.g. here is a UIImage extension to retrieve the unrotated PNG:

    extension UIImage {
        func unrotatedPngData() -> Data? {
            if imageOrientation == .up {
                return pngData()
            }
    
            let format = UIGraphicsImageRendererFormat()
            format.scale = scale
            return UIGraphicsImageRenderer(size: size, format: format).image { _ in
                draw(at: .zero)
            }.pngData()
        }
    }
    

It should be noted that there are other, more obscure, ways that image orientation can be messed up, but pngData is the likely culprit here. If you want to test this thesis, try jpgData, and if that works, it is indeed this pngData problem. If jpgData does not work either, though, then the problem rests earlier in your pipeline, but we would need to see that code in order to further diagnose it. But the smart money is on pngData. Regardless, neither the base-64 encoding nor the database is not the root of the problem.


That having been said, I would not advise base-64 encoding images in order to store them in a database. That is slower and increases the size of the saved asset by ⅓. I would just save the image as a BLOB:

var statement: OpaquePointer?

var result = sqlite3_prepare(db, "INSERT INTO images (filename, image) VALUES (?, ?)", -1, &statement, nil)

guard result == SQLITE_OK else {
    printError(in: db)
    return
}

defer { sqlite3_finalize(statement) }

result = sqlite3_bind_text(statement, 1, filename, -1, SQLITE_TRANSIENT)

guard result == SQLITE_OK else {
    printError(in: db)
    return
}

result = image.unrotatedPngData()?.withUnsafeBytes { bufferPointer -> Int32 in
    result = sqlite3_bind_blob(statement, 2, bufferPointer.baseAddress, Int32(bufferPointer.count), SQLITE_TRANSIENT)
}

guard result == SQLITE_OK else {
    printError(in: db)
    return
}

result = sqlite3_step(statement)

guard result == SQLITE_DONE else {
    printError(in: db)
    return
}

And you can fetch the image as a BLOB, too:

var statement: OpaquePointer?

let result = sqlite3_prepare(db, "SELECT filename, image FROM images", -1, &statement, nil)
guard result == SQLITE_OK else { ... }
defer { sqlite3_finalize(statement) }

while sqlite3_step(statement) == SQLITE_ROW {
    guard
        let filename = sqlite3_column_text(statement, 0).flatMap({ String(cString: $0) }),
        let bytes = sqlite3_column_blob(statement, 1)
    else {
        printError(in: db)
        return
    }

    let data = Data(bytes: bytes, count: Int(sqlite3_column_bytes(statement, 1)))
    guard let image = UIImage(data: data) else { ... }

    // do whatever you want with the image
}

Where

func printError() {
    let message = sqlite3_errmsg(db).flatMap { String(cString: $0) } ?? "Unknown error"
    print(message)
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I'm liking the idea for this. Was originally trying to store as a blob but couldn't find anything online until now. I updated my original post to include the full functions I'm currently using for storing and pulling data. How can I incorporate your methods into my existing code? – Nick08 Mar 19 '21 at 22:03
  • So for the withUnsafeBytes part, I'm getting the error 'Type of expression is ambiguous without more context,' which appears to be due to withUnsafeBytes being deprecated. I'm not sure the solutions I found from other posts are applicable to solving that error. – Nick08 Mar 22 '21 at 00:57
  • 1
    No, there are two renditions of `withUnsafeBytes`, the old, deprecated one that takes a `UnsafePointer`, and the new one that takes a `UnsafeRawBufferPointer`. Often, if you have any errors in your closure, Swift has a tough time doing type inference, and incorrectly assumes that you were trying to use the deprecated `UnsafePointer` rendition. You might try being explicit in your type declaration, e.g. `data.withUnsafeBytes { (bufferPointer: UnsafeRawBufferPointer) -> Int32 in ... }`. Or post your code at gist.github.com and I'm happy to take a look. But the above syntax is correct. – Rob Mar 22 '21 at 03:39
  • Ok so the issue was that I'm an idiot and forgot to correct the Family class and change the image properties, which were originally Strings, to Images. That fixed the ambiguity error. But still a bit confused on how to bind multiple images. Nested calls using the buffer pointer?? – Nick08 Apr 14 '21 at 10:16
  • 1
    No nesting needed. A separate `withUnsafeBytes` call for each blob column. I’ve adjusted my example to only wrap the binding of the blob in the `withUnsafeBytes` and that should make it clear how to repeat this binding for each image. – Rob Apr 14 '21 at 12:45
  • Alright progress, I think. But it's exiting the guard when attempting to read the blob. I updated my original post to include the code I'm testing right now. I've got a logger in the else statement but not really sure what the exact error is. printError isn't recognized. Is there something I need to import for that, or is that just supposed to be a placeholder for my actual logging? – Nick08 Apr 24 '21 at 09:12
  • The sqlite3_errmsg(self.db)) in the second logger statement just produces "another row available" which I think is a misdirect and not actually an issue, but I could be wrong – Nick08 Apr 24 '21 at 09:23
  • Re `printError` that's just a routine that prints `sqlite3_errmsg` (which I've added to my answer above). That safe unwrapping of `sqlite3_errmsg` is not something I like to litter around in my code, so I have one function that does that for me. But it's not terribly relevant to the broader questions here. Regardless, re the reading of the blob failing, first one calls `sqlite3_column_blob` on columns that were populated with `sqlite3_bind_blob` (which I don't see you doing in your code snippet). – Rob Apr 24 '21 at 17:23
  • On the basis of what described, it sounds like nothing was in the column in question. Try opening the SQLite database in some desktop tool (e.g. command line [`sqlite3`](https://sqlite.org/cli.html); [Base](https://apps.apple.com/us/app/base-sqlite-editor/id402383384?mt=12); [TablePlus](https://tableplus.com); etc.) and seeing what is in that column. – Rob Apr 24 '21 at 17:23
  • If you really are still struggling on storing and retrieving a blob, then post another question on that separate topic. – Rob Apr 24 '21 at 17:37
  • Yeah sorry, I just felt the issue was me using Strings to begin with instead of Blobs. I've got it working and so far the image that was originally giving me issues is no longer inverted. Thanks for all you help! – Nick08 Apr 25 '21 at 21:52