'Why do PDFs resized in SwiftUI getting sharp edges?

I try to include a pdf in my SwiftUI enabled app using Xcode 11.4 and iOS 13.4. However, when I resize the pdf, it gets crips edges. I have included two versions of the pdf: One large pdf (icon.pdf) and one small pdf (icon_small.pdf). When I resize icon.pdf it gets start edges, while icon_small.pdf gets smooth edges. The issue applies to all other pdfs I have tried as well.

enter image description here

This is my code:

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer()
            Text("icon.pdf:")
            Image("icon")
                .resizable()
                .renderingMode(.template)
                .aspectRatio(contentMode: .fit)
                .frame(width: 27.0, height: 27.0)
            Spacer()
            Text("icon_small.pdf:")
            Image("icon_small")
            Spacer()
        }
    }
}

Both icon.pdf and icon_small.pdf have the following asset settings:

  • Render As: Template Image
  • Resizing: Preserve Vector Data
  • Devices: Universal
  • Scales: Single Scale

The pdfs are available here:



Solution 1:[1]

PDF vectors needs to be programmatically resized via UIGraphicsBeginImageContextWithOptions so that they are not shown blurred when you scale them up (or down). There is no need to have multiple PDFs with different resolution to accomplish this.

Unfortunately this is not done automatically by UIKit or SwiftUI. Here is an example where a 24x24 PDF vector is tinted and resized to 200x200.

Image(uiImage: UIImage(named: "heart")!.tinted(withColor: .blue,
                                               biggerSize: CGSize(width: 200, height: 200)))
      .resizable()
      .frame(width: 200, height: 200,
             alignment: .center)
extension UIImage {

    /// Uses compositor blending to apply color to an image. When an image is too small it will be shown
    /// blurred. So you have to provide a size property to get a good resolution image
    public func tinted(withColor: UIColor?, biggerSize: CGSize = .zero) -> UIImage {
        guard let withColor = withColor else { return self }
        
        let size = biggerSize == .zero ? self.size : biggerSize
        let img2 = UIImage.createWithColor(size, color: withColor)
        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        let renderer = UIGraphicsImageRenderer(size: size)
        let result = renderer.image { _ in
            img2.draw(in: rect, blendMode: .normal, alpha: 1)
            draw(in: rect, blendMode: .destinationIn, alpha: 1)
        }
        return result
    }

    public static func createWithColor(_ size: CGSize, color: UIColor) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        let context = UIGraphicsGetCurrentContext()
        let rect = CGRect(size: size)
        color.setFill()
        context!.fill(rect)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image!
    }
}

Solution 2:[2]

There are essentially no differences between the two PDF files, other than the fact that in one case the co-ordinates of the content are scaled by a factor of ~14.2.

I would guess th difference is not in the PDF files, but in the rendering engine you are using to draw the contents. Note that the PDF file uses transparency (it has a constant alpha of 0.4) so the blending calculations might lead to slightly different results at the edges.

Looking at the two files in Adobe Acrobat, scaled to be the same size on screen, there is no visible difference between them.

Zooming in to your PNG file I see that icon_small.pdf has anti-aliased edges, while icon.pdf does not. You don't say what you are using to render the PDF files to a PNG but I think you're going to have to discuss it with the authors of whatever tool that is.

Solution 3:[3]

The same issue show up in this post: Xcode 11 PDF image assets "Preserve Vector Data" not working in SwiftUI?

UIKit solution from the link:

let uiImage = UIImage(named: "Logo-vector")!
var image: Image {
        Image(uiImage: uiImage.resized(to: CGSize(width: 500, height: 500)))
            .resizable()
}

var body: some View {
        NavigationView {
            VStack(alignment: .center, spacing: 8) {
                Spacer()
                Text("Logo vector SwiftUI")
                image
                    .frame(width: 240, height: 216)
                ...
                }
                ...
            }
        }
}

extension UIImage {
    func resized(to size: CGSize) -> UIImage {
        return UIGraphicsImageRenderer(size: size).image { _ in
            draw(in: CGRect(origin: .zero, size: size))
        }
    }
}

I put together a cross-platform framework called ResizableVector which performs the same resizing method:

extension PlatformImage {
    func resized(to size: CGSize) -> PlatformImage {
        return GraphicsImageRenderer(size: size).image { _ in
            self.draw(in: CGRect(origin: .zero, size: size))
        }
    }
}

Where PlatformImage is simply a typealias for UIImage or NSImage and GraphicsImageRenderer is a typealias for the UIGraphicsImageRenderer or MacGraphicsImageRenderer.

The framework provides a SwiftUI View named ResizableVector to use in place of Image. It can also respect the original aspect ratio, if desired.

You can add this using SPM - check it out on GitHub: https://github.com/Matt54/ResizableVector

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 KenS
Solution 3 Matt54