Strange Issue With Transparent PNG Files From iOS Photo Library

Viewed 783

I'm having a very strange issue with transparent PNG files, sourced from the Photos app.

The issue is that I am writing an app that allows the user to bring up an instance of UIImagePickerController, where they select an image, and that image is then added to a UIImageView via its image property.

Pretty straightforward, eh? The issue is when the image in the library is a transparent PNG.

For whatever reason, whenever I try to render the image, it always has the background white.

As far as I can tell, the image is stored in the library as a transparent PNG. When I drag it out, and examine it with an image editor, it's fine. Just what I expect.

But when I extract it programmatically, it has a white background. I can't seem to get it to be transparent.

Here's the code that I use to extract the image (It's a picker callback):

func imagePickerController(_ inPicker: UIImagePickerController, didFinishPickingMediaWithInfo inInfo: [UIImagePickerController.InfoKey: Any]) {
    let info = Dictionary(uniqueKeysWithValues: inInfo.map { key, value in (key.rawValue, value) })

    guard let image = (info[UIImagePickerController.InfoKey.editedImage.rawValue] as? UIImage ?? info[UIImagePickerController.InfoKey.originalImage.rawValue] as? UIImage)?.resizeThisImage(toNewWidth: Self.maximumImageWidthAndHeightInPixels) else { return }
    
    organization?.icon = image
    
    inPicker.dismiss(animated: true) { DispatchQueue.main.async { [weak self] in
        self?.imageButton?.image = image
        self?.imageButton?.alpha = 1.0
        self?.imageButton?.tintColor = self?.view.tintColor 
        self?.updateUI()
        }
    }
}

It's not actually a UIButton. It's a UIImageView, with an attached tap recognizer.

The resizeThisImage() method is in an extension that I wrote for UIImage. It works fine. I've been using it forever:

func resizeThisImage(toNewWidth inNewWidth: CGFloat? = nil, toNewHeight inNewHeight: CGFloat? = nil) -> UIImage? {
    guard nil == inNewWidth,
          nil == inNewHeight else {
        var scaleX: CGFloat = (inNewWidth ?? size.width) / size.width
        var scaleY: CGFloat = (inNewHeight ?? size.height) / size.height

        scaleX = nil == inNewWidth ? scaleY : scaleX
        scaleY = nil == inNewHeight ? scaleX : scaleY

        let destinationSize = CGSize(width: size.width * scaleX, height: size.height * scaleY)
        let destinationRect = CGRect(origin: .zero, size: destinationSize)

        UIGraphicsBeginImageContextWithOptions(destinationSize, false, 0)
        defer { UIGraphicsEndImageContext() }   // This makes sure that we get rid of the offscreen context.
        draw(in: destinationRect, blendMode: .normal, alpha: 1)
        return UIGraphicsGetImageFromCurrentImageContext()
    }
    
    return nil
}

In any case, it happens whether or not I use the resizeThisImage() method. That's not the issue.

Does anyone have any ideas what may be causing the issue?

UPDATE: I implemented @DonMag 's example, and here's what I got:

enter image description here

Note that the generated "A" is surrounded by white.

I should note that I'm using a classic storyboard UIKit app (no scene stuff). I don't think that should be an issue, but I'm happy to provide my little sample app. I don't think it's worth creating a GH repo for.

1 Answers

There doesn't seem to be anything wrong with your code, so I have to wonder if your images really, truly have transparency?

Here's a simple example to check. It looks like this when run:

enter image description here

The code creates Red and Blue image views, with .contentMode = .center.

Tapping the "Create" button will generate a UIImage using SF Symbol -- green with transparent background, the size of the Red image view -- and save it to Photos in PNG format with transparency.

Tapping the "Load" button will bring up the image picker. Selecting an image (such as the one just created and saved) will load the image and - using your extension - resize it to 80 x 80 and assign it to the .image property of the Blue image view.

As you can see, the image loaded from the Photo Picker still has its transparency.

Your UIImage extension for resizing

extension UIImage {
    func resizeThisImage(toNewWidth inNewWidth: CGFloat? = nil, toNewHeight inNewHeight: CGFloat? = nil) -> UIImage? {
        guard nil == inNewWidth,
              nil == inNewHeight else {
            var scaleX: CGFloat = (inNewWidth ?? size.width) / size.width
            var scaleY: CGFloat = (inNewHeight ?? size.height) / size.height
            
            scaleX = nil == inNewWidth ? scaleY : scaleX
            scaleY = nil == inNewHeight ? scaleX : scaleY
            
            let destinationSize = CGSize(width: size.width * scaleX, height: size.height * scaleY)
            let destinationRect = CGRect(origin: .zero, size: destinationSize)
            
            UIGraphicsBeginImageContextWithOptions(destinationSize, false, 0)
            defer { UIGraphicsEndImageContext() }   // This makes sure that we get rid of the offscreen context.
            draw(in: destinationRect, blendMode: .normal, alpha: 1)
            return UIGraphicsGetImageFromCurrentImageContext()
        }
        
        return nil
    }
}

UIImage extension to save to Photos in PNG format with transparency

extension UIImage {
    // save to Photos in PNG format with transparency
    func saveToPhotos(completion: @escaping (_ success:Bool) -> ()) {
        if let pngData = self.pngData() {
            PHPhotoLibrary.shared().performChanges({ () -> Void in
                let creationRequest = PHAssetCreationRequest.forAsset()
                let options = PHAssetResourceCreationOptions()
                creationRequest.addResource(with: PHAssetResourceType.photo, data: pngData, options: options)
            }, completionHandler: { (success, error) -> Void in
                if success == false {
                    if let errorString = error?.localizedDescription  {
                        print("Photo could not be saved: \(errorString))")
                    }
                    completion(false)
                } else {
                    print("Photo saved!")
                    completion(true)
                }
            })
        } else {
            completion(false)
        }
    }
}

Example view controller uses (essentially) your func imagePickerController for loading a photo

class TestImageViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    
    var imgViewA: UIImageView = UIImageView()
    var imgViewB: UIImageView = UIImageView()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let vStack = UIStackView()
        vStack.axis = .vertical
        vStack.spacing = 20
        
        let btnStack = UIStackView()
        btnStack.axis = .horizontal
        btnStack.distribution = .fillEqually
        btnStack.spacing = 20
        
        let btnCreate = UIButton()
        let btnLoad = UIButton()

        btnCreate.setTitle("Create", for: [])
        btnLoad.setTitle("Load", for: [])
        
        [btnCreate, btnLoad].forEach { b in
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.lightGray, for: .highlighted)
            b.backgroundColor = UIColor(red: 0.0, green: 0.5, blue: 0.75, alpha: 1.0)
            btnStack.addArrangedSubview(b)
        }
        
        vStack.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(vStack)
        
        [btnStack, imgViewA, imgViewB].forEach { v in
            vStack.addArrangedSubview(v)
        }
        
        [imgViewA, imgViewB].forEach { v in
            v.contentMode = .center
        }
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            vStack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            vStack.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            vStack.widthAnchor.constraint(equalToConstant: 200.0),
            
            imgViewA.heightAnchor.constraint(equalTo: imgViewA.widthAnchor),
            imgViewB.heightAnchor.constraint(equalTo: imgViewB.widthAnchor),
        ])
        
        imgViewA.backgroundColor = .red
        imgViewB.backgroundColor = .blue
        
        btnCreate.addTarget(self, action: #selector(self.createAndSave(_:)), for: .touchUpInside)
        btnLoad.addTarget(self, action: #selector(importPicture(_:)), for: .touchUpInside)
        
    }
    
    @objc func createAndSave(_ sender: Any) {
        
        let w = imgViewA.frame.width
        
        // create a Green image with transparent background
        if let img = drawSystemImage("a.circle.fill", at: 80, centeredIn: CGSize(width: w, height: w)) {
            imgViewA.image = img
            
            // save it to Photos in PNG format with transparency
            img.saveToPhotos { (success) in
                if success {
                    // image saved to photos
                    print("saved")
                }
                else {
                    // image not saved
                    fatalError("save failed")
                }
            }
        }
        
    }
    
    // create UIImage from SF Symbol system image
    //  at Point Size
    //  centered in CGSize
    // will draw symbol in Green on transparent background
    private func drawSystemImage(_ sysName: String, at pointSize: CGFloat, centeredIn size: CGSize) -> UIImage? {
        let cfg = UIImage.SymbolConfiguration(pointSize: pointSize)
        guard let img = UIImage(systemName: sysName, withConfiguration: cfg)?.withTintColor(.green, renderingMode: .alwaysOriginal) else { return nil }
        let x = (size.width - img.size.width) * 0.5
        let y = (size.height - img.size.height) * 0.5
        let renderer = UIGraphicsImageRenderer(size: size)
        return renderer.image { context in
            img.draw(in: CGRect(origin: CGPoint(x: x, y: y), size: img.size))
        }
    }
    
    @objc func importPicture(_ sender: Any) {
        let picker = UIImagePickerController()
        picker.allowsEditing = true
        picker.delegate = self
        present(picker, animated: true)
    }
    
    func imagePickerController(_ inPicker: UIImagePickerController, didFinishPickingMediaWithInfo inInfo: [UIImagePickerController.InfoKey: Any]) {
        let info = Dictionary(uniqueKeysWithValues: inInfo.map { key, value in (key.rawValue, value) })
        
        guard let image = (info[UIImagePickerController.InfoKey.editedImage.rawValue] as? UIImage ?? info[UIImagePickerController.InfoKey.originalImage.rawValue] as? UIImage)?.resizeThisImage(toNewWidth: 80) else { return }
        
        // organization?.icon = image
        
        inPicker.dismiss(animated: true) {
            DispatchQueue.main.async { [weak self] in
                self?.imgViewB.image = image
                //self?.imageButton?.image = image
                //self?.imageButton?.alpha = 1.0
                //self?.imageButton?.tintColor = self?.view.tintColor
                //self?.updateUI()
            }
        }
    }
}
Related