PNG の IDAT のみを弄る

Swift の練習を兼ねて PNG ファイルを弄るプログラムを作ってみる。

処理内容は

1. PNG ファイルをチャンク単位にして読み込む
2. IDAT 以外は変更しない
3. IDAT を zlib で展開 (uncompress)
4. フィルタ タイプを弄る
5. IDAT を zlib で圧縮 (compress)
6. PNG ファイルを出力する

で、IDAT 内のフィルタ タイプの処理をテストする。

<details><summary>テスト プログラム(Xcode のコンソールアプリ)</summary><div>

```swift:main.swift
import Foundation
import Compression

// ############

var flag_update_image = true
var flag_image_filter = true
var flag_overwrite = false
var flag_verbose = false

// ############

enum PNGError : Error {
    case system
    case open
    case exists
    case create
    case signature
    case unsupported
    case data
    case io
    func errorPrint(_ msg: String) {
        switch (self)
        {
        case .system:
            print("内部エラー: \(msg)")
        case .open:
            print("ファイルが開けません: \(msg)")
        case .exists:
            print("ファイルが既にあります: \(msg)")
        case .create:
            print("ファイルが作れません: \(msg)")
        case .signature:
            print("PNG ではありません: \(msg)")
        case .unsupported:
            print("非対応の形式です: \(msg)")
        case .data:
            print("データが破損しています: \(msg)")
        case .io:
            print("読み込みに失敗しました: \(msg)")
        }
    }
}

// ############

func UInt32EB(_ byteArray: ArraySlice<UInt8>) -> UInt32 {
    var value: UInt32 = 0
    for b in byteArray {
        value <<= 8
        value |= UInt32(b)
    }
    return value
}

extension Data {
    func ToArrayU8() -> [UInt8] {
        return [UInt8](unsafeUninitializedCapacity: count)
        { buffer, initializedCount in
            copyBytes(to: buffer, from: 0..<count)
            initializedCount = count
        }
    }
}

// ############

extension FileHandle {
    func readDataU8(ofLength count: Int) -> [UInt8] {
        return readData(ofLength: count).ToArrayU8()
    }
    func readU32EB() -> UInt32? {
        let data = readDataU8(ofLength: 4)
        if data.count != 4 {
            return nil
        }
        return UInt32EB(data[0..<4])
    }
    func writeU32EB(_ value: UInt32) {
        write(Data([
            UInt8((value >> 24) & 0xff),
            UInt8((value >> 16) & 0xff),
            UInt8((value >>  8) & 0xff),
            UInt8((value >>  0) & 0xff),
        ]))
    }
}

// ############

class CRC32Table {
    private static var dictionary: [UInt32: [UInt32]] = [:]
    private let table: [UInt32]
    subscript(index: Int) -> UInt32 {
        return table[index]
    }
    init(_ polynomial: UInt32) {
        if let dictTable = CRC32Table.dictionary[polynomial] {
            table = dictTable
            return
        }
        let newTable = [UInt32](unsafeUninitializedCapacity: 256)
        { buffer, initializedCount in
            for n in 0..<256 {
                var v = UInt32(n)
                for _ in 0..<8 {
                    let f = v & 1
                    v >>= 1
                    if f != 0 {
                        v ^= polynomial
                    }
                }
                buffer[n] = v
            }
            initializedCount = 256
        }
        CRC32Table.dictionary[polynomial] = newTable
        table = newTable
    }
}

class CRC32 {
    private let table: CRC32Table
    private var value_: UInt32 = ~0
    var value : UInt32 {
        get {
            return ~value_
        }
    }
    init(_ polynomial: UInt32 = 0xEDB88320) {
        table = CRC32Table(polynomial)
    }
    func update(_ data: UInt8) {
        let index = Int(UInt8(value_ & 0xff) ^ data)
        value_ = (value_ >> 8) ^ table[index]
    }
    func update(_ data: Data)  {
        update(data.ToArrayU8())
    }
    func updateEB(_ data: UInt32) {
        update(UInt8((data >> 24) & 0xff))
        update(UInt8((data >> 16) & 0xff))
        update(UInt8((data >>  8) & 0xff))
        update(UInt8((data >>  0) & 0xff))
    }
    func update(_ data: [UInt8]) {
        for v in data {
            update(v)
        }
    }
}

// ############

// Compression Module (ZLIB)

func CompressM(_ src: [UInt8]) -> [UInt8]? {
    var buffer: [UInt8] = [0x78, 0x5E]
    do {
        let out = try OutputFilter(.compress, using: .zlib)
        { (data: Data?) -> Void in
            buffer.append(contentsOf: data?.ToArrayU8() ?? [])
        }
        try out.write(src)
        try out.finalize()
    } catch let error {
        print(error.localizedDescription)
        return nil
    }
    return buffer
}

func ZLibUncompress(_ size: UInt32 , _ src: [UInt8]) -> [UInt8]? {
    var buffer: [UInt8] = []
    do {
        let out = try OutputFilter(.decompress, using: .zlib)
        { (data: Data?) -> Void in
            buffer.append(contentsOf: data?.ToArrayU8() ?? [])
        }
        try out.write(src[2...])
        try out.finalize()
    } catch let error {
        print(error.localizedDescription)
        return nil
    }
    return buffer
}

// C Library

func zlibCompress(_ src: [UInt8]) -> [UInt8]? {
    var buffer = [UInt8](repeating: 0, count: Int(src.count * 2))
    let status = CompressC(&buffer, UInt(buffer.count), src, UInt(src.count), 9)
    if status < 0 {
        return nil
    }
    return [UInt8](buffer[0..<status])
}

// https://developer.apple.com/documentation/compression/compression_zlib
// Compression モジュールは圧縮レベル 5 のみだから高圧縮を求めるため C ライブラリを使う.
var ZLibCompress = zlibCompress // C ライブラリ (libz) を使用する.

// ############

class Chunk
{
    var length: UInt32 = 0
    var idData = Data()
    var id = ""
    var data: [UInt8] = []
    var crc: UInt32 = 0
    init() { /*NOP*/ }
    init(_ handle: FileHandle) throws {
        try read(handle)
    }
    func read(_ handle: FileHandle) throws {
        guard let rLen = handle.readU32EB() else {
            throw PNGError.io
        }
        length = rLen
        
        idData = handle.readData(ofLength: 4)
        if idData.count != 4 {
            throw PNGError.io
        }
        guard let cId = String(data: idData, encoding: String.Encoding.utf8) else {
            throw PNGError.data
        }
        id = cId
        
        let rData = handle.readDataU8(ofLength: Int(rLen))
        if rData.count != rLen {
            throw PNGError.data
        }
        data = rData
        
        guard let rCrc = handle.readU32EB() else {
            throw PNGError.io
        }
        crc = rCrc
        
        let crc32 = CRC32()
        crc32.update(idData)
        crc32.update(data)
        if crc32.value != rCrc {
            throw PNGError.data
        }
        VerbosePrint(String(format: "\(id): len=%-6d  crc=%#010x", length, crc))
    }
    func write(_ handle: FileHandle) {
        handle.writeU32EB(length)
        handle.write(idData)
        handle.write(Data(data))
        
        let crc32 = CRC32()
        crc32.update(idData)
        crc32.update(data)
        handle.writeU32EB(crc32.value)
        
        VerbosePrint(String(format: "\(id): len=%-6d  crc=%#010x", length, crc))
    }
}

class IHDRChunk
{
    static let colorTypeTable: [UInt8] = [1,0,3,1,2,0,4]
    var width: UInt32 = 0
    var height: UInt32 = 0
    var colorDepth: UInt8 = 0
    var colorType: UInt8 = 0
    var compression: UInt8 = 0
    var filter: UInt8 = 0
    var interlace: UInt8 = 0
    var bitsPerPixel: UInt8 = 0
    var bytesPerPixel : UInt8 = 0
    init() { /*NO-OP*/ }
    init(_ chunk: Chunk) {
        let ihdr = chunk.data
        width  = UInt32EB(ihdr[0..<4])
        height = UInt32EB(ihdr[4..<8])
        colorDepth = ihdr[8]
        colorType = ihdr[9]
        compression = ihdr[10]
        filter = ihdr[11]
        interlace = ihdr[12]
        let numDepth = IHDRChunk.colorTypeTable[Int(colorType)]
        bitsPerPixel = colorDepth * numDepth
        bytesPerPixel = (bitsPerPixel + 7) >> 3
    }
}

struct ADAM7Param {
    static let xOffsetTable: [UInt8] = [0, 4, 0, 2, 0, 1, 0, 0]
    static let yOffsetTable: [UInt8] = [0, 0, 4, 0, 2, 0, 1, 0]
    static let xStepTable  : [UInt8] = [8, 8, 4, 4, 2, 2, 1, 1]
    static let yStepTable  : [UInt8] = [8, 8, 8, 4, 4, 2, 2, 1]
    let xOffs: UInt32
    let yOffs: UInt32
    let xStep: UInt32
    let yStep: UInt32
    let width: UInt32
    let height: UInt32
    let rawBytes: UInt32
    let stride: UInt32
    let imageSize: UInt32
    let imageOffset: UInt32
    init(_ ihdr: IHDRChunk, _ level:Int, _ offs:UInt32) {
        xOffs = UInt32(ADAM7Param.xOffsetTable[level])
        yOffs = UInt32(ADAM7Param.yOffsetTable[level])
        xStep = UInt32(ADAM7Param.xStepTable[level])
        yStep = UInt32(ADAM7Param.yStepTable[level])
        //
        width    = (ihdr.width  >= xOffs) ? ((ihdr.width  - xOffs + xStep - 1) / xStep) : 0
        height   = (ihdr.height >= yOffs) ? ((ihdr.height - yOffs + yStep - 1) / yStep) : 0
        rawBytes = (width * UInt32(ihdr.bitsPerPixel) + 7) >> 3
        stride   = (rawBytes != 0) ? (rawBytes + 1) : 0
        //
        imageSize   = stride * height
        imageOffset = offs
    }
}

class ADAM7 {
    let interlace: [ADAM7Param]
    let interlaceSize: UInt32
    let imageSize: UInt32
    var progressive: ADAM7Param {
        get {
            return interlace[7]
        }
    }
    subscript(index: Int) -> ADAM7Param {
        return interlace[index]
    }
    init(_ ihdr: IHDRChunk) {
        interlace = [ADAM7Param](unsafeUninitializedCapacity: 8)
        { buffer, initializedCount in
            var offs: UInt32 = 0
            for n in 0..<7 {
                buffer[n] = ADAM7Param(ihdr, n, offs)
                offs += buffer[n].imageSize
            }
            buffer[7] = ADAM7Param(ihdr, 7, 0)
            initializedCount = 8
        }
        interlaceSize = interlace[6].imageOffset + interlace[6].imageSize
        imageSize = (ihdr.interlace != 0) ? interlaceSize: interlace[7].imageSize
    }
}

class PNGFile {
    static let signature: [UInt8] = [137, 80, 78, 71, 13, 10, 26, 10]
    var chunk: [Chunk] = []
    var idat_length: [UInt32] = []
    var idat_average: UInt32 = 0
    var ihdr : IHDRChunk = IHDRChunk()
    var image: [UInt8] = []
    subscript(id: String) -> Chunk? {
        let n = find(id)
        if n < 0 {
            return nil
        }
        return chunk[n]
    }
    init(_ path: String) throws {
        try self.read(path)
    }
    func find(_ id: String) -> Int {
        for n in 0..<chunk.count {
            if id == chunk[n].id {
                return n
            }
        }
        return -1
    }
    func setImageData(_ image: [UInt8]) {
        let n = find("IDAT")
        chunk[n].length = UInt32(image.count)
        chunk[n].data = image
    }
    func read(_ handle: FileHandle) throws {
        let signature: [UInt8] = handle.readDataU8(ofLength: 8)
        if signature != PNGFile.signature {
            throw PNGError.signature
        }
        var rChunk = try Chunk(handle)
        ihdr = IHDRChunk(rChunk)
        var chunk_id = rChunk.id
        if chunk_id != "IHDR" {
            throw PNGError.data
        }
        chunk.append(rChunk)
        while chunk_id != "IEND" {
            let last_chunk = chunk_id
            rChunk = try Chunk(handle)
            chunk_id = rChunk.id
            switch (chunk_id) {
            case "IHDR":
                throw PNGError.data
            case "IDAT":
                idat_length.append(rChunk.length)
                if last_chunk == "IDAT" {
                    let idatIdx = chunk.count - 1
                    chunk[idatIdx].data.append(contentsOf: rChunk.data)
                    chunk[idatIdx].length = UInt32(chunk[idatIdx].data.count)
                    continue
                }
                if idat_length.count != 1 {
                    throw PNGError.data
                }
            default: break
            }
            chunk.append(rChunk)
        }
        if ![1,2,4,8,16].contains(ihdr.colorDepth) {
            throw PNGError.unsupported
        }
        if ![0,2,3,4,6].contains(ihdr.colorType) {
            throw PNGError.unsupported
        }
        if ihdr.compression != 0 {
            throw PNGError.unsupported
        }
        if ihdr.filter != 0 {
            throw PNGError.unsupported
        }
        if ihdr.interlace >= 2 {
            throw PNGError.unsupported
        }

        idat_length.removeLast()
        idat_average = 0
        if idat_length.count != 0 {
            let split1 = idat_length.reduce(0, +)
            idat_average = split1 / UInt32(idat_length.count)
        }
    }
    func read(_ path: String) throws {
        guard let handle = FileHandle(forReadingAtPath: path) else {
            throw PNGError.open
        }
        try! self.read(handle)
        handle.closeFile()
    }
    func write(_ handle: FileHandle) {
        handle.write(Data(PNGFile.signature))
        for ch in chunk {
            if ch.id != "IDAT" {
                ch.write(handle)
                continue
            }
            if idat_average == 0 {
                ch.write(handle)
                continue
            }
            var idat_spIdx = 0
            var idat_offset = 0
            var idat_total = ch.data.count
            while idat_total > 0 {
                var idat_split = Int(idat_average)
                if idat_spIdx < idat_length.count {
                    idat_split = Int(idat_length[idat_spIdx])
                    idat_spIdx += 1
                }
                if idat_split > idat_total {
                    idat_split = idat_total
                }
                let idat_next = idat_offset + idat_split
                let split_data = Data(ch.data[idat_offset..<idat_next])

                handle.writeU32EB(UInt32(idat_split))
                handle.write(ch.idData)
                handle.write(split_data)

                let crc32 = CRC32()
                crc32.update(ch.idData)
                crc32.update(split_data)
                let crc = crc32.value
                handle.writeU32EB(crc)

                idat_offset = idat_next
                idat_total -= idat_split

                VerbosePrint(String(format: "\(ch.id): len=%-6d  crc=%#010x", split_data.count, crc))
            }
        }
    }
    func write(_ path: String, _ overwrite: Bool = false) throws {
        if FileManager.default.fileExists(atPath: path) {
            if !overwrite {
                throw PNGError.exists
            }
            try FileManager.default.removeItem(atPath: path)
        }
        if !FileManager.default.createFile(atPath: path, contents: nil, attributes: nil) {
            throw PNGError.system
        }
        guard let handle = FileHandle(forWritingAtPath: path) else {
            throw PNGError.open
        }
        self.write(handle)
        handle.closeFile()
    }
}

// ############

func ImageReconstruct(_ ihdr: IHDRChunk, _ adam7: ADAM7, _ image: inout [UInt8]) throws {
    VerbosePrint("Reconstruct:")
    for adam in adam7.interlace[(ihdr.filter != 0) ? (0..<7) : (7..<8)] {
        VerbosePrint(String(format: "  (W:%04d, H:%04d)", adam.width, adam.height))
        VerbosePrint(String(format: "  (X:%04d, Y:%04d)", adam.xOffs, adam.yOffs))
        VerbosePrint(String(format: "  (R:%04d, B:%04d)", adam.xStep, adam.yStep))
        let pixelBytes = Int(ihdr.bytesPerPixel)
        let stride = Int(adam.stride)
        let rawBytes = Int(adam.rawBytes)
        let lineBytes = pixelBytes + stride - 1
        var lineOffs = Int(adam.imageOffset)
        var imgOffs = lineOffs + 1
        if stride == 0 {
            continue
        }
        var lastLine = [UInt8](repeating: 0, count: pixelBytes + lineBytes)
        for y in 0..<adam.height {
            var currentLine = [UInt8](repeating: 0, count: pixelBytes)
            currentLine.append(contentsOf: image[imgOffs..<(imgOffs+rawBytes)])
            switch (image[lineOffs])
            {
            case 0: // NONE
                VerbosePrint(String(format: "  Line %4d: NONE", y))
                break
            case 1: // SUB
                VerbosePrint(String(format: "  Line %4d: SUB", y))
                image[lineOffs] = 0
                for x in 0..<rawBytes {
                    let a = UInt16(currentLine[x])
                    let p = UInt16(currentLine[x+pixelBytes])
                    let q = UInt8((p + a) & 0xff)
                    image[imgOffs+x] = q
                    currentLine[x+pixelBytes] = q
                }
            case 2: // UP
                VerbosePrint(String(format: "  Line %4d: UP", y))
                image[lineOffs] = 0
                for x in 0..<rawBytes {
                    let b = UInt16(lastLine[x+pixelBytes])
                    let p = UInt16(currentLine[x+pixelBytes])
                    let q = UInt8((p + b) & 0xff)
                    image[imgOffs+x] = q
                    currentLine[x+pixelBytes] = q
                }
            case 3: // AVE
                VerbosePrint(String(format: "  Line %4d: AVE", y))
                image[lineOffs] = 0
                for x in 0..<rawBytes {
                    let a = UInt16(currentLine[x])
                    let b = UInt16(lastLine[x+pixelBytes])
                    let p = UInt16(currentLine[x+pixelBytes])
                    let q = UInt8((p + ((a + b) >> 1)) & 0xff)
                    image[imgOffs+x] = q
                    currentLine[x+pixelBytes] = q
                }
            case 4: // PAETH
                VerbosePrint(String(format: "  Line %4d: PAETH", y))
                image[lineOffs] = 0
                for x in 0..<rawBytes {
                    let a = Int16(currentLine[x])
                    let b = Int16(lastLine[x+pixelBytes])
                    let c = Int16(lastLine[x])
                    let p = Int16(currentLine[x+pixelBytes])
                    let d = a + b - c
                    let pa = (d > a) ? (d - a) : (a - d)
                    var pb = (d > b) ? (d - b) : (b - d)
                    let pc = (d > c) ? (d - c) : (c - d)
                    var qa = a
                    var qb = b
                    if pb > pc { qb = c; pb = pc }
                    if pa > pb { qa = qb }
                    let q = UInt8((p + qa) & 0xff)
                    image[imgOffs+x] = q
                    currentLine[x+pixelBytes] = q
                }
            default:
                throw PNGError.data
            }
            lastLine = currentLine
            lineOffs += stride
            imgOffs += stride
        }
    }
}

func ImageFilterHint(_ data: [UInt8]) -> UInt32 {
    var hist = [UInt32](repeating: 0, count: 256)
    for n in data {
        hist[Int(n)] += 1
    }
    return hist.reduce(0) { ($0 > $1) ? $0 : $1 }
}

let filterName = ["NONE", "SUB", "UP", "AVE", "PAETH"]
func ImageFilter(_ ihdr: IHDRChunk, _ adam7: ADAM7, _ image: inout [UInt8]) {
    VerbosePrint("Filter:")
    for adam in adam7.interlace[(ihdr.filter != 0) ? (0..<7) : (7..<8)] {
        VerbosePrint(String(format: "  (W:%04d, H:%04d)", adam.width, adam.height))
        VerbosePrint(String(format: "  (X:%04d, Y:%04d)", adam.xOffs, adam.yOffs))
        VerbosePrint(String(format: "  (R:%04d, B:%04d)", adam.xStep, adam.yStep))
        let pixelBytes = Int(ihdr.bytesPerPixel)
        let stride = Int(adam.stride)
        let rawBytes = Int(adam.rawBytes)
        let lineBytes = pixelBytes + stride - 1
        var lineOffs = Int(adam.imageOffset)
        var imgOffs = lineOffs + 1
        if stride == 0 {
            continue
        }
        var lastLine = [UInt8](repeating: 0, count: pixelBytes + lineBytes)
        for y in 0..<adam.height {
            let pixelData = image[imgOffs..<(imgOffs+rawBytes)]
            var currentLine = [UInt8](repeating: 0, count: pixelBytes)
            currentLine.append(contentsOf: pixelData)

            var f0: [UInt8] = [0]
            var f1: [UInt8] = [1]
            var f2: [UInt8] = [2]
            var f3: [UInt8] = [3]
            var f4: [UInt8] = [4]

            // NONE
            f0.append(contentsOf: pixelData)
            // SUB
            for x in 0..<rawBytes {
                let a = Int16(currentLine[x])
                let p = Int16(currentLine[x+pixelBytes])
                let q = UInt8((p - a) & 0xff)
                f1.append(q)
            }
            // UP
            for x in 0..<rawBytes {
                let b = Int16(lastLine[x+pixelBytes])
                let p = Int16(currentLine[x+pixelBytes])
                let q = UInt8((p - b) & 0xff)
                f2.append(q)
            }
            // AVE
            for x in 0..<rawBytes {
                let a = Int16(currentLine[x])
                let b = Int16(lastLine[x+pixelBytes])
                let p = Int16(currentLine[x+pixelBytes])
                let q = UInt8((p - ((a + b) >> 1)) & 0xff)
                f3.append(q)
            }
            // PAETH
            for x in 0..<rawBytes {
                let a = Int16(currentLine[x])
                let b = Int16(lastLine[x+pixelBytes])
                let c = Int16(lastLine[x])
                let p = Int16(currentLine[x+pixelBytes])
                let d = a + b - c
                let pa = (d > a) ? (d - a) : (a - d)
                var pb = (d > b) ? (d - b) : (b - d)
                let pc = (d > c) ? (d - c) : (c - d)
                var qa = a
                var qb = b
                if pb > pc { qb = c; pb = pc }
                if pa > pb { qa = qb }
                let q = UInt8((p - qa) & 0xff)
                f4.append(q)
            }

            let filters = [f0,f1,f2,f3,f4]
            var hist: UInt32 = 0
            var filter: Int = 0
            for n in 0..<5 {
                let fh = ImageFilterHint(filters[n])
                if fh > hist {
                    hist = fh
                    filter = n
                }
            }

            VerbosePrint(String(format: "  Line %4d: \(filterName[filter])", y))
            let filterData = filters[filter]
            for n in 0..<stride {
                image[lineOffs+n] = filterData[n]
            }

            lastLine = currentLine
            lineOffs += stride
            imgOffs += stride
        }
    }
}

func UpdateImage(_ png: inout PNGFile) throws {
    guard let idat = png["IDAT"] else {
        throw PNGError.data
    }
    let ihdr = png.ihdr
    let adam7 = ADAM7(ihdr)
    guard var image_buffer = ZLibUncompress(adam7.imageSize, idat.data) else {
        throw PNGError.data
    }
    VerbosePrint(String(format: "Uncompress: %d(%d)", image_buffer.count, adam7.imageSize))
    if flag_update_image {
        try ImageReconstruct(ihdr, adam7, &image_buffer)
        if flag_image_filter {
            ImageFilter(ihdr, adam7, &image_buffer)
        }
    }
    guard let deflate_buffer = ZLibCompress(image_buffer) else {
        throw PNGError.system
    }
    VerbosePrint(String(format: "Compress: %d -> %d", idat.data.count, deflate_buffer.count))
    if flag_update_image {
        png.setImageData(deflate_buffer)
    }
}

// ############

func VerbosePrint(_ msg: String) {
    if flag_verbose {
        print(msg)
    }
}

// ############

var cmd_args = CommandLine.arguments
var program = cmd_args.removeFirst()
var program_paths = program.split(separator: "/")
var program_name = program_paths.last

func cmd_arg_parse() {
    while cmd_args.count > 0 {
        var arg = cmd_args[0]
        if arg.removeFirst() != "-" {
            break
        }
        cmd_args.removeFirst()
        while arg.count > 0 {
            switch (arg.removeFirst())
            {
            case "O":
                flag_overwrite = true
            case "u":
                flag_update_image = false
            case "v":
                flag_verbose = true
            case "f":
                flag_image_filter = false
            case "M":
                ZLibCompress = CompressM
            default:
                usage()
            }
        }
    }
    if cmd_args.count < 2 {
        usage()
    }
}
cmd_arg_parse()

func usage() -> Never {
    print("""
        Usage: \(String(describing: program_name)) [オプション] 入力ファイル 出力ファイル
        オプション:
            -O   既存ファイルへ上書きする
            -u   IDAT を更新しない(複写になる)
            -v   冗長出力モード
        IDAT 更新オプション:
            -f   フィルターなしにする
            -M   Compression モジュールを使う
        """)
    exit(1)
}

func main(_ inpFile: String, _ outFile: String) -> Int32 {
    var path = inpFile
    do {
        var png = try PNGFile(inpFile)
        try UpdateImage(&png)
        path = outFile
        try png.write(outFile, flag_overwrite)
    } catch let error {
        let rError = error as! PNGError
        rError.errorPrint(path)
        return 2
    }
    return 0
}

exit(main(cmd_args[0], cmd_args[1]))
```

```objective_c:zlibwrapper.m
#import <Foundation/Foundation.h>
#import <zlib.h>

long CompressC(void *buffer, unsigned long length, const void *data, unsigned long size, int level)
{
    unsigned long bufsz = length;
    if (compress2((Bytef*)buffer, &bufsz, (const Bytef*)data, size, level) != 0)
        return -1;
    return bufsz;
}
```

```objective_c:project-Bridging-Header.h
long CompressC(void *buffer, unsigned long length, const void *data, unsigned long size, int level);
```

</div></details>

幾つか試した限りでは、データが小さくなった。uncompress → compress (Lv:9) の処理だけでも小さくなるので、書き出したアプリでの圧縮レベルが最大ではないのだろう。更にフィルタ タイプを弄ると僅かに縮むので、悪い選択処理ではなさそうです。