为什么在SwiftUI中调整大小的PDF会有锐利的边缘?

17

我试图在使用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文件在此处提供:


既然它是矢量图像,为什么需要 icon.pdficon_small.pdf?我的第一猜测是 icon.pdf 出了问题。直接使用 icon_small.pdf 适用于所有尺寸。它是矢量图像,会根据需要进行缩放。 - staticVoidMan
1
两者都是矢量图形(pdf)。我包含它们是为了说明 SwiftUI 调整大小后 pdf 可以获得锐利的边缘。因此,在将它们包含到我的项目之前,我必须手动调整它们的大小。然而,据我所知,SwiftUI 应该能够在调整图像大小时避免边缘变得锐利。 - Simen
嗯...我尝试了一张大的矢量图像并调整了大小,但它变得有像素了。这可能是SwiftUI中矢量图像支持的问题。让我们看看其他人有什么说法。 - staticVoidMan
在 macOS 上也是 True - Ryan
似乎它仍然没有按预期工作。你在某个地方报告了这个问题吗? - mallow
其实,我不记得 @mallow 了。但我认为在将 PDF 导入 Xcode 之前,我最终会将它们缩放到正确的大小。 - Simen
4个回答

43

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

起初,我使用了SwiftUI内置的Image,但正如之前提到的,两者在极端情况下表现不佳:

  • 当缩小大图像时,边缘变得锐利
  • 当放大小图像时,变得模糊

起初我以为可能是你的pdf矢量图像有问题,所以我使用了我之前项目中表现良好的矢量图像,但问题依旧。
认为这可能是UIImage的问题,我使用了SwiftUIImage(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
    }
  }
}

这将丢失一些 SwiftUIImage修饰符(您仍然拥有普通的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($0) }

  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. 上排右边:在SwiftUI Image中显示小图像
  3. 下排左边:在UIImageView中显示大图像
  4. 下排右边:在SwiftUI Image中显示大图像

UIKitUIImageView表现一致,而SwiftUIImage存在问题。

20x20


60x60


100x100


180x180


感谢@staticVoidMan分享这个,总结得很好,解决方法对我帮助很大。 - Patrick

2

这篇文章中出现了相同的问题: Xcode 11 PDF 图像资源“保留矢量数据”在 SwiftUI 中无效?

链接中的 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的跨平台框架,它执行相同的调整大小方法。
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 的 typealias,而 GraphicsImageRenderer 是 UIGraphicsImageRenderer 或 MacGraphicsImageRenderer 的 typealias。

该框架提供了一个名为 ResizableVector 的 SwiftUI 视图,可用于替换 Image。如果需要,它还可以保持原始纵横比。

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


2

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

0

这两个PDF文件本质上没有什么区别,除了一个事实,即在某种情况下,内容的坐标缩放了约14.2倍。

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

在Adobe Acrobat中查看两个文件,缩放到屏幕上相同的大小,它们之间没有可见的区别。

放大您的PNG文件后,我发现icon_small.pdf具有反锯齿边缘,而icon.pdf则没有。您没有说明用于将PDF文件呈现为PNG的工具,但我认为您需要与该工具的作者讨论。


两个图像都是pdf格式的 - 这里没有png。该pdf最初是使用CairoSVG转换为pdf的svg:cairosvg icon.svg -o icon.pdficon_small.pdf是使用cairosvg icon.svg -o icon_small.pdf --output-width 36 --output-height 36创建的。 - Simen
我指的是你问题中提到的那个PNG文件,其中展示了(我猜测)“锐利边缘”的效果。在相同大小下使用Acrobat渲染时,两个PDF文件完全相同,并且它们的内容也相同(除了比例缩放)。PNG文件中还有其他细节(手机之类的东西?),我猜它是一张截图。将这两个PDF文件(它们不是位图图片)在屏幕上呈现的工具对其进行了不同的渲染,可能是因为其中一个是另一个的14倍,但它们在相同的屏幕区域中显示。问题在于渲染而不是PDF文件本身。 - KenS
啊,我明白了。PNG只是来自iPhone 8模拟器的截图。但在我的iPhone 8上也发生了同样的情况。我同意PDF没有任何问题。因此,问题(和解决方案)可能在SwiftUI中。 - Simen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接