Swift 将 CGMutablePath 另存为 JSON 的最佳方式
Swift Best Way to Save CGMutablePath as JSON
将 CGMutablePath 保存为 JSON 可以上传到后端的数据的最佳方法是什么?
我知道可以使用符合 NSCoding 的 UIBezierPath
将其转换为 Data
对象,然后数据可以再次转换为字符串并保存到后端,但是似乎不是一个好方法。有没有更好的方法将这个对象保存到后端?
也许您可以提取构成路径的大量点,将其转换为字符串并保存。这样最好吗?
一个CGPath
或CGMutablePath
是一个非常简单的数据结构。它是一组路径元素。每个路径元素是 move、line、cubicCurve、curve 或 closeSubpath 操作与 0 到 3 个点。就这样。没有其他属性或变体。
因此,将路径转换为路径元素数组 (struct PathElement
) 然后将其编码为 JSON 非常简单。它导致 JSON 可以使用任何编程语言轻松阅读,并适用于许多图形系统(包括 iOS/macOS Quartz、Postscript、PDF、Windows GDI+)。
下面示例代码的输出包括打印的CGMutablePath
、生成的JSON和从JSON恢复的路径:
Path 0x10100d960:
moveto (10, 10)
lineto (30, 30)
quadto (100, 100) (200, 200)
curveto (150, 120) (100, 350) (20, 400)
closepath
moveto (200, 200)
lineto (230, 230)
lineto (260, 210)
closepath
[
{
"type" : 0,
"points" : [
[
10,
10
]
]
},
{
"type" : 1,
"points" : [
[
30,
30
]
]
},
{
"type" : 2,
"points" : [
[
100,
100
],
[
200,
200
]
]
},
{
"type" : 3,
"points" : [
[
150,
120
],
[
100,
350
],
[
20,
400
]
]
},
{
"type" : 4
},
{
"type" : 0,
"points" : [
[
200,
200
]
]
},
{
"type" : 1,
"points" : [
[
230,
230
]
]
},
{
"type" : 1,
"points" : [
[
260,
210
]
]
},
{
"type" : 4
}
]
Path 0x10100bd20:
moveto (10, 10)
lineto (30, 30)
quadto (100, 100) (200, 200)
curveto (150, 120) (100, 350) (20, 400)
closepath
moveto (200, 200)
lineto (230, 230)
lineto (260, 210)
closepath
示例代码:
import Foundation
var path = CGMutablePath()
path.move(to: CGPoint(x: 10, y: 10))
path.addLine(to: CGPoint(x: 30, y: 30))
path.addQuadCurve(to: CGPoint(x: 200, y: 200), control: CGPoint(x: 100, y: 100))
path.addCurve(to: CGPoint(x: 20, y: 400), control1: CGPoint(x: 150, y: 120), control2: CGPoint(x: 100, y: 350))
path.closeSubpath()
path.move(to: CGPoint(x: 200, y: 200))
path.addLine(to: CGPoint(x: 230, y: 230))
path.addLine(to: CGPoint(x: 260, y: 210))
path.closeSubpath()
print(path)
let jsonData = encode(path: path)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
print("")
let restoredPath = try! decode(data: jsonData)
print(restoredPath)
func encode(path: CGPath) -> Data {
var elements = [PathElement]()
path.applyWithBlock() { elem in
let elementType = elem.pointee.type
let n = numPoints(forType: elementType)
var points: Array<CGPoint>?
if n > 0 {
points = Array(UnsafeBufferPointer(start: elem.pointee.points, count: n))
}
elements.append(PathElement(type: Int(elementType.rawValue), points: points))
}
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return try encoder.encode(elements)
} catch {
return Data()
}
}
func decode(data: Data) throws -> CGPath {
let decoder = JSONDecoder()
let elements = try decoder.decode([PathElement].self, from: data)
let path = CGMutablePath()
for elem in elements {
switch elem.type {
case 0:
path.move(to: elem.points![0])
case 1:
path.addLine(to: elem.points![0])
case 2:
path.addQuadCurve(to: elem.points![1], control: elem.points![0])
case 3:
path.addCurve(to: elem.points![2], control1: elem.points![0], control2: elem.points![1])
case 4:
path.closeSubpath()
default:
break
}
}
return path
}
func numPoints(forType type: CGPathElementType) -> Int
{
var n = 0
switch type {
case .moveToPoint:
n = 1
case .addLineToPoint:
n = 1
case .addQuadCurveToPoint:
n = 2
case .addCurveToPoint:
n = 3
case .closeSubpath:
n = 0
default:
n = 0
}
return n
}
struct PathElement: Codable {
var type: Int
var points: Array<CGPoint>?
}
“最佳”完全是主观的,完全取决于您的需求和优先级。人类可读性重要吗?数据大小重要吗?与某处现有 API 的兼容性?您需要回答这些问题才能确定适合您的用例的内容。
如果数据的人类可读性很重要,那么 JSON 明显优于 NSCoding
。如果大小很重要,那么 compressed JSON 通常比 NSKeyedArchiver
数据小,但解压缩后也可以大得多。
用于生成一些随机路径并从中生成数据的小型(非生产就绪)测试工具:
import Foundation
import GameplayKit
import UIKit
extension GKRandom {
func nextCGFloat(upperBound: CGFloat) -> CGFloat {
CGFloat(nextUniform()) * upperBound
}
func nextCGPoint(scale: CGFloat = 100) -> CGPoint {
CGPoint(x: nextCGFloat(upperBound: scale),
y: nextCGFloat(upperBound: scale))
}
func nextCGRect(widthScale: CGFloat = 100, heightScale: CGFloat = 100) -> CGRect {
CGRect(origin: nextCGPoint(),
size: CGSize(width: 4 + nextCGFloat(upperBound: widthScale - 4),
height: 4 + nextCGFloat(upperBound: heightScale - 4)))
}
func nextCGPath() -> CGMutablePath {
let path = CGMutablePath()
path.move(to: nextCGPoint())
let minPathElements = 3
for _ in minPathElements ..< minPathElements + nextInt(upperBound: 10) {
switch nextInt(upperBound: 6) {
case 0:
path.addArc(center: nextCGPoint(),
radius: nextCGFloat(upperBound: 20),
startAngle: nextCGFloat(upperBound: CGFloat(2 * Double.pi)),
endAngle: nextCGFloat(upperBound: CGFloat(2 * Double.pi)),
clockwise: nextBool())
case 1:
path.addCurve(to: nextCGPoint(),
control1: nextCGPoint(),
control2: nextCGPoint())
case 2:
path.addEllipse(in: nextCGRect())
case 3:
path.addLine(to: nextCGPoint())
case 4:
path.addQuadCurve(to: nextCGPoint(),
control: nextCGPoint())
case 5:
path.addRect(nextCGRect())
default:
continue
}
}
path.closeSubpath()
return path
}
}
func encodePathWithArchiver(_ path: CGMutablePath) -> Data {
let bezierPath = UIBezierPath(cgPath: path)
return try! NSKeyedArchiver.archivedData(withRootObject: bezierPath, requiringSecureCoding: true)
}
extension CGPathElement: Encodable {
private enum CodingKeys: CodingKey {
case type
case points
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type.rawValue, forKey: .type)
let pointCount: Int
switch type {
case .moveToPoint, .addLineToPoint, .addQuadCurveToPoint:
pointCount = 2
case .addCurveToPoint:
pointCount = 3
case .closeSubpath: fallthrough
@unknown default:
pointCount = 0
}
try container.encode(Array(UnsafeBufferPointer(start: points, count: pointCount)), forKey: .points)
}
}
func encodePathAsJSON(_ path: CGMutablePath) -> Data {
var elements = [CGPathElement]()
path.applyWithBlock { element in
elements.append(element.pointee)
}
return try! JSONEncoder().encode(elements)
}
let randomSource = GKMersenneTwisterRandomSource(seed: 0 /* some reproducible seed here, or omit for a random run each time */)
let path = randomSource.nextCGPath()
let keyedArchiverData = encodePathWithArchiver(path)
let jsonData = encodePathAsJSON(path)
print(keyedArchiverData.count, "<=>", jsonData.count)
let compressedArchiverData = try! (keyedArchiverData as NSData).compressed(using: .lzma) as Data
let compressedJSONData = try! (jsonData as NSData).compressed(using: .lzma) as Data
print(compressedArchiverData.count, "<=>", compressedJSONData.count)
以上代码使用 GameplayKit 使用种子实现可重现的随机性:您可以尝试使用它来查看各种结果。例如,0
的种子生成 JSON 数据,在压缩和未压缩时都小于 NSKeyedArchiver
数据,但是 14283523348572255252
的种子生成 JSON压缩前数据大于 NSKeyedArchiver
数据的 2 倍。
这里的要点在很大程度上取决于您的具体用例,以及您的数据存储优先级。
注意:很容易看到这里的小数字并尝试得出什么是“最佳”的结论,但请记住这里的比例:除非你的路径有数千个点长, effective 这些方法之间的差异导致大小差异可以忽略不计。无论您是否努力维护 CGPath
引用的编码接口,或者是否将单行转换为 UIBezierPath
对您来说可能比任何大小节省都更重要。
将 CGMutablePath 保存为 JSON 可以上传到后端的数据的最佳方法是什么?
我知道可以使用符合 NSCoding 的 UIBezierPath
将其转换为 Data
对象,然后数据可以再次转换为字符串并保存到后端,但是似乎不是一个好方法。有没有更好的方法将这个对象保存到后端?
也许您可以提取构成路径的大量点,将其转换为字符串并保存。这样最好吗?
一个CGPath
或CGMutablePath
是一个非常简单的数据结构。它是一组路径元素。每个路径元素是 move、line、cubicCurve、curve 或 closeSubpath 操作与 0 到 3 个点。就这样。没有其他属性或变体。
因此,将路径转换为路径元素数组 (struct PathElement
) 然后将其编码为 JSON 非常简单。它导致 JSON 可以使用任何编程语言轻松阅读,并适用于许多图形系统(包括 iOS/macOS Quartz、Postscript、PDF、Windows GDI+)。
下面示例代码的输出包括打印的CGMutablePath
、生成的JSON和从JSON恢复的路径:
Path 0x10100d960:
moveto (10, 10)
lineto (30, 30)
quadto (100, 100) (200, 200)
curveto (150, 120) (100, 350) (20, 400)
closepath
moveto (200, 200)
lineto (230, 230)
lineto (260, 210)
closepath
[
{
"type" : 0,
"points" : [
[
10,
10
]
]
},
{
"type" : 1,
"points" : [
[
30,
30
]
]
},
{
"type" : 2,
"points" : [
[
100,
100
],
[
200,
200
]
]
},
{
"type" : 3,
"points" : [
[
150,
120
],
[
100,
350
],
[
20,
400
]
]
},
{
"type" : 4
},
{
"type" : 0,
"points" : [
[
200,
200
]
]
},
{
"type" : 1,
"points" : [
[
230,
230
]
]
},
{
"type" : 1,
"points" : [
[
260,
210
]
]
},
{
"type" : 4
}
]
Path 0x10100bd20:
moveto (10, 10)
lineto (30, 30)
quadto (100, 100) (200, 200)
curveto (150, 120) (100, 350) (20, 400)
closepath
moveto (200, 200)
lineto (230, 230)
lineto (260, 210)
closepath
示例代码:
import Foundation
var path = CGMutablePath()
path.move(to: CGPoint(x: 10, y: 10))
path.addLine(to: CGPoint(x: 30, y: 30))
path.addQuadCurve(to: CGPoint(x: 200, y: 200), control: CGPoint(x: 100, y: 100))
path.addCurve(to: CGPoint(x: 20, y: 400), control1: CGPoint(x: 150, y: 120), control2: CGPoint(x: 100, y: 350))
path.closeSubpath()
path.move(to: CGPoint(x: 200, y: 200))
path.addLine(to: CGPoint(x: 230, y: 230))
path.addLine(to: CGPoint(x: 260, y: 210))
path.closeSubpath()
print(path)
let jsonData = encode(path: path)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
print("")
let restoredPath = try! decode(data: jsonData)
print(restoredPath)
func encode(path: CGPath) -> Data {
var elements = [PathElement]()
path.applyWithBlock() { elem in
let elementType = elem.pointee.type
let n = numPoints(forType: elementType)
var points: Array<CGPoint>?
if n > 0 {
points = Array(UnsafeBufferPointer(start: elem.pointee.points, count: n))
}
elements.append(PathElement(type: Int(elementType.rawValue), points: points))
}
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return try encoder.encode(elements)
} catch {
return Data()
}
}
func decode(data: Data) throws -> CGPath {
let decoder = JSONDecoder()
let elements = try decoder.decode([PathElement].self, from: data)
let path = CGMutablePath()
for elem in elements {
switch elem.type {
case 0:
path.move(to: elem.points![0])
case 1:
path.addLine(to: elem.points![0])
case 2:
path.addQuadCurve(to: elem.points![1], control: elem.points![0])
case 3:
path.addCurve(to: elem.points![2], control1: elem.points![0], control2: elem.points![1])
case 4:
path.closeSubpath()
default:
break
}
}
return path
}
func numPoints(forType type: CGPathElementType) -> Int
{
var n = 0
switch type {
case .moveToPoint:
n = 1
case .addLineToPoint:
n = 1
case .addQuadCurveToPoint:
n = 2
case .addCurveToPoint:
n = 3
case .closeSubpath:
n = 0
default:
n = 0
}
return n
}
struct PathElement: Codable {
var type: Int
var points: Array<CGPoint>?
}
“最佳”完全是主观的,完全取决于您的需求和优先级。人类可读性重要吗?数据大小重要吗?与某处现有 API 的兼容性?您需要回答这些问题才能确定适合您的用例的内容。
如果数据的人类可读性很重要,那么 JSON 明显优于 NSCoding
。如果大小很重要,那么 compressed JSON 通常比 NSKeyedArchiver
数据小,但解压缩后也可以大得多。
用于生成一些随机路径并从中生成数据的小型(非生产就绪)测试工具:
import Foundation
import GameplayKit
import UIKit
extension GKRandom {
func nextCGFloat(upperBound: CGFloat) -> CGFloat {
CGFloat(nextUniform()) * upperBound
}
func nextCGPoint(scale: CGFloat = 100) -> CGPoint {
CGPoint(x: nextCGFloat(upperBound: scale),
y: nextCGFloat(upperBound: scale))
}
func nextCGRect(widthScale: CGFloat = 100, heightScale: CGFloat = 100) -> CGRect {
CGRect(origin: nextCGPoint(),
size: CGSize(width: 4 + nextCGFloat(upperBound: widthScale - 4),
height: 4 + nextCGFloat(upperBound: heightScale - 4)))
}
func nextCGPath() -> CGMutablePath {
let path = CGMutablePath()
path.move(to: nextCGPoint())
let minPathElements = 3
for _ in minPathElements ..< minPathElements + nextInt(upperBound: 10) {
switch nextInt(upperBound: 6) {
case 0:
path.addArc(center: nextCGPoint(),
radius: nextCGFloat(upperBound: 20),
startAngle: nextCGFloat(upperBound: CGFloat(2 * Double.pi)),
endAngle: nextCGFloat(upperBound: CGFloat(2 * Double.pi)),
clockwise: nextBool())
case 1:
path.addCurve(to: nextCGPoint(),
control1: nextCGPoint(),
control2: nextCGPoint())
case 2:
path.addEllipse(in: nextCGRect())
case 3:
path.addLine(to: nextCGPoint())
case 4:
path.addQuadCurve(to: nextCGPoint(),
control: nextCGPoint())
case 5:
path.addRect(nextCGRect())
default:
continue
}
}
path.closeSubpath()
return path
}
}
func encodePathWithArchiver(_ path: CGMutablePath) -> Data {
let bezierPath = UIBezierPath(cgPath: path)
return try! NSKeyedArchiver.archivedData(withRootObject: bezierPath, requiringSecureCoding: true)
}
extension CGPathElement: Encodable {
private enum CodingKeys: CodingKey {
case type
case points
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type.rawValue, forKey: .type)
let pointCount: Int
switch type {
case .moveToPoint, .addLineToPoint, .addQuadCurveToPoint:
pointCount = 2
case .addCurveToPoint:
pointCount = 3
case .closeSubpath: fallthrough
@unknown default:
pointCount = 0
}
try container.encode(Array(UnsafeBufferPointer(start: points, count: pointCount)), forKey: .points)
}
}
func encodePathAsJSON(_ path: CGMutablePath) -> Data {
var elements = [CGPathElement]()
path.applyWithBlock { element in
elements.append(element.pointee)
}
return try! JSONEncoder().encode(elements)
}
let randomSource = GKMersenneTwisterRandomSource(seed: 0 /* some reproducible seed here, or omit for a random run each time */)
let path = randomSource.nextCGPath()
let keyedArchiverData = encodePathWithArchiver(path)
let jsonData = encodePathAsJSON(path)
print(keyedArchiverData.count, "<=>", jsonData.count)
let compressedArchiverData = try! (keyedArchiverData as NSData).compressed(using: .lzma) as Data
let compressedJSONData = try! (jsonData as NSData).compressed(using: .lzma) as Data
print(compressedArchiverData.count, "<=>", compressedJSONData.count)
以上代码使用 GameplayKit 使用种子实现可重现的随机性:您可以尝试使用它来查看各种结果。例如,0
的种子生成 JSON 数据,在压缩和未压缩时都小于 NSKeyedArchiver
数据,但是 14283523348572255252
的种子生成 JSON压缩前数据大于 NSKeyedArchiver
数据的 2 倍。
这里的要点在很大程度上取决于您的具体用例,以及您的数据存储优先级。
注意:很容易看到这里的小数字并尝试得出什么是“最佳”的结论,但请记住这里的比例:除非你的路径有数千个点长, effective 这些方法之间的差异导致大小差异可以忽略不计。无论您是否努力维护 CGPath
引用的编码接口,或者是否将单行转换为 UIBezierPath
对您来说可能比任何大小节省都更重要。