【问题标题】:Swift Best Way to Save CGMutablePath as JSON将 CGMutablePath 保存为 JSON 的 Swift 最佳方法
【发布时间】:2021-12-27 12:19:37
【问题描述】:

将 CGMutablePath 保存为可上传到后端的 JSON 数据的最佳方法是什么?

我知道可以使用符合 NSCoding 的 UIBezierPath 将其转换为 Data 对象,然后可以将数据再次转换为字符串并保存到后端,但这看起来不像好方法。有没有更好的方法将此对象保存到后端?

也许您可以提取构成路径的大量点,将其转换为字符串并保存。这样最好吗?

【问题讨论】:

  • “最佳方式”非常含糊。将其保存为 Data 就足够了,但对您的用例有好处吗?我们不知道,因为我们不知道您要做什么。
  • @EmilioPelaez 通常,将原始数据存储在后端不是您想要做的。与后端的可读 json 数据相比,压缩原始 blob 数据的选项要少得多。
  • 我同意,但我们不知道您如何使用CGMutablePath,因此我们无法为您提供好的答案。我可以告诉你最好存储点,但也许你还需要支持曲线、圆弧等。
  • 它是用来沿着路径绘制的,所以需要曲线等

标签: swift cgpath


【解决方案1】:

CGPathCGMutablePath 是一个非常简单的数据结构。它是一个路径元素数组。每个路径元素是 movelinecubicCurvecurvecloseSubpath 操作 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>?
}

【讨论】:

  • 这太好了,谢谢!
  • Awk。出于某种原因,这会以指数方式增加大小。直接在路径上使用 NSKeyedArchiver 会产生一个 5MB 的数据文件,而使用这种编码方法对其进行编码会产生一个 28MB 的文件。知道如何改进 5MB 的吗?
  • 您能否更详细地说明您当前如何保存路径?多少条路径加起来达到 5MB(或 28MB)?你是在 iOS 还是 macOS 上工作?
【解决方案2】:

“最佳”完全是主观的,完全取决于您的需求和优先事项。人类可读性重要吗?数据大小重要吗?与某处现有 API 的兼容性?您需要回答这些问题才能确定适合您的用例。

如果数据的人类可读性很重要,那么 JSON 显然是 NSCoding 的赢家。如果大小很重要,那么压缩后的 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 倍。

这里的要点很大程度上取决于您的具体用例,以及您的数据存储优先级。


注意:这里很容易查看少量数字并尝试得出关于什么是“最佳”的结论,但请记住这里的规模:除非您的路径有数千个点长,否则有效 这些方法之间的差异导致大小差异可以忽略不计。无论您是努力维护CGPath 引用的编码接口,还是将单行转换为UIBezierPath,都可能比任何大小节省对您产生的影响更大。

【讨论】:

    猜你喜欢
    • 2015-11-21
    • 2013-09-22
    • 2015-08-23
    • 2013-05-15
    • 2012-12-24
    • 1970-01-01
    • 2012-06-19
    • 2013-01-08
    • 2021-12-17
    相关资源
    最近更新 更多