为什么在 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.pdf
和 icon_small.pdf
都有以下资产设置:
- 渲染为:模板图像
- 调整大小:保留矢量数据
- 设备:通用
- 音阶:单音阶
pdf 可在此处获得:
这两个 PDF 文件之间基本上没有区别,除了在一种情况下内容的坐标缩放了 ~14.2 倍之外。
我猜想区别不在于 PDF 文件,而在于用于绘制内容的渲染引擎。请注意,PDF 文件使用透明度(它具有 0.4 的常数 alpha),因此混合计算可能会导致边缘处的结果略有不同。
在 Adobe Acrobat 中查看这两个文件,在屏幕上缩放为相同大小,它们之间没有明显差异。
放大你的 PNG 文件,我看到 icon_small.pdf 有消除锯齿的边缘,而 icon.pdf 没有。您没有说您使用什么将 PDF 文件呈现为 PNG,但我认为您将不得不与任何工具的作者讨论它。
我使用您提供的图像对两个矢量图像进行了并排比较:
起初,我使用了 SwiftUI
的内置 Image
并且如前所述,两者在极端情况下都表现不佳:
- 大图在缩小时边缘锐利
- 小图片在放大时变得模糊
起初我以为它可能是你的 pdf 向量,所以我使用了我知道在我以前的项目中运行良好的向量,但我遇到了同样的问题。
认为这是一个 UIImage
问题,我使用 SwiftUI
s 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
修饰符)但是你总是可以传入一些参数,例如 contentMode
和 tintColor
如上所示。如果需要添加更多并相应地处理。
用法示例:
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
。
以下示例是 UIKit
的 UIImageView
和 SwiftUI
的 Image
中渲染的小型和大型矢量图像的简单并排比较。
比较:
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)
}
}
}
}
}
}
}
结果:
- 顶行;左:
UIImageView
中的小图像
- 顶行;右:
SwiftUI
Image
中的小图片
- 最后一行;左:
UIImageView
中的大图
- 最后一行;右:
SwiftUI
Image
中的大图
UIKit
的 UIImageView
性能稳定,而 SwiftUI
的 Image
有问题。
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
我尝试使用 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.pdf
和 icon_small.pdf
都有以下资产设置:
- 渲染为:模板图像
- 调整大小:保留矢量数据
- 设备:通用
- 音阶:单音阶
pdf 可在此处获得:
这两个 PDF 文件之间基本上没有区别,除了在一种情况下内容的坐标缩放了 ~14.2 倍之外。
我猜想区别不在于 PDF 文件,而在于用于绘制内容的渲染引擎。请注意,PDF 文件使用透明度(它具有 0.4 的常数 alpha),因此混合计算可能会导致边缘处的结果略有不同。
在 Adobe Acrobat 中查看这两个文件,在屏幕上缩放为相同大小,它们之间没有明显差异。
放大你的 PNG 文件,我看到 icon_small.pdf 有消除锯齿的边缘,而 icon.pdf 没有。您没有说您使用什么将 PDF 文件呈现为 PNG,但我认为您将不得不与任何工具的作者讨论它。
我使用您提供的图像对两个矢量图像进行了并排比较:
起初,我使用了 SwiftUI
的内置 Image
并且如前所述,两者在极端情况下都表现不佳:
- 大图在缩小时边缘锐利
- 小图片在放大时变得模糊
起初我以为它可能是你的 pdf 向量,所以我使用了我知道在我以前的项目中运行良好的向量,但我遇到了同样的问题。
认为这是一个 UIImage
问题,我使用 SwiftUI
s 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
修饰符)但是你总是可以传入一些参数,例如 contentMode
和 tintColor
如上所示。如果需要添加更多并相应地处理。
用法示例:
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
。
以下示例是 UIKit
的 UIImageView
和 SwiftUI
的 Image
中渲染的小型和大型矢量图像的简单并排比较。
比较:
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)
}
}
}
}
}
}
}
结果:
- 顶行;左:
UIImageView
中的小图像
- 顶行;右:
SwiftUI
Image
中的小图片
- 最后一行;左:
UIImageView
中的大图
- 最后一行;右:
SwiftUI
Image
中的大图
UIKit
的 UIImageView
性能稳定,而 SwiftUI
的 Image
有问题。
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