import Foundation import LottieCpp final class WriteBuffer { private(set) var data: Data private var capacity: Int var length: Int init() { self.capacity = 1024 self.data = Data(count: self.capacity) self.length = 0 } func trim() { self.data.count = self.length self.capacity = self.data.count } func write(bytes: UnsafeRawBufferPointer) { if self.data.count < self.length + bytes.count { self.data.count = self.data.count * 2 } self.data.withUnsafeMutableBytes { buffer -> Void in memcpy(buffer.baseAddress!.advanced(by: self.length), bytes.baseAddress!, bytes.count) } self.length += bytes.count } func write(uInt32 value: UInt32) { var value = value withUnsafeBytes(of: &value, { bytes in self.write(bytes: bytes) }) } func write(uInt16 value: UInt16) { var value = value withUnsafeBytes(of: &value, { bytes in self.write(bytes: bytes) }) } func write(uInt8 value: UInt8) { var value = value withUnsafeBytes(of: &value, { bytes in self.write(bytes: bytes) }) } func write(float value: Float) { var value = value withUnsafeBytes(of: &value, { bytes in self.write(bytes: bytes) }) } func write(point: CGPoint) { self.write(float: Float(point.x)) self.write(float: Float(point.y)) } func write(size: CGSize) { self.write(float: Float(size.width)) self.write(float: Float(size.height)) } func write(rect: CGRect) { self.write(point: rect.origin) self.write(size: rect.size) } func write(transform: CATransform3D) { self.write(float: Float(transform.m11)) self.write(float: Float(transform.m12)) self.write(float: Float(transform.m13)) self.write(float: Float(transform.m14)) self.write(float: Float(transform.m21)) self.write(float: Float(transform.m22)) self.write(float: Float(transform.m23)) self.write(float: Float(transform.m24)) self.write(float: Float(transform.m31)) self.write(float: Float(transform.m32)) self.write(float: Float(transform.m33)) self.write(float: Float(transform.m34)) self.write(float: Float(transform.m41)) self.write(float: Float(transform.m42)) self.write(float: Float(transform.m43)) self.write(float: Float(transform.m44)) } } final class ReadBuffer { private let data: Data private var offset: Int init(data: Data) { self.data = data self.offset = 0 } func read(bytes: UnsafeMutableRawBufferPointer) { if self.offset + bytes.count <= self.data.count { self.data.withUnsafeBytes { buffer -> Void in memcpy(bytes.baseAddress!, buffer.baseAddress!.advanced(by: self.offset), bytes.count) } self.offset += bytes.count } else { preconditionFailure() } } func readUInt32() -> UInt32 { var value: UInt32 = 0 withUnsafeMutableBytes(of: &value, { bytes in self.read(bytes: bytes) }) return value } func readUInt16() -> UInt16 { var value: UInt16 = 0 withUnsafeMutableBytes(of: &value, { bytes in self.read(bytes: bytes) }) return value } func readUInt8() -> UInt8 { var value: UInt8 = 0 withUnsafeMutableBytes(of: &value, { bytes in self.read(bytes: bytes) }) return value } func readFloat() -> Float { var value: Float = 0 withUnsafeMutableBytes(of: &value, { bytes in self.read(bytes: bytes) }) return value } func readPoint() -> CGPoint { return CGPoint(x: CGFloat(self.readFloat()), y: CGFloat(self.readFloat())) } func readSize() -> CGSize { return CGSize(width: CGFloat(self.readFloat()), height: CGFloat(self.readFloat())) } func readRect() -> CGRect { return CGRect(origin: self.readPoint(), size: self.readSize()) } func readTransform() -> CATransform3D { return CATransform3D( m11: CGFloat(self.readFloat()), m12: CGFloat(self.readFloat()), m13: CGFloat(self.readFloat()), m14: CGFloat(self.readFloat()), m21: CGFloat(self.readFloat()), m22: CGFloat(self.readFloat()), m23: CGFloat(self.readFloat()), m24: CGFloat(self.readFloat()), m31: CGFloat(self.readFloat()), m32: CGFloat(self.readFloat()), m33: CGFloat(self.readFloat()), m34: CGFloat(self.readFloat()), m41: CGFloat(self.readFloat()), m42: CGFloat(self.readFloat()), m43: CGFloat(self.readFloat()), m44: CGFloat(self.readFloat()) ) } } private extension LottieColor { init(argb: UInt32) { self.init(r: CGFloat((argb >> 16) & 0xff) / 255.0, g: CGFloat((argb >> 8) & 0xff) / 255.0, b: CGFloat(argb & 0xff) / 255.0, a: CGFloat((argb >> 24) & 0xff) / 255.0) } var argb: UInt32 { return (UInt32(self.a * 255.0) << 24) | (UInt32(max(0.0, self.r) * 255.0) << 16) | (UInt32(max(0.0, self.g) * 255.0) << 8) | (UInt32(max(0.0, self.b) * 255.0)) } } private struct NodeFlags: OptionSet { var rawValue: UInt8 init(rawValue: UInt8) { self.rawValue = rawValue } static let masksToBounds = NodeFlags(rawValue: 1 << 0) static let isHidden = NodeFlags(rawValue: 1 << 1) static let hasSimpleContents = NodeFlags(rawValue: 1 << 2) static let isInvertedMatte = NodeFlags(rawValue: 1 << 3) static let hasRenderContent = NodeFlags(rawValue: 1 << 4) static let hasSubnodes = NodeFlags(rawValue: 1 << 5) static let hasMask = NodeFlags(rawValue: 1 << 6) } private struct LottieContentFlags: OptionSet { var rawValue: UInt8 init(rawValue: UInt8) { self.rawValue = rawValue } static let hasStroke = LottieContentFlags(rawValue: 1 << 0) static let hasFill = LottieContentFlags(rawValue: 1 << 1) } func serializePath(buffer: WriteBuffer, path: LottiePath) { let lengthOffset = buffer.length buffer.write(uInt32: 0) path.enumerateItems { pathItem in switch pathItem.pointee.type { case .moveTo: let point = pathItem.pointee.points.0 buffer.write(uInt8: 0) buffer.write(point: point) case .lineTo: let point = pathItem.pointee.points.0 buffer.write(uInt8: 1) buffer.write(point: point) case .curveTo: let cp1 = pathItem.pointee.points.0 let cp2 = pathItem.pointee.points.1 let point = pathItem.pointee.points.2 buffer.write(uInt8: 2) buffer.write(point: cp1) buffer.write(point: cp2) buffer.write(point: point) case .close: buffer.write(uInt8: 3) @unknown default: break } } let dataLength = buffer.length - lengthOffset - 4 let previousLength = buffer.length buffer.length = lengthOffset buffer.write(uInt32: UInt32(dataLength)) buffer.length = previousLength } func deserializePath(buffer: ReadBuffer) -> LottiePath { let itemDataLength = Int(buffer.readUInt32()) var itemData = Data(count: itemDataLength) itemData.withUnsafeMutableBytes { bytes in buffer.read(bytes: bytes) } return LottiePath(customData: itemData) } func serializeContentShading(buffer: WriteBuffer, shading: LottieRenderContentShading) { if let shading = shading as? LottieRenderContentSolidShading { buffer.write(uInt8: 0) buffer.write(uInt32: shading.color.argb) buffer.write(uInt8: UInt8(clamping: Int(shading.opacity * 255.0))) } else if let shading = shading as? LottieRenderContentGradientShading { buffer.write(uInt8: 1) buffer.write(uInt8: UInt8(clamping: Int(shading.opacity * 255.0))) buffer.write(uInt8: UInt8(shading.gradientType.rawValue)) let colorStopCount = min(shading.colorStops.count, 255) buffer.write(uInt8: UInt8(colorStopCount)) for i in 0 ..< colorStopCount { buffer.write(uInt32: shading.colorStops[i].color.argb) buffer.write(float: Float(shading.colorStops[i].location)) } buffer.write(point: shading.start) buffer.write(point: shading.end) } else { buffer.write(uInt8: 0) buffer.write(uInt8: UInt8(clamping: Int(1.0 * 255.0))) } } func deserializeContentShading(buffer: ReadBuffer) -> LottieRenderContentShading { switch buffer.readUInt8() { case 0: return LottieRenderContentSolidShading( color: LottieColor(argb: buffer.readUInt32()), opacity: CGFloat(buffer.readUInt8()) / 255.0 ) case 1: let opacity = CGFloat(buffer.readUInt8()) / 255.0 let gradientType = LottieGradientType(rawValue: UInt(buffer.readUInt8()))! var colorStops: [LottieColorStop] = [] let colorStopCount = Int(buffer.readUInt8()) for _ in 0 ..< colorStopCount { colorStops.append(LottieColorStop( color: LottieColor(argb: buffer.readUInt32()), location: CGFloat(buffer.readFloat()) )) } let start = buffer.readPoint() let end = buffer.readPoint() return LottieRenderContentGradientShading( opacity: opacity, gradientType: gradientType, colorStops: colorStops, start: start, end: end ) default: preconditionFailure() } } func serializeStroke(buffer: WriteBuffer, stroke: LottieRenderContentStroke) { serializeContentShading(buffer: buffer, shading: stroke.shading) buffer.write(float: Float(stroke.lineWidth)) buffer.write(uInt8: UInt8(stroke.lineJoin.rawValue)) buffer.write(uInt8: UInt8(stroke.lineCap.rawValue)) buffer.write(float: Float(stroke.miterLimit)) } func deserializeStroke(buffer: ReadBuffer) -> LottieRenderContentStroke { return LottieRenderContentStroke( shading: deserializeContentShading(buffer: buffer), lineWidth: CGFloat(buffer.readFloat()), lineJoin: CGLineJoin(rawValue: Int32(buffer.readUInt8()))!, lineCap: CGLineCap(rawValue: Int32(buffer.readUInt8()))!, miterLimit: CGFloat(buffer.readFloat()), dashPhase: 0.0, dashPattern: nil ) } func serializeFill(buffer: WriteBuffer, fill: LottieRenderContentFill) { serializeContentShading(buffer: buffer, shading: fill.shading) buffer.write(uInt8: UInt8(fill.fillRule.rawValue)) } func deserializeFill(buffer: ReadBuffer) -> LottieRenderContentFill { return LottieRenderContentFill( shading: deserializeContentShading(buffer: buffer), fillRule: LottieFillRule(rawValue: UInt(buffer.readUInt8()))! ) } func serializeRenderContent(buffer: WriteBuffer, renderContent: LottieRenderContent) { var flags: LottieContentFlags = [] if renderContent.stroke != nil { flags.insert(.hasStroke) } if renderContent.fill != nil { flags.insert(.hasFill) } buffer.write(uInt8: flags.rawValue) serializePath(buffer: buffer, path: renderContent.path) if let stroke = renderContent.stroke { serializeStroke(buffer: buffer, stroke: stroke) } if let fill = renderContent.fill { serializeFill(buffer: buffer, fill: fill) } } func deserializeRenderContent(buffer: ReadBuffer) -> LottieRenderContent { let flags = LottieContentFlags(rawValue: buffer.readUInt8()) let path = deserializePath(buffer: buffer) var stroke: LottieRenderContentStroke? if flags.contains(.hasStroke) { stroke = deserializeStroke(buffer: buffer) } var fill: LottieRenderContentFill? if flags.contains(.hasFill) { fill = deserializeFill(buffer: buffer) } return LottieRenderContent( path: path, stroke: stroke, fill: fill ) } func serializeNode(buffer: WriteBuffer, node: LottieRenderNode) { var flags: NodeFlags = [] if node.masksToBounds { flags.insert(.masksToBounds) } if node.isHidden { flags.insert(.isHidden) } if node.hasSimpleContents { flags.insert(.hasSimpleContents) } if node.isInvertedMatte { flags.insert(.isInvertedMatte) } if node.renderContent != nil { flags.insert(.hasRenderContent) } if !node.subnodes.isEmpty { flags.insert(.hasSubnodes) } if node.mask != nil { flags.insert(.hasMask) } buffer.write(uInt8: flags.rawValue) buffer.write(point: node.position) buffer.write(rect: node.bounds) buffer.write(transform: node.transform) buffer.write(uInt8: UInt8(clamping: Int(node.opacity * 255.0))) buffer.write(rect: node.globalRect) buffer.write(transform: node.globalTransform) if let renderContent = node.renderContent { serializeRenderContent(buffer: buffer, renderContent: renderContent) } if !node.subnodes.isEmpty { let count = min(node.subnodes.count, 4095) buffer.write(uInt16: UInt16(count)) for i in 0 ..< count { serializeNode(buffer: buffer, node: node.subnodes[i]) } } if let mask = node.mask { serializeNode(buffer: buffer, node: mask) } } func deserializeNode(buffer: ReadBuffer) -> LottieRenderNode { let flags = NodeFlags(rawValue: buffer.readUInt8()) let position = buffer.readPoint() let bounds = buffer.readRect() let transform = buffer.readTransform() let opacity = CGFloat(buffer.readUInt8()) / 255.0 let globalRect = buffer.readRect() let globalTransform = buffer.readTransform() var renderContent: LottieRenderContent? if flags.contains(.hasRenderContent) { renderContent = deserializeRenderContent(buffer: buffer) } var subnodes: [LottieRenderNode] = [] if flags.contains(.hasSubnodes) { let count = Int(buffer.readUInt16()) for _ in 0 ..< count { subnodes.append(deserializeNode(buffer: buffer)) } } var mask: LottieRenderNode? if flags.contains(.hasMask) { mask = deserializeNode(buffer: buffer) } return LottieRenderNode( position: position, bounds: bounds, transform: transform, opacity: opacity, masksToBounds: flags.contains(.masksToBounds), isHidden: flags.contains(.isHidden), globalRect: globalRect, globalTransform: globalTransform, renderContent: renderContent, hasSimpleContents: flags.contains(.hasSimpleContents), isInvertedMatte: flags.contains(.isInvertedMatte), subnodes: subnodes, mask: mask ) }