Skip to content

Instantly share code, notes, and snippets.

@mbotsu
Last active February 27, 2023 04:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbotsu/817c96b9a31901ec392fc8ba1389902a to your computer and use it in GitHub Desktop.
Save mbotsu/817c96b9a31901ec392fc8ba1389902a to your computer and use it in GitHub Desktop.
Swift Animation PNG Encoder

Features

  • True Color Image PNG
  • 24bit PNG Support
  • Compression Support
  • Stream write Support

Dependencies

Example

import opencv2

...

let fileUrl: URL = FileManager.default.temporaryDirectory.appendingPathComponent("test.png")
do {
  try FileManager.default.removeItem(atPath: fileUrl.path)
} catch {
  print(error.localizedDescription)
}

let frameCount = 3
let width = 100
let height = 100
let apng = AnimationPngEncoder(fileUrl: fileUrl, width: width, height: height,
                               frameCount: frameCount, fps: 10, compression: 3)
do {
  for p in 0..<frameCount {
    try autoreleasepool {
      let img = Mat(size: Size(width: Int32(height), height: Int32(width)),
                    type: CvType.CV_8UC3, scalar: Scalar(0,0,0))
      Imgproc.circle(img: img, center: Point2i(x: Int32(p * 10), y: 50), radius: 5, color: Scalar(255,0,0), thickness: -1)
      Imgproc.circle(img: img, center: Point2i(x: 50, y: Int32(p * 10)), radius: 5, color: Scalar(0,0,255), thickness: -1)
      Imgproc.cvtColor(src: img, dst: img, code: ColorConversionCodes.COLOR_BGR2RGB)
      try apng.push(img: img)
    }
  }
  try apng.close()
} catch {
  print("error:", error)
}
print(fileUrl)
import opencv2
class AnimationPngEncoder {
// IDAT = Length + ChunkType + ChunkDATA + CRC
let blockSize = 4 + 4 + 8192 + 4
let fileUrl: URL
var sequance = 0
var compression: Int
let signature: Data
let acTL: Data
var fcTL: Data
var IHDR: Data
enum StreamError: Error {
case open
case write
}
init(fileUrl: URL, width: Int, height: Int,
frameCount: Int, loopCount: Int = 0, fps: Int = 30, compression: Int = 3) {
self.fileUrl = fileUrl
self.compression = compression
self.signature = Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
// bitDepth, colorType, compression , filter, interlace
self.IHDR = (width.data + height.data + Data([ 8 , 2 , 0 , 0 , 0 ])).conv("IHDR")
self.acTL = Data(frameCount.uint8 + loopCount.uint8).conv("acTL")
// fcTL
let xOffset = UInt32(0).uint8
let yOffset = UInt32(0).uint8
let delayNum = UInt16(1).uint8
let delayDen = UInt16(fps).uint8
let disposeOp: [UInt8] = [0]
let blendOp: [UInt8] = [0]
self.fcTL = width.data + height.data + Data(xOffset + yOffset +
delayNum + delayDen +
disposeOp + blendOp)
}
func push(img: Mat) throws {
let buf = ByteVector()
let pngCompression = IntVector([ImwriteFlags.IMWRITE_PNG_COMPRESSION.rawValue,
Int32(compression)])
Imgcodecs.imencode(ext: ".png", img: img, buf: buf, params: pngCompression)
let IDAT = Data(buf.data[8+25..<buf.length-12])
let fcTL = (self.sequance.data + self.fcTL).conv("fcTL")
if self.sequance == 0 {
try write(self.signature + self.IHDR + self.acTL + fcTL + IDAT)
} else {
try write(fcTL)
let idatCount = Int(ceil(Float(IDAT.count) / Float(blockSize)))
for i in 0..<idatCount {
let pos = blockSize*i
let length = Data(IDAT[pos..<pos+4]).int
let chunkData = IDAT[pos+8..<pos+8+length]
self.sequance += 1
try write((self.sequance.data + chunkData).conv("fdAT"))
}
}
self.sequance += 1
}
func close() throws {
try write(Data().conv("IEND"))
}
func write(_ data: Data) throws {
guard let stream = OutputStream(url: self.fileUrl, append: true) else {
throw StreamError.open
}
stream.open()
defer {
stream.close()
}
let result = stream.write(data)
if result < 1 {
throw StreamError.write
}
}
}
import Foundation
import zlib
extension OutputStream {
func write(_ data: Data) -> Int {
return data.withUnsafeBytes {
self.write($0.bindMemory(to: Bytef.self).baseAddress!, maxLength: data.count)
}
}
}
extension Data {
var uint32: UInt32 {
return UInt32(bigEndian: withUnsafeBytes { $0.load(as: UInt32.self) })
}
var int: Int {
return Int(self.uint32)
}
var crc: Data {
let checksum = withUnsafeBytes { crc32_z(0, $0.bindMemory(to: Bytef.self).baseAddress, self.count) }
return UInt32(checksum).data
}
func conv(_ chunkType: String) -> Data {
let chunkData = self
let data = Data(chunkType.utf8) + chunkData
return chunkData.count.data + data + data.crc
}
}
extension UInt16 {
var uint8: [UInt8] {
return withUnsafeBytes(of: self.bigEndian) { Array($0) }
}
var data: Data {
return Data(self.uint8)
}
}
extension Int {
var uint8: [UInt8] {
return UInt32(self).uint8
}
var data: Data {
return UInt32(self).data
}
}
extension UInt32 {
var uint8: [UInt8] {
return withUnsafeBytes(of: self.bigEndian) { Array($0) }
}
var data: Data {
return Data(self.uint8)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment