为什么在 SwiftUI 中调整大小的 PDF 会出现锐边?

Why do PDFs resized in SwiftUI getting sharp edges?

我尝试使用 Xcode 11.4 和 iOS 13.4 在我启用 SwiftUI 的应用程序中包含一个 pdf。但是,当我调整 pdf 大小时,它的边缘会变脆。我提供了两个版本的 pdf:一个大 pdf (icon.pdf) 和一个小 pdf (icon_small.pdf)。当我调整 icon.pdf 的大小时,它会得到起始边缘,而 icon_small.pdf 会得到平滑的边缘。该问题也适用于我尝试过的所有其他 pdf。

这是我的代码:

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()
        }
    }
}

icon.pdficon_small.pdf 都有以下资产设置:

pdf 可在此处获得:

这两个 PDF 文件之间基本上没有区别,除了在一种情况下内容的坐标缩放了 ~14.2 倍之外。

我猜想区别不在于 PDF 文件,而在于用于绘制内容的渲染引擎。请注意,PDF 文件使用透明度(它具有 0.4 的常数 alpha),因此混合计算可能会导致边缘处的结果略有不同。

在 Adob​​e Acrobat 中查看这两个文件,在屏幕上缩放为相同大小,它们之间没有明显差异。

放大你的 PNG 文件,我看到 icon_small.pdf 有消除锯齿的边缘,而 icon.pdf 没有。您没有说您使用什么将 PDF 文件呈现为 PNG,但我认为您将不得不与任何工具的作者讨论它。

我使用您提供的图像对两个矢量图像进行了并排比较:

起初,我使用了 SwiftUI 的内置 Image 并且如前所述,两者在极端情况下都表现不佳:

  • 大图在缩小时边缘锐利
  • 小图片在放大时变得模糊

起初我以为它可能是你的 pdf 向量,所以我使用了我知道在我以前的项目中运行良好的向量,但我遇到了同样的问题。
认为这是一个 UIImage 问题,我使用 SwiftUIs Image(uiImage:) 但同样的问题。

最后猜测是图像容器,并且知道 UIImageView 已经很好地处理了矢量图像,让 UIViewRepresentable 包裹 UIImageView 似乎可以解决这个问题。现在它看起来像是一个可能的解决方法。

解决方法:

struct MyImageView: UIViewRepresentable {
  var name: String
  var contentMode: UIView.ContentMode = .scaleAspectFit
  var tintColor: UIColor = .black

  func makeUIView(context: Context) -> UIImageView {
    let imageView = UIImageView()
    imageView.setContentCompressionResistancePriority(.fittingSizeLevel, 
                                                      for: .vertical)
    return imageView
  }

  func updateUIView(_ uiView: UIImageView, context: Context) {
    uiView.contentMode = contentMode
    uiView.tintColor = tintColor
    if let image = UIImage(named: name) {
      uiView.image = image
    }
  }
}

这会失去一些 SwiftUI Image 修饰符(你仍然有正常的 View 修饰符)但是你总是可以传入一些参数,例如 contentModetintColor 如上所示。如果需要添加更多并相应地处理。


用法示例:

struct ContentView: View {
  var body: some View {
    VStack {
      MyImageView(name: "icon", //REQUIRED
                  contentMode: .scaleAspectFit, //OPTIONAL
                  tintColor: .black /*OPTIONAL*/)
        .frame(width: 27, height: 27)
      MyImageView(name: "icon_small", //REQUIRED
                  contentMode: .scaleAspectFit, //OPTIONAL
                  tintColor: .black /*OPTIONAL*/)
        .frame(width: 27, height: 27)
    }
  }
}

现在这都是猜测,但看起来 SwiftUI 将矢量图像视为 PNG

以下示例是 UIKitUIImageViewSwiftUIImage 中渲染的小型和大型矢量图像的简单并排比较。

比较:

struct ContentView: View {
  let (largeImage, smallImage) = ("icon", "icon_small")
  let range = stride(from: 20, to: 320, by: 40).map { CGFloat([=12=]) }

  var body: some View {
    List(range, id: \.self) { (side) in
      ScrollView(.horizontal) {
        VStack(alignment: .leading) {
          Text(String(format: "%gx%g", side, side))
          HStack {
            VStack {
              Text("UIKit")
              MyImageView(name: self.smallImage)
                .frame(width: side, height: side)
              MyImageView(name: self.largeImage)
                .frame(width: side, height: side)
            }
            VStack {
              Text("SwiftUI")
              Image(self.smallImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: side)
              Image(self.largeImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: side)
            }
          }
        }
      }
    }
  }
}

结果:

  1. 顶行;左:UIImageView
  2. 中的小图像
  3. 顶行;右:SwiftUI Image
  4. 中的小图片
  5. 最后一行;左:UIImageView
  6. 中的大图
  7. 最后一行;右:SwiftUI Image
  8. 中的大图

UIKitUIImageView 性能稳定,而 SwiftUIImage 有问题。




PDF 矢量需要通过 UIGraphicsBeginImageContextWithOptions 以编程方式调整大小,以便在放大(或缩小)时它们不会显示模糊。无需具有不同分辨率的多个 PDF 即可完成此操作。

不幸的是,这不是由 UIKit 或 SwiftUI 自动完成的。这是一个示例,其中 24x24 PDF 矢量被着色并调整为 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!
    }
}

此 post 中出现了同样的问题: Xcode 11 PDF image assets "Preserve Vector Data" not working in SwiftUI?

来自 link 的 UIKit 解决方案:

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))
        }
    }
}

我组装了一个名为 ResizableVector 的 cross-platform 框架,它执行相同的调整大小方法:

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

其中 PlatformImage 只是 UIImage 或 NSImage 的类型别名,而 GraphicsImageRenderer 是 UIGraphicsImageRenderer 或 MacGraphicsImageRenderer 的类型别名。

框架提供了一个名为 ResizableVector 的 SwiftUI View 来代替 Image。如果需要,它还可以尊重原始纵横比。

您可以使用 SPM 添加它 - 在 GitHub 上查看: https://github.com/Matt54/ResizableVector