diff --git a/.github/workflows/android.yml b/.github/disabled/android.yml similarity index 100% rename from .github/workflows/android.yml rename to .github/disabled/android.yml diff --git a/Package.swift b/Package.swift index 591b822c..28ef08f4 100644 --- a/Package.swift +++ b/Package.swift @@ -13,8 +13,6 @@ let package:Package = .init(name: "swift-png", dependencies: [ .package(url: "https://github.com/tayloraswift/swift-hash", .upToNextMinor( from: "0.7.1")), - .package(url: "https://github.com/tayloraswift/swift-grammar", .upToNextMinor( - from: "0.4.0")), ], targets: [ .target(name: "LZ77", @@ -32,29 +30,29 @@ let package:Package = .init(name: "swift-png", .target(name: "PNG"), ]), - .executableTarget(name: "LZ77Tests", + .testTarget(name: "LZ77Tests", dependencies: [ .target(name: "LZ77"), - .product(name: "Testing_", package: "swift-grammar"), ], + path: "Sources/LZ77Tests", swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) ]), - .executableTarget(name: "PNGTests", + .testTarget(name: "PNGTests", dependencies: [ .target(name: "PNG"), - .product(name: "Testing_", package: "swift-grammar"), ], + path: "Sources/PNGTests", swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) ]), - .executableTarget(name: "PNGIntegrationTests", + .testTarget(name: "PNGIntegrationTests", dependencies: [ .target(name: "PNG"), - .product(name: "Testing_", package: "swift-grammar"), ], + path: "Sources/PNGIntegrationTests", exclude: [ "PngSuite.LICENSE", "PngSuite.README", @@ -63,11 +61,11 @@ let package:Package = .init(name: "swift-png", "RGBA/", ]), - .executableTarget(name: "PNGCompressionTests", + .testTarget(name: "PNGCompressionTests", dependencies: [ .target(name: "PNG"), - .product(name: "Testing_", package: "swift-grammar"), - ]), + ], + path: "Sources/PNGCompressionTests"), .executableTarget(name: "PNGCompressionBenchmarks", dependencies: [ diff --git a/Scripts/TestAll b/Scripts/TestAll index 8f6a6776..47f6bc1e 100755 --- a/Scripts/TestAll +++ b/Scripts/TestAll @@ -4,9 +4,7 @@ swift --version swift build swift build -c release # Debug-only tests -swift run -c debug LZ77Tests -swift run -c debug PNGTests +swift test -c debug --filter LZ77Tests +swift test -c debug --filter PNGTests # All tests -for f in .build/release/*Tests; do - $f -done +swift test -c release --no-parallel diff --git a/Sources/LZ77Tests/Bitstreams.swift b/Sources/LZ77Tests/Bitstreams.swift new file mode 100644 index 00000000..ddea9a94 --- /dev/null +++ b/Sources/LZ77Tests/Bitstreams.swift @@ -0,0 +1,187 @@ +#if DEBUG +@testable +import LZ77 +import Testing + +@Suite +enum CompressionInternals +{ + @Test + static func BitstreamDecoding() + { + var bits:LZ77.InflatorIn = [ + 0b1001_1110, + 0b1111_0110, + 0b0010_0011, + ] + #expect(bits[ 0] == 0b1111_0110_1001_1110) + #expect(bits[ 1] == 0b1_1111_0110_1001_111) + #expect(bits[ 2] == 0b11_1111_0110_1001_11) + #expect(bits[ 3] == 0b011_1111_0110_1001_1) + #expect(bits[ 4] == 0b0011_1111_0110_1001) + #expect(bits[ 5] == 0b0_0011_1111_0110_100) + #expect(bits[ 6] == 0b10_0011_1111_0110_10) + #expect(bits[ 7] == 0b010_0011_1111_0110_1) + #expect(bits[ 8] == 0b0010_0011_1111_0110) + #expect(bits[ 9] == 0b0_0010_0011_1111_011) + #expect(bits[23] == 0b0000_0000_0000_0000) + + #expect(bits[0, count: 4, as: Int.self] == 0b1110) + #expect(bits[1, count: 4, as: Int.self] == 0b1_111) + #expect(bits[1, count: 6, as: Int.self] == 0b001_111) + #expect(bits[2, count: 6, as: Int.self] == 0b1001_11) + #expect(bits[2, count: 16, as: Int.self] == 0b11_1111_0110_1001_11) + + // test rebase + // { 0010_0011, 1111_0110, 1001_1110 } + // ^ + // b = 20 + // -> + // { 0001_1000, 1010_1101, 0010_0011 } + // ^ + // b = 4 + var b:Int = 20 + + bits.rebase([0b1010_1101, 0b0001_1000], pointer: &b) + + #expect(bits[b ] == 0b1000_1010_1101_0010) + #expect(bits[b + 1] == 0b1_1000_1010_1101_001) + + // test rebase + // { 0001_1000, 1010_1101, 0010_0011 } + // ^ + // b = 4 + // { 1111_1100, 0011_1111, 0001_1000, 1010_1101, 0010_0011 } + bits.rebase([0b0011_1111, 0b1111_1100], pointer: &b) + + #expect(bits[b ] == 0b1000_1010_1101_0010) + #expect(bits[b + 8] == 0b1111_0001_1000_1010) + } + @Test + static func BitstreamEncoding() + { + var bits:LZ77.DeflatorOut = .init(hint: 4) + + bits.append(0b11, count: 2) + bits.append(0b01_10, count: 4) + + bits.append(0b0110, count: 0) + + bits.append(0b1_1111_11, count: 7) + bits.append(0b1010_1010_1010_101, count: 15) + bits.append(0b000, count: 3) + bits.append(0b0_1101_1, count: 6) + bits.append(0b1_0000_0000_111, count: 12) + + var encoded:[UInt8] = [] + + while let chunk:[UInt8] = bits.pop() + { + encoded.append(contentsOf: chunk) + } + + encoded.append(contentsOf: bits.pull()) + + #expect(encoded == [ + 0b1101_1011, + 0b1011_1111, + 0b1010_1010, + 0b1000_1010, + 0b1110_1101, + 0b0000_0000, + 0b0000_0001 + ]) + } + + @Test + static func Matching() + { + let segments:[[UInt8]] = [ + [1, 2, 3, 3, 1, 2, 3, 3, 1, 2, 3, 1, 2, 2, 2, 2, 2, 2, 0, 1, 2], + [2, 2, 2, 2, 0, 1, 2, 2, 0, 0, 0, 0, 2, 3, 2, 1, 2, 3, 3, 1, 5], + [1, 1, 3, 3, 1, 2, 3, 1, 2, 4, 4, 2, 1] + ] + var input:LZ77.DeflatorIn = .init() + var window:LZ77.DeflatorWindow = .init(exponent: 4) + var output:[[UInt8]] = [] + for (s, segment):(Int, [UInt8]) in segments.enumerated() + { + input.enqueue(contentsOf: segment[...]) + + let lookahead:Int = (s == segments.count - 1 ? 0 : 10) + while window.endIndex < 0, input.count > lookahead + { + window.initialize(with: input.dequeue()) + } + while input.count > lookahead + { + let head:(index:Int, next:UInt16?) = window.update(with: input.dequeue()) + if let match:(run:Int, distance:Int) = window.match(from: head, + lookahead: input, + attempts: .max, + goal: .max) + { + var run:[UInt8] = [window.literal] + for _:Int in 1 ..< match.run + { + window.update(with: input.dequeue()) + run.append(window.literal) + } + output.append(run) + } + else + { + output.append([window.literal]) + } + } + + guard s == segments.count - 1 + else + { + continue + } + + // epilogue: get the matches still sitting in the pipeline + let epilogue:Int = -3 - min(0, window.endIndex) + while input.count > epilogue + { + window.update(with: input.dequeue()) + output.append([window.literal]) + } + } + #expect(output == [ + [1], + [2], + [3], + [3], + [1, 2, 3, 3, 1, 2, 3], + [1], + [2], [2], [2], [2], [2], [2], + [0], + [1, 2, 2, 2, 2, 2], + [0], + [1], + [2], [2], + [0], [0], [0], [0], + [2], + [3], + [2], + [1], + [2], + [3], [3], + [1], + [5], + [1], [1], + [3], [3], + [1], + [2], + [3], + [1], + [2], + [4], [4], + [2], + [1] + ]) + } +} +#endif diff --git a/Sources/LZ77Tests/Compression.swift b/Sources/LZ77Tests/Compression.swift new file mode 100644 index 00000000..7fd3c1d7 --- /dev/null +++ b/Sources/LZ77Tests/Compression.swift @@ -0,0 +1,50 @@ +import LZ77 +import Testing + +@Suite +enum Compression +{ + @Test(arguments: [4, 7, 9], [5, 15, 100, 200, 2000, 5000]) + static func LZ77(_ level:Int, _ count:Int) throws + { + let input:[UInt8] = (0 ..< count).map{ _ in .random(in: .min ... .max) } + + var deflator:LZ77.Deflator = .init(level: level, exponent: 8, hint: 16) + deflator.push(input[...], last: true) + + var compressed:[UInt8] = [] + while let part:[UInt8] = deflator.pull() + { + compressed += part + } + + var inflator:LZ77.Inflator = .init() + try inflator.push(compressed[...]) + + let output:[UInt8] = inflator.pull() + + #expect(input == output) + } + + @Test(arguments: [5, 15, 100, 200, 2000, 5000]) + static func Gzip(_ count:Int) throws + { + let input:[UInt8] = (0 ..< count).map{ _ in .random(in: .min ... .max) } + + var deflator:Gzip.Deflator = .init(level: 7, exponent: 15, hint: 64 << 10) + deflator.push(input[...], last: true) + + var compressed:[UInt8] = [] + while let part:[UInt8] = deflator.pull() + { + compressed += part + } + + var inflator:Gzip.Inflator = .init() + try inflator.push(compressed[...]) + + let output:[UInt8] = inflator.pull() + + #expect(input == output) + } +} diff --git a/Sources/LZ77Tests/CompressionMicro.swift b/Sources/LZ77Tests/CompressionMicro.swift new file mode 100644 index 00000000..a0844386 --- /dev/null +++ b/Sources/LZ77Tests/CompressionMicro.swift @@ -0,0 +1,29 @@ +import LZ77 +import Testing + +@Suite +enum CompressionMicro +{ + @Test(arguments: [[], [1], [2, 3], [4, 5, 6]]) + static func Roundtrip(_ bytes:[UInt8]) throws + { + let archive:[UInt8] = Gzip.archive(bytes: bytes[...], level: 10) + #expect(try Gzip.extract(from: archive[...]) == bytes) + } + + @Test + static func InParts() throws + { + var deflator:Gzip.Deflator = .init(level: 13, exponent: 15) + deflator.push([1], last: false) + deflator.push([2], last: true) + + var archive:[UInt8] = [] + while let part:[UInt8] = deflator.pull() + { + archive += part + } + + #expect(try Gzip.extract(from: archive[...]) == [1, 2]) + } +} diff --git a/Sources/LZ77Tests/HardwareAcceleration.swift b/Sources/LZ77Tests/HardwareAcceleration.swift new file mode 100644 index 00000000..ac11bb0c --- /dev/null +++ b/Sources/LZ77Tests/HardwareAcceleration.swift @@ -0,0 +1,50 @@ +#if DEBUG +@testable +import LZ77 +import Testing + +@Suite +enum HardwareAcceleration +{ + @Test + static func DictionarySemantics() + { + let dictionary:F14.HashTable = .init(exponent: 10) + + #expect(nil == dictionary.update(key: 0, value: 1)) + #expect(nil == dictionary.update(key: 1, value: 2)) + #expect(dictionary.update(key: 0, value: 3) == 1) + #expect(nil == dictionary.update(key: 2, value: 4)) + #expect(nil != dictionary.remove(key: 1, value: 5)) + #expect(dictionary.update(key: 1, value: 6) == 2) + #expect(nil != dictionary.remove(key: 1, value: 6)) + #expect(nil == dictionary.update(key: 1, value: 7)) + + var a:F14.HashTable = .init(exponent: 15), + b:[UInt32: UInt16] = [:] + for i:UInt16 in ((0 ... .max).map{ $0 & 0x00ff }) + { + let key:UInt32 = .random(in: 0 ... 1000) + + #expect(a.update(key: key, value: i) == b.updateValue(i, forKey: key)) + } + for i:UInt16 in ((0 ... .max).map{ $0 & 0x00ff }) + { + let key:UInt32 = .random(in: 0 ... 1000) + + if b[key] == i + { + b[key] = nil + } + + a.remove(key: key, value: i) + } + for i:UInt16 in ((0 ... .max).map{ $0 & 0x00ff }) + { + let key:UInt32 = .random(in: 0 ... 1000) + + #expect(a.update(key: key, value: i) == b.updateValue(i, forKey: key)) + } + } +} +#endif diff --git a/Sources/LZ77Tests/Main.Bitstreams.swift b/Sources/LZ77Tests/Main.Bitstreams.swift deleted file mode 100644 index 52f52504..00000000 --- a/Sources/LZ77Tests/Main.Bitstreams.swift +++ /dev/null @@ -1,105 +0,0 @@ -#if DEBUG -@testable -import LZ77 -import Testing_ - -extension Main -{ - enum Bitstreams - { - } -} -extension Main.Bitstreams:TestBattery -{ - static - func run(tests:TestGroup) - { - if let tests:TestGroup = tests / "Decode" - { - var bits:LZ77.InflatorIn = - [ - 0b1001_1110, - 0b1111_0110, - 0b0010_0011, - ] - tests.expect(bits[ 0] ==? 0b1111_0110_1001_1110) - tests.expect(bits[ 1] ==? 0b1_1111_0110_1001_111) - tests.expect(bits[ 2] ==? 0b11_1111_0110_1001_11) - tests.expect(bits[ 3] ==? 0b011_1111_0110_1001_1) - tests.expect(bits[ 4] ==? 0b0011_1111_0110_1001) - tests.expect(bits[ 5] ==? 0b0_0011_1111_0110_100) - tests.expect(bits[ 6] ==? 0b10_0011_1111_0110_10) - tests.expect(bits[ 7] ==? 0b010_0011_1111_0110_1) - tests.expect(bits[ 8] ==? 0b0010_0011_1111_0110) - tests.expect(bits[ 9] ==? 0b0_0010_0011_1111_011) - tests.expect(bits[23] ==? 0b0000_0000_0000_0000) - - tests.expect(bits[0, count: 4, as: Int.self] ==? 0b1110) - tests.expect(bits[1, count: 4, as: Int.self] ==? 0b1_111) - tests.expect(bits[1, count: 6, as: Int.self] ==? 0b001_111) - tests.expect(bits[2, count: 6, as: Int.self] ==? 0b1001_11) - tests.expect(bits[2, count: 16, as: Int.self] ==? 0b11_1111_0110_1001_11) - - // test rebase - // { 0010_0011, 1111_0110, 1001_1110 } - // ^ - // b = 20 - // -> - // { 0001_1000, 1010_1101, 0010_0011 } - // ^ - // b = 4 - var b:Int = 20 - - bits.rebase([0b1010_1101, 0b0001_1000], pointer: &b) - - tests.expect(bits[b ] ==? 0b1000_1010_1101_0010) - tests.expect(bits[b + 1] ==? 0b1_1000_1010_1101_001) - - // test rebase - // { 0001_1000, 1010_1101, 0010_0011 } - // ^ - // b = 4 - // { 1111_1100, 0011_1111, 0001_1000, 1010_1101, 0010_0011 } - bits.rebase([0b0011_1111, 0b1111_1100], pointer: &b) - - tests.expect(bits[b ] ==? 0b1000_1010_1101_0010) - tests.expect(bits[b + 8] ==? 0b1111_0001_1000_1010) - } - if let tests:TestGroup = tests / "Encode" - { - var bits:LZ77.DeflatorOut = .init(hint: 4) - - bits.append(0b11, count: 2) - bits.append(0b01_10, count: 4) - - bits.append(0b0110, count: 0) - - bits.append(0b1_1111_11, count: 7) - bits.append(0b1010_1010_1010_101, count: 15) - bits.append(0b000, count: 3) - bits.append(0b0_1101_1, count: 6) - bits.append(0b1_0000_0000_111, count: 12) - - var encoded:[UInt8] = [] - - while let chunk:[UInt8] = bits.pop() - { - encoded.append(contentsOf: chunk) - } - - encoded.append(contentsOf: bits.pull()) - - tests.expect(encoded ..? - [ - 0b1101_1011, - 0b1011_1111, - 0b1010_1010, - 0b1000_1010, - 0b1110_1101, - 0b0000_0000, - 0b0000_0001 - ]) - } - } -} -#endif diff --git a/Sources/LZ77Tests/Main.Compression.swift b/Sources/LZ77Tests/Main.Compression.swift deleted file mode 100644 index 718b4eb9..00000000 --- a/Sources/LZ77Tests/Main.Compression.swift +++ /dev/null @@ -1,94 +0,0 @@ -import LZ77 -import Testing_ - -extension Main -{ - enum Compression - { - } -} -extension Main.Compression:TestBattery -{ - static - func run(tests:TestGroup) - { - for (level, name):(Int, String) in - [ - (4, "Greedy"), - (7, "Lazy"), - (9, "Full"), - ] - { - guard - let tests:TestGroup = tests / name - else - { - continue - } - for count:Int in [5, 15, 100, 200, 2000, 5000] - { - guard - let tests:TestGroup = tests / "\(count)" - else - { - continue - } - - tests.do - { - let input:[UInt8] = (0 ..< count).map{ _ in .random(in: .min ... .max) } - - var deflator:LZ77.Deflator = .init(level: level, exponent: 8, hint: 16) - deflator.push(input[...], last: true) - - var compressed:[UInt8] = [] - while let part:[UInt8] = deflator.pull() - { - compressed += part - } - - var inflator:LZ77.Inflator = .init() - try inflator.push(compressed[...]) - - let output:[UInt8] = inflator.pull() - - tests.expect(input ..? output) - } - } - } - - if let tests:TestGroup = tests / "Gzip" - { - for count:Int in [5, 15, 100, 200, 2000, 5000] - { - guard - let tests:TestGroup = tests / "\(count)" - else - { - continue - } - - tests.do - { - let input:[UInt8] = (0 ..< count).map{ _ in .random(in: .min ... .max) } - - var deflator:Gzip.Deflator = .init(level: 7, exponent: 15, hint: 64 << 10) - deflator.push(input[...], last: true) - - var compressed:[UInt8] = [] - while let part:[UInt8] = deflator.pull() - { - compressed += part - } - - var inflator:Gzip.Inflator = .init() - try inflator.push(compressed[...]) - - let output:[UInt8] = inflator.pull() - - tests.expect(input ..? output) - } - } - } - } -} diff --git a/Sources/LZ77Tests/Main.CompressionMicro.swift b/Sources/LZ77Tests/Main.CompressionMicro.swift deleted file mode 100644 index 47ba852d..00000000 --- a/Sources/LZ77Tests/Main.CompressionMicro.swift +++ /dev/null @@ -1,59 +0,0 @@ -import LZ77 -import Testing_ - -extension Main -{ - enum CompressionMicro - { - } -} -extension Main.CompressionMicro:TestBattery -{ - static - func run(tests:TestGroup) - { - if let tests:TestGroup = tests / "Empty" - { - Self.roundtrip(bytes: [], with: tests) - } - if let tests:TestGroup = tests / "OneByte" - { - Self.roundtrip(bytes: [1], with: tests) - } - if let tests:TestGroup = tests / "TwoBytes" - { - Self.roundtrip(bytes: [2, 3], with: tests) - } - if let tests:TestGroup = tests / "ThreeBytes" - { - Self.roundtrip(bytes: [4, 5, 6], with: tests) - } - if let tests:TestGroup = tests / "InParts" - { - var deflator:Gzip.Deflator = .init(level: 13, exponent: 15) - deflator.push([1], last: false) - deflator.push([2], last: true) - - var archive:[UInt8] = [] - while let part:[UInt8] = deflator.pull() - { - archive += part - } - - tests.do - { - tests.expect(try Gzip.extract(from: archive[...]) ..? [1, 2]) - } - } - } - - private static - func roundtrip(bytes:[UInt8], with tests:TestGroup) - { - let archive:[UInt8] = Gzip.archive(bytes: bytes[...], level: 10) - tests.do - { - tests.expect(try Gzip.extract(from: archive[...]) ..? bytes) - } - } -} diff --git a/Sources/LZ77Tests/Main.F14.swift b/Sources/LZ77Tests/Main.F14.swift deleted file mode 100644 index affa48e3..00000000 --- a/Sources/LZ77Tests/Main.F14.swift +++ /dev/null @@ -1,55 +0,0 @@ -#if DEBUG -@testable -import LZ77 -import Testing_ - -extension Main -{ - enum F14 - { - } -} -extension Main.F14:TestBattery -{ - static - func run(tests:TestGroup) - { - let dictionary:F14.HashTable = .init(exponent: 10) - - tests.expect(nil: dictionary.update(key: 0, value: 1)) - tests.expect(nil: dictionary.update(key: 1, value: 2)) - tests.expect(dictionary.update(key: 0, value: 3) ==? 1) - tests.expect(nil: dictionary.update(key: 2, value: 4)) - tests.expect(value: dictionary.remove(key: 1, value: 5)) - tests.expect(dictionary.update(key: 1, value: 6) ==? 2) - tests.expect(value: dictionary.remove(key: 1, value: 6)) - tests.expect(nil: dictionary.update(key: 1, value: 7)) - - var a:F14.HashTable = .init(exponent: 15), - b:[UInt32: UInt16] = [:] - for i:UInt16 in ((0 ... .max).map{ $0 & 0x00ff }) - { - let key:UInt32 = .random(in: 0 ... 1000) - - tests.expect(a.update(key: key, value: i) ==? b.updateValue(i, forKey: key)) - } - for i:UInt16 in ((0 ... .max).map{ $0 & 0x00ff }) - { - let key:UInt32 = .random(in: 0 ... 1000) - - if b[key] == i - { - b[key] = nil - } - - a.remove(key: key, value: i) - } - for i:UInt16 in ((0 ... .max).map{ $0 & 0x00ff }) - { - let key:UInt32 = .random(in: 0 ... 1000) - - tests.expect(a.update(key: key, value: i) ==? b.updateValue(i, forKey: key)) - } - } -} -#endif diff --git a/Sources/LZ77Tests/Main.Matching.swift b/Sources/LZ77Tests/Main.Matching.swift deleted file mode 100644 index 17f27d0c..00000000 --- a/Sources/LZ77Tests/Main.Matching.swift +++ /dev/null @@ -1,107 +0,0 @@ -#if DEBUG -@testable -import LZ77 -import Testing_ - -extension Main -{ - enum Matching - { - } -} -extension Main.Matching:TestBattery -{ - static - func run(tests:TestGroup) - { - let segments:[[UInt8]] = - [ - [1, 2, 3, 3, 1, 2, 3, 3, 1, 2, 3, 1, 2, 2, 2, 2, 2, 2, 0, 1, 2], - [2, 2, 2, 2, 0, 1, 2, 2, 0, 0, 0, 0, 2, 3, 2, 1, 2, 3, 3, 1, 5], - [1, 1, 3, 3, 1, 2, 3, 1, 2, 4, 4, 2, 1] - ] - var input:LZ77.DeflatorIn = .init() - var window:LZ77.DeflatorWindow = .init(exponent: 4) - var output:[[UInt8]] = [] - for (s, segment):(Int, [UInt8]) in segments.enumerated() - { - input.enqueue(contentsOf: segment[...]) - - let lookahead:Int = (s == segments.count - 1 ? 0 : 10) - while window.endIndex < 0, input.count > lookahead - { - window.initialize(with: input.dequeue()) - } - while input.count > lookahead - { - let head:(index:Int, next:UInt16?) = window.update(with: input.dequeue()) - if let match:(run:Int, distance:Int) = window.match(from: head, - lookahead: input, - attempts: .max, - goal: .max) - { - var run:[UInt8] = [window.literal] - for _:Int in 1 ..< match.run - { - window.update(with: input.dequeue()) - run.append(window.literal) - } - output.append(run) - } - else - { - output.append([window.literal]) - } - } - - guard s == segments.count - 1 - else - { - continue - } - - // epilogue: get the matches still sitting in the pipeline - let epilogue:Int = -3 - min(0, window.endIndex) - while input.count > epilogue - { - window.update(with: input.dequeue()) - output.append([window.literal]) - } - } - tests.expect(output ..? - [ - [1], - [2], - [3], - [3], - [1, 2, 3, 3, 1, 2, 3], - [1], - [2], [2], [2], [2], [2], [2], - [0], - [1, 2, 2, 2, 2, 2], - [0], - [1], - [2], [2], - [0], [0], [0], [0], - [2], - [3], - [2], - [1], - [2], - [3], [3], - [1], - [5], - [1], [1], - [3], [3], - [1], - [2], - [3], - [1], - [2], - [4], [4], - [2], - [1] - ]) - } -} -#endif diff --git a/Sources/LZ77Tests/Main.swift b/Sources/LZ77Tests/Main.swift deleted file mode 100644 index e77ce0bf..00000000 --- a/Sources/LZ77Tests/Main.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Testing_ - -@main -enum Main:TestMain -{ - #if DEBUG - static - let all:[any TestBattery.Type] = - [ - F14.self, - Bitstreams.self, - Matching.self, - CompressionMicro.self, - Compression.self, - ] - #else - static - let all:[any TestBattery.Type] = - [ - CompressionMicro.self, - Compression.self - ] - #endif -} diff --git a/Sources/PNGCompressionTests/Compression.swift b/Sources/PNGCompressionTests/Compression.swift new file mode 100644 index 00000000..86e9b715 --- /dev/null +++ b/Sources/PNGCompressionTests/Compression.swift @@ -0,0 +1,85 @@ +import PNG +import Testing + +@Suite +enum Compression +{ + @Test(arguments: [ + "v8-monochrome-photographic", + "v8-monochrome-nonphotographic", + "v16-monochrome-photographic", + "v16-monochrome-nonphotographic", + "va8-monochrome-photographic", + "va8-monochrome-nonphotographic", + "va16-monochrome-photographic", + "va16-monochrome-nonphotographic", + "indexed8-monochrome-photographic", + "indexed8-color-photographic", + "indexed8-monochrome-nonphotographic", + "indexed8-color-nonphotographic", + "rgb8-monochrome-photographic", + "rgb8-color-photographic", + "rgb8-monochrome-nonphotographic", + "rgb8-color-nonphotographic", + "rgb16-monochrome-photographic", + "rgb16-color-photographic", + "rgb16-monochrome-nonphotographic", + "rgb16-color-nonphotographic", + "rgba8-monochrome-photographic", + "rgba8-color-photographic", + "rgba8-monochrome-nonphotographic", + "rgba8-color-nonphotographic", + "rgba16-monochrome-photographic", + "rgba16-color-photographic", + "rgba16-monochrome-nonphotographic", + "rgba16-color-nonphotographic", + ]) + static func Encode(_ name:String) throws + { + let path:(png:String, out:String) = + ( + "Tests/Baselines/\(name).png", + "Tests/Outputs/\(name).png" + ) + + guard let baseline:(image:PNG.Image, size:Int) = + (try System.File.Source.open(path: path.png) + { + (try .decompress(stream: &$0), $0.count!) + }) + else + { + Issue.record("failed to open file '\(path.png)'") + return + } + + try baseline.image.compress(path: path.out, level: 9) + + guard let output:(image:PNG.Image, size:Int) = + (try System.File.Source.open(path: path.out) + { + (try .decompress(stream: &$0), $0.count!) + }) + else + { + Issue.record("failed to open file '\(path.out)'") + return + } + + let pixels:[PNG.RGBA] = baseline.image.unpack(as: PNG.RGBA.self) + + print() + print(name) + print(""" + baseline: \(baseline.size >> 10) KB, \ + output: \(output.size >> 10) KB, \ + ratio: \(Double.init(output.size) / Double.init(baseline.size)) + """) + + for (i, pair):(Int, (PNG.RGBA, PNG.RGBA)) in + zip(output.image.unpack(as: PNG.RGBA.self), pixels).enumerated() + { + #expect(pair.0 == pair.1, "mismatch in pixel \(i)") + } + } +} diff --git a/Sources/PNGCompressionTests/Main.swift b/Sources/PNGCompressionTests/Main.swift deleted file mode 100644 index 15660ba9..00000000 --- a/Sources/PNGCompressionTests/Main.swift +++ /dev/null @@ -1,172 +0,0 @@ -import PNG -import Testing_ - -struct _TestFailure:Error -{ - let message:String -} - -@main -enum Main:TestMain, TestBattery -{ - static - func run(tests:TestGroup) - { - let suite:[(name:String, members:[String])] = - [ - ( - "v8", - [ - "v8-monochrome-photographic", - "v8-monochrome-nonphotographic", - ] - ), - ( - "v16", - [ - "v16-monochrome-photographic", - "v16-monochrome-nonphotographic", - ] - ), - ( - "va8", - [ - "va8-monochrome-photographic", - "va8-monochrome-nonphotographic", - ] - ), - ( - "va16", - [ - "va16-monochrome-photographic", - "va16-monochrome-nonphotographic", - ] - ), - ( - "indexed8", - [ - "indexed8-monochrome-photographic", - "indexed8-color-photographic", - "indexed8-monochrome-nonphotographic", - "indexed8-color-nonphotographic", - ] - ), - ( - "rgb8", - [ - "rgb8-monochrome-photographic", - "rgb8-color-photographic", - "rgb8-monochrome-nonphotographic", - "rgb8-color-nonphotographic", - ] - ), - ( - "rgb16", - [ - "rgb16-monochrome-photographic", - "rgb16-color-photographic", - "rgb16-monochrome-nonphotographic", - "rgb16-color-nonphotographic", - ] - ), - ( - "rgba8", - [ - "rgba8-monochrome-photographic", - "rgba8-color-photographic", - "rgba8-monochrome-nonphotographic", - "rgba8-color-nonphotographic", - ] - ), - ( - "rgba16", - [ - "rgba16-monochrome-photographic", - "rgba16-color-photographic", - "rgba16-monochrome-nonphotographic", - "rgba16-color-nonphotographic", - ] - ), - ] - for (name, members):(String, [String]) in suite - { - guard - let tests:TestGroup = tests / name - else - { - continue - } - - for member:String in members - { - guard - let tests:TestGroup = tests / member - else - { - continue - } - - tests.do - { - try Self.encode(member).get() - } - } - } - } - - static - func encode(_ name:String) -> Result - { - let path:(png:String, out:String) = - ( - "Tests/Baselines/\(name).png", - "Tests/Outputs/\(name).png" - ) - - do - { - guard let baseline:(image:PNG.Image, size:Int) = - (try System.File.Source.open(path: path.png) - { - (try .decompress(stream: &$0), $0.count!) - }) - else - { - return .failure(.init(message: "failed to open file '\(path.png)'")) - } - - try baseline.image.compress(path: path.out, level: 9) - - guard let output:(image:PNG.Image, size:Int) = - (try System.File.Source.open(path: path.out) - { - (try .decompress(stream: &$0), $0.count!) - }) - else - { - return .failure(.init(message: "failed to open file '\(path.out)'")) - } - - let pixels:[PNG.RGBA] = baseline.image.unpack(as: PNG.RGBA.self) - print() - print(name) - print("baseline: \(baseline.size >> 10) KB, output: \(output.size >> 10) KB, ratio: \(Double.init(output.size) / Double.init(baseline.size))") - - for (i, pair):(Int, (PNG.RGBA, PNG.RGBA)) in - zip(output.image.unpack(as: PNG.RGBA.self), pixels).enumerated() - { - guard pair.0 == pair.1 - else - { - return .failure(.init(message: "pixel \(i) has value \(pair.0) (expected \(pair.1))")) - } - } - - return .success(()) - } - catch - { - return .failure(.init(message: "\(error)")) - } - } -} diff --git a/Sources/PNGIntegrationTests/Array (ext).swift b/Sources/PNGIntegrationTests/Array (ext).swift new file mode 100644 index 00000000..1239ccfa --- /dev/null +++ b/Sources/PNGIntegrationTests/Array (ext).swift @@ -0,0 +1,8 @@ +extension Array where Element == UInt8 +{ + func load(littleEndian:T.Type, as type:U.Type = U.self, at byte:Int) -> U + where T:FixedWidthInteger, U:BinaryInteger + { + return self[byte ..< byte + MemoryLayout.size].load(littleEndian: T.self, as: U.self) + } +} diff --git a/Sources/PNGIntegrationTests/ArraySlice (ext).swift b/Sources/PNGIntegrationTests/ArraySlice (ext).swift new file mode 100644 index 00000000..a58d32a5 --- /dev/null +++ b/Sources/PNGIntegrationTests/ArraySlice (ext).swift @@ -0,0 +1,29 @@ +extension ArraySlice where Element == UInt8 +{ + func load(littleEndian:T.Type, as type:U.Type = U.self) -> U + where T:FixedWidthInteger, U:BinaryInteger + { + self.withUnsafeBufferPointer + { + (buffer:UnsafeBufferPointer) in + + assert(buffer.count >= MemoryLayout.size, + "attempt to load \(T.self) from slice of size \(buffer.count)") + + var storage:T = .init() + let value:T = withUnsafeMutablePointer(to: &storage) + { + $0.deinitialize(count: 1) + + let source:UnsafeRawPointer = .init(buffer.baseAddress!), + raw:UnsafeMutableRawPointer = .init($0) + + raw.copyMemory(from: source, byteCount: MemoryLayout.size) + + return raw.load(as: T.self) + } + + return U.init(T.init(littleEndian: value)) + } + } +} diff --git a/Sources/PNGIntegrationTests/ErrorHandling.swift b/Sources/PNGIntegrationTests/ErrorHandling.swift new file mode 100644 index 00000000..3d8ca8cb --- /dev/null +++ b/Sources/PNGIntegrationTests/ErrorHandling.swift @@ -0,0 +1,90 @@ +import PNG +import Testing + +@Suite +enum ErrorHandling +{ + @Test(arguments: [ + "xs1n0g01", + "xs2n0g01", + "xs4n0g01", + "xs7n0g01", + "xcrn0g04", + "xlfn0g04" + ]) + static func InvalidSignatures(_ name:String) throws + { + #expect(throws: PNG.LexingError.self) + { + try Self.decode(name) + } + } + + @Test + static func InvalidIHDRChecksum() throws + { + do + { + try decode("xhdn0g08") + } + catch PNG.LexingError.invalidChunkChecksum(declared: 1129534797, computed: 1443964200) + { + } + } + + @Test + static func InvalidIDATChecksum() throws + { + do + { + try decode("xcsn0g01") + } + catch PNG.LexingError.invalidChunkChecksum(declared: 1129534797, computed: 3492746441) + { + } + } + + @Test(arguments: [ + ("xc1n0g08", ( 8, 1)), + ("xc9n2c08", ( 8, 9)), + ("xd0n2c08", ( 0, 2)), + ("xd3n2c08", ( 3, 2)), + ("xd9n2c08", (99, 2)), + ]) + static func InvalidColorFormat(_ name:String, _ code:(UInt8, UInt8)) throws + { + do + { + try Self.decode(name) + } + catch PNG.ParsingError.invalidHeaderPixelFormatCode((code.0, code.1)) + { + } + } + + @Test(arguments: ["xdtn0g01"]) + static func MissingIDAT(_ name:String) throws + { + do + { + try decode(name) + } + catch PNG.DecodingError.required(chunk: .IDAT, before: .IEND) + { + } + } + + private + static func decode(_ name:String) throws + { + let path:String = "Sources/PNGIntegrationTests/Inputs/Invalid/\(name).png" + if let _:PNG.Image = try .decompress(path: path) + { + Issue.record("file '\(path)' is invalid, but decoded without errors") + } + else + { + Issue.record("failed to read file '\(path)'") + } + } +} diff --git a/Sources/PNGIntegrationTests/Main.swift b/Sources/PNGIntegrationTests/Main.swift deleted file mode 100644 index be05e9e4..00000000 --- a/Sources/PNGIntegrationTests/Main.swift +++ /dev/null @@ -1,695 +0,0 @@ -import PNG -import Testing_ - -struct _TestFailure:Error -{ - let message:String -} - -@main -enum Main:TestMain, TestBattery -{ - static - func run(tests:TestGroup) - { - let suite:[(name:String, members:[String])] = - [ - ( - "basic", - [ - "PngSuite", - - "basn0g01", - "basn0g02", - "basn0g04", - "basn0g08", - "basn0g16", - "basn2c08", - "basn2c16", - "basn3p01", - "basn3p02", - "basn3p04", - "basn3p08", - "basn4a08", - "basn4a16", - "basn6a08", - "basn6a16" - ] - ), - ( - "interlaced", - [ - "basi0g01", - "basi0g02", - "basi0g04", - "basi0g08", - "basi0g16", - "basi2c08", - "basi2c16", - "basi3p01", - "basi3p02", - "basi3p04", - "basi3p08", - "basi4a08", - "basi4a16", - "basi6a08", - "basi6a16" - ] - ), - ( - "odd-sizes", - [ - "s01i3p01", - "s01n3p01", - "s02i3p01", - "s02n3p01", - "s03i3p01", - "s03n3p01", - "s04i3p01", - "s04n3p01", - "s05i3p02", - "s05n3p02", - "s06i3p02", - "s06n3p02", - "s07i3p02", - "s07n3p02", - "s08i3p02", - "s08n3p02", - "s09i3p02", - "s09n3p02", - "s32i3p04", - "s32n3p04", - "s33i3p04", - "s33n3p04", - "s34i3p04", - "s34n3p04", - "s35i3p04", - "s35n3p04", - "s36i3p04", - "s36n3p04", - "s37i3p04", - "s37n3p04", - "s38i3p04", - "s38n3p04", - "s39i3p04", - "s39n3p04", - "s40i3p04", - "s40n3p04" - ] - ), - ( - "backgrounds", - [ - "bgai4a08", - "bgai4a16", - "bgan6a08", - "bgan6a16", - "bgbn4a08", - "bggn4a16", - "bgwn6a08", - "bgyn6a16" - ] - ), - ( - "transparency", - [ - "tbbn0g04", - "tbbn2c16", - "tbbn3p08", - "tbgn2c16", - "tbgn3p08", - "tbrn2c08", - "tbwn0g16", - "tbwn3p08", - "tbyn3p08", - "tm3n3p02", - "tp0n0g08", - "tp0n2c08", - "tp0n3p08", - "tp1n3p08" - ] - ), - ( - "gamma", - [ - "g03n0g16", - "g03n2c08", - "g03n3p04", - "g04n0g16", - "g04n2c08", - "g04n3p04", - "g05n0g16", - "g05n2c08", - "g05n3p04", - "g07n0g16", - "g07n2c08", - "g07n3p04", - "g10n0g16", - "g10n2c08", - "g10n3p04", - "g25n0g16", - "g25n2c08", - "g25n3p04" - ] - ), - ( - "filters", - [ - "f00n0g08", - "f00n2c08", - "f01n0g08", - "f01n2c08", - "f02n0g08", - "f02n2c08", - "f03n0g08", - "f03n2c08", - "f04n0g08", - "f04n2c08", - "f99n0g04" - ] - ), - ( - "palettes", - [ - "pp0n2c16", - "pp0n6a08", - "ps1n0g08", - "ps1n2c16", - "ps2n0g08", - "ps2n2c16" - ] - ), - ( - "ancillary-chunks", - [ - "ccwn2c08", - "ccwn3p08", - "cdfn2c08", - "cdhn2c08", - "cdsn2c08", - "cdun2c08", - "ch1n3p04", - "ch2n3p08", - "cm0n0g04", - "cm7n0g04", - "cm9n0g04", - "cs3n2c16", - "cs3n3p08", - "cs5n2c08", - "cs5n3p08", - "cs8n2c08", - "cs8n3p08", - "ct0n0g04", - "ct1n0g04", - "cten0g04", - "ctfn0g04", - "ctgn0g04", - "cthn0g04", - "ctjn0g04", - "ctzn0g04" - ] - ), - ( - "chunk-ordering", - [ - "oi1n0g16", - "oi1n2c16", - "oi2n0g16", - "oi2n2c16", - "oi4n0g16", - "oi4n2c16", - "oi9n0g16", - "oi9n2c16" - ] - ), - ( - "lz77-compression", - [ - "z00n2c08", - "z03n2c08", - "z06n2c08", - "z09n2c08" - ] - ), - ] - - let iOS:[String] = - [ - "PngSuite", - "basi2c08", - "basi6a08", - "basn2c08", - "basn6a08", - "bgan6a08", - "bgwn6a08", - "ccwn2c08", - "cdfn2c08", - "cdhn2c08", - "cdsn2c08", - "cdun2c08", - "cs5n2c08", - "cs8n2c08", - "f00n2c08", - "f01n2c08", - "f02n2c08", - "f03n2c08", - "f04n2c08", - "g03n2c08", - "g04n2c08", - "g05n2c08", - "g07n2c08", - "g10n2c08", - "g25n2c08", - "pp0n6a08", - "tbrn2c08", - "tp0n2c08", - "z00n2c08", - "z03n2c08", - "z06n2c08", - "z09n2c08", - ] - - if let tests:TestGroup = tests / "ErrorHandling" - { - tests.do { try Self.errorHandling().get() } - } - - for (name, members):(String, [String]) in suite - { - guard - let tests:TestGroup = tests / "Decode-\(name)" - else - { - continue - } - - for member:String in members - { - guard - let tests:TestGroup = tests / member - else - { - continue - } - - tests.do - { - try Self.decode(member, subdirectory: "Common").get() - } - } - } - - if let tests:TestGroup = tests / "Decode" / "iPhoneOptimized" - { - for member:String in iOS - { - guard - let tests:TestGroup = tests / member - else - { - continue - } - - tests.do - { - try Self.decode(member, subdirectory: "iOS").get() - } - } - } - - if let tests:TestGroup = tests / "Encode" - { - if let tests:TestGroup = tests / "iPhoneOptimized" - { - for member:String in iOS - { - guard - let tests:TestGroup = tests / member - else - { - continue - } - - tests.do - { - try Self.encode(member, subdirectory: "iOS", level: 13).get() - } - } - } - for level:Int in [4, 7, 10] - { - for (name, members):(String, [String]) in suite - { - guard - let tests:TestGroup = tests / "\(level)" / "\(name)" - else - { - continue - } - - for member:String in members - { - guard - let tests:TestGroup = tests / member - else - { - continue - } - - tests.do - { - try Self.encode(member, subdirectory: "Common", level: level).get() - } - } - } - } - } - - } - - static - func decode(_ name:String, subdirectory:String) -> Result - { - let path:(in:String, rgba:String) = - ( - "Sources/PNGIntegrationTests/Inputs/\(subdirectory)/\(name).png", - "Sources/PNGIntegrationTests/RGBA/\(name).png.rgba" - ) - return Self.decode(path: path, premultiplied: subdirectory == "iOS") - } - - static - func decode(path:(in:String, rgba:String), premultiplied:Bool) - -> Result - { - do - { - guard let rectangular:PNG.Image = try .decompress(path: path.in) - else - { - return .failure(.init(message: "failed to open file '\(path.in)'")) - } - - let image:[PNG.RGBA] = rectangular.unpack(as: PNG.RGBA.self) - - // if !Global.options.contains(.compact) - // { - // print(Self.terminal(image: image, size: rectangular.size)) - // print(rectangular.metadata) - // print() - // } - - guard let result:[PNG.RGBA]? = (System.File.Source.open(path: path.rgba) - { - let pixels:Int = rectangular.size.x * rectangular.size.y, - bytes:Int = pixels * MemoryLayout>.stride - - guard let data:[UInt8] = $0.read(count: bytes) - else - { - return nil - } - - return (0 ..< pixels).map - { - let r:UInt16 = data.load(littleEndian: UInt16.self, as: UInt16.self, at: $0 << 3), - g:UInt16 = data.load(littleEndian: UInt16.self, as: UInt16.self, at: $0 << 3 | 2), - b:UInt16 = data.load(littleEndian: UInt16.self, as: UInt16.self, at: $0 << 3 | 4), - a:UInt16 = data.load(littleEndian: UInt16.self, as: UInt16.self, at: $0 << 3 | 6) - - let pixel:PNG.RGBA = .init(r, g, b, a) - // have to manually premultiply since the CgBI formula does the - // multiplication in 8-bit precision - if premultiplied - { - return pixel.premultiplied(as: UInt8.self) - } - else - { - return pixel - } - } - }) - else - { - return .failure(.init(message: "failed to open file '\(path.rgba)'")) - } - - guard let reference:[PNG.RGBA] = result - else - { - return .failure(.init(message: "failed to read file '\(path.rgba)'")) - } - - for (i, pair):(Int, (PNG.RGBA, PNG.RGBA)) in - zip(image, reference).enumerated() - { - guard pair.0 == pair.1 - else - { - return .failure(.init(message: "pixel \(i) has value \(pair.0) (expected \(pair.1))")) - } - } - - return .success(()) - } - catch let error - { - return .failure(.init(message: "\(error)")) - } - } - - // TODO: make error types ``Equatable``, and use expected failure API from - // the ``/Testing`` module. - static - func printError(_ error:E) where E:PNG.Error - { - // guard Global.options.contains(.printExpectedFailures) - // else - // { - // return - // } - - // var width:Int - // { - // 80 - // } - - // let accent:(Double, Double, Double) = (1.0, 0.6, 0.3) - - // let heading:String = .pad(.init("\(E.namespace): \(error.message)"), right: width) - // print(String.highlight("\(String.bold)\(heading)\(String.reset)", bg: accent)) - // if let details:String = error.details - // { - // // wrap text - // let characters:[Character] = .init(details) - // for start:Int in stride(from: 0, to: characters.count, by: width - 4) - // { - // let end:Int = min(start + width - 4, characters.count) - // print(" \(String.color(.init(characters[start ..< end]), fg: accent))") - // } - // } - // print() - } - - static - func errorHandling() -> Result - { - func decode(_ name:String) throws -> Result? - { - let path:String = "Sources/PNGIntegrationTests/Inputs/Invalid/\(name).png" - if let _:PNG.Image = try .decompress(path: path) - { - return .failure(.init(message: "file '\(path)' is invalid, but decoded without errors")) - } - else - { - return .failure(.init(message: "failed to read file '\(path)'")) - } - } - - // invalid signatures - for name:String in - [ - "xs1n0g01", "xs2n0g01", "xs4n0g01", "xs7n0g01", - "xcrn0g04", "xlfn0g04" - ] - { - do - { - if let result:Result = try decode(name) - { - return result - } - } - catch PNG.LexingError.invalidSignature(let signature) - { - Self.printError(PNG.LexingError.invalidSignature(signature)) - } - catch let error - { - return .failure(.init(message: "\(error)")) - } - } - - // invalid ihdr checksum - do - { - if let result:Result = try decode("xhdn0g08") - { - return result - } - } - catch PNG.LexingError.invalidChunkChecksum(declared: 1129534797, computed: 1443964200) - { - Self.printError( - PNG.LexingError.invalidChunkChecksum(declared: 1129534797, computed: 1443964200)) - } - catch let error - { - return .failure(.init(message: "\(error)")) - } - - // invalid color format - for (name, code):(String, (UInt8, UInt8)) in - [ - ("xc1n0g08", ( 8, 1)), - ("xc9n2c08", ( 8, 9)), - ("xd0n2c08", ( 0, 2)), - ("xd3n2c08", ( 3, 2)), - ("xd9n2c08", (99, 2)), - ] - { - do - { - if let result:Result = try decode(name) - { - return result - } - } - catch let error - { - // need to work around compiler bug preventing tuple matching - guard case PNG.ParsingError.invalidHeaderPixelFormatCode(let expected) = error, - expected == code - else - { - return .failure(.init(message: "\(error)")) - } - - Self.printError(PNG.ParsingError.invalidHeaderPixelFormatCode(expected)) - } - } - - // missing idat - do - { - if let result:Result = try decode("xdtn0g01") - { - return result - } - } - catch PNG.DecodingError.required(chunk: .IDAT, before: .IEND) - { - Self.printError(PNG.DecodingError.required(chunk: .IDAT, before: .IEND)) - } - catch let error - { - return .failure(.init(message: "\(error)")) - } - - // invalid idat checksum - do - { - if let result:Result = try decode("xcsn0g01") - { - return result - } - } - catch PNG.LexingError.invalidChunkChecksum(declared: 1129534797, computed: 3492746441) - { - Self.printError( - PNG.LexingError.invalidChunkChecksum(declared: 1129534797, computed: 3492746441)) - } - catch let error - { - return .failure(.init(message: "\(error)")) - } - - return .success(()) - } - - static - func encode(_ name:String, subdirectory:String, level:Int) -> Result - { - let path:(in:String, rgba:String, out:String) = - ( - "Sources/PNGIntegrationTests/Inputs/\(subdirectory)/\(name).png", - "Sources/PNGIntegrationTests/RGBA/\(name).png.rgba", - "Sources/PNGIntegrationTests/Outputs/\(subdirectory)/\(name).png" - ) - - return Self.encode(path: path, level: level, premultiplied: subdirectory == "iOS") - } - - static - func encode(path:(in:String, rgba:String, out:String), level:Int, premultiplied:Bool) - -> Result - { - do - { - guard let rectangular:PNG.Image = try .decompress(path: path.in) - else - { - return .failure(.init(message: "failed to open file '\(path.in)'")) - } - - try rectangular.compress(path: path.out, level: level) - } - catch let error - { - return .failure(.init(message: "\(error)")) - } - return Self.decode(path: (in: path.out, rgba: path.rgba), premultiplied: premultiplied) - } -} - -fileprivate -extension Array where Element == UInt8 -{ - func load(littleEndian:T.Type, as type:U.Type, at byte:Int) -> U - where T:FixedWidthInteger, U:BinaryInteger - { - return self[byte ..< byte + MemoryLayout.size].load(littleEndian: T.self, as: U.self) - } -} -fileprivate -extension ArraySlice where Element == UInt8 -{ - func load(littleEndian:T.Type, as type:U.Type) -> U - where T:FixedWidthInteger, U:BinaryInteger - { - return self.withUnsafeBufferPointer - { - (buffer:UnsafeBufferPointer) in - - assert(buffer.count >= MemoryLayout.size, - "attempt to load \(T.self) from slice of size \(buffer.count)") - - var storage:T = .init() - let value:T = withUnsafeMutablePointer(to: &storage) - { - $0.deinitialize(count: 1) - - let source:UnsafeRawPointer = .init(buffer.baseAddress!), - raw:UnsafeMutableRawPointer = .init($0) - - raw.copyMemory(from: source, byteCount: MemoryLayout.size) - - return raw.load(as: T.self) - } - - return U(T(littleEndian: value)) - } - } -} diff --git a/Sources/PNGIntegrationTests/Roundtripping.swift b/Sources/PNGIntegrationTests/Roundtripping.swift new file mode 100644 index 00000000..b8c84a53 --- /dev/null +++ b/Sources/PNGIntegrationTests/Roundtripping.swift @@ -0,0 +1,513 @@ +import PNG +import Testing + +@Suite +enum Roundtripping +{ + @Test(arguments: Self.basic) + static func DecodeBasic(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.interlaced) + static func DecodeInterlaced(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.oddSizes) + static func DecodeOddSizes(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.backgrounds) + static func DecodeBackgrounds(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.transparency) + static func DecodeTransparency(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.gamma) + static func DecodeGamma(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.filters) + static func DecodeFilters(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.palettes) + static func DecodePalettes(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.ancillary) + static func DecodeAncillary(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.chunkOrdering) + static func DecodeChunkOrdering(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.lz77) + static func DecodeLZ77(_ name:String) throws + { + try Self.decode(name, subdirectory: "Common") + } + + @Test(arguments: Self.iOS) + static func DecodeiPhoneOptimized(_ name:String) throws + { + try Self.decode(name, subdirectory: "iOS") + } + + + @Test(arguments: Self.basic, [4, 7, 10]) + static func EncodeBasic(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.interlaced, [4, 7, 10]) + static func EncodeInterlaced(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.oddSizes, [4, 7, 10]) + static func EncodeOddSizes(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.backgrounds, [4, 7, 10]) + static func EncodeBackgrounds(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.transparency, [4, 7, 10]) + static func EncodeTransparency(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.gamma, [4, 7, 10]) + static func EncodeGamma(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.filters, [4, 7, 10]) + static func EncodeFilters(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.palettes, [4, 7, 10]) + static func EncodePalettes(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.ancillary, [4, 7, 10]) + static func EncodeAncillary(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.chunkOrdering, [4, 7, 10]) + static func EncodeChunkOrdering(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.lz77, [4, 7, 10]) + static func EncodeLZ77(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "Common", level: level) + } + + @Test(arguments: Self.iOS, [13]) + static func EncodeiPhoneOptimized(_ name:String, _ level:Int) throws + { + try Self.encode(name, subdirectory: "iOS", level: level) + } +} +extension Roundtripping +{ + private + static func decode(_ name:String, subdirectory:String) throws + { + try Self.decode(path: + ( + "Sources/PNGIntegrationTests/Inputs/\(subdirectory)/\(name).png", + "Sources/PNGIntegrationTests/RGBA/\(name).png.rgba" + ), + premultiplied: subdirectory == "iOS") + } + + private + static func decode(path:(in:String, rgba:String), premultiplied:Bool) throws + { + guard + let rectangular:PNG.Image = try .decompress(path: path.in) + else + { + Issue.record("failed to open file '\(path.in)'") + return + } + + let image:[PNG.RGBA] = rectangular.unpack(as: PNG.RGBA.self) + + // if !Global.options.contains(.compact) + // { + // print(Self.terminal(image: image, size: rectangular.size)) + // print(rectangular.metadata) + // print() + // } + + guard + let result:[PNG.RGBA]? = (System.File.Source.open(path: path.rgba) + { + let pixels:Int = rectangular.size.x * rectangular.size.y, + bytes:Int = pixels * MemoryLayout>.stride + + guard + let data:[UInt8] = $0.read(count: bytes) + else + { + return nil + } + + return (0 ..< pixels).map + { + let r:UInt16 = data.load(littleEndian: UInt16.self, at: $0 << 3), + g:UInt16 = data.load(littleEndian: UInt16.self, at: $0 << 3 | 2), + b:UInt16 = data.load(littleEndian: UInt16.self, at: $0 << 3 | 4), + a:UInt16 = data.load(littleEndian: UInt16.self, at: $0 << 3 | 6) + + let pixel:PNG.RGBA = .init(r, g, b, a) + // have to manually premultiply since the CgBI formula does the + // multiplication in 8-bit precision + if premultiplied + { + return pixel.premultiplied(as: UInt8.self) + } + else + { + return pixel + } + } + }) + else + { + Issue.record("failed to open file '\(path.rgba)'") + return + } + + guard let reference:[PNG.RGBA] = result + else + { + Issue.record("failed to read file '\(path.rgba)'") + return + } + + for (i, pair):(Int, (PNG.RGBA, PNG.RGBA)) in + zip(image, reference).enumerated() + { + #expect(pair.0 == pair.1, "mismatch in pixel \(i)") + } + } +} +extension Roundtripping +{ + private + static func encode(_ name:String, subdirectory:String, level:Int) throws + { + try Self.encode(path: + ( + "Sources/PNGIntegrationTests/Inputs/\(subdirectory)/\(name).png", + "Sources/PNGIntegrationTests/RGBA/\(name).png.rgba", + "Sources/PNGIntegrationTests/Outputs/\(subdirectory)/\(name).png" + ), + level: level, + premultiplied: subdirectory == "iOS") + } + + private + static func encode(path:(in:String, rgba:String, out:String), + level:Int, + premultiplied:Bool) throws + { + guard + let rectangular:PNG.Image = try .decompress(path: path.in) + else + { + Issue.record("failed to open file '\(path.in)'") + return + } + + try rectangular.compress(path: path.out, level: level) + try Self.decode(path: (in: path.out, rgba: path.rgba), premultiplied: premultiplied) + } +} +extension Roundtripping +{ + private + static let basic:[String] = [ + "PngSuite", + + "basn0g01", + "basn0g02", + "basn0g04", + "basn0g08", + "basn0g16", + "basn2c08", + "basn2c16", + "basn3p01", + "basn3p02", + "basn3p04", + "basn3p08", + "basn4a08", + "basn4a16", + "basn6a08", + "basn6a16" + ] + + private + static let interlaced:[String] = [ + "basi0g01", + "basi0g02", + "basi0g04", + "basi0g08", + "basi0g16", + "basi2c08", + "basi2c16", + "basi3p01", + "basi3p02", + "basi3p04", + "basi3p08", + "basi4a08", + "basi4a16", + "basi6a08", + "basi6a16" + ] + + private + static let oddSizes:[String] = [ + "s01i3p01", + "s01n3p01", + "s02i3p01", + "s02n3p01", + "s03i3p01", + "s03n3p01", + "s04i3p01", + "s04n3p01", + "s05i3p02", + "s05n3p02", + "s06i3p02", + "s06n3p02", + "s07i3p02", + "s07n3p02", + "s08i3p02", + "s08n3p02", + "s09i3p02", + "s09n3p02", + "s32i3p04", + "s32n3p04", + "s33i3p04", + "s33n3p04", + "s34i3p04", + "s34n3p04", + "s35i3p04", + "s35n3p04", + "s36i3p04", + "s36n3p04", + "s37i3p04", + "s37n3p04", + "s38i3p04", + "s38n3p04", + "s39i3p04", + "s39n3p04", + "s40i3p04", + "s40n3p04" + ] + + private + static let backgrounds:[String] = [ + "bgai4a08", + "bgai4a16", + "bgan6a08", + "bgan6a16", + "bgbn4a08", + "bggn4a16", + "bgwn6a08", + "bgyn6a16" + ] + + private + static let transparency:[String] = [ + "tbbn0g04", + "tbbn2c16", + "tbbn3p08", + "tbgn2c16", + "tbgn3p08", + "tbrn2c08", + "tbwn0g16", + "tbwn3p08", + "tbyn3p08", + "tm3n3p02", + "tp0n0g08", + "tp0n2c08", + "tp0n3p08", + "tp1n3p08" + ] + + private + static let gamma:[String] = [ + "g03n0g16", + "g03n2c08", + "g03n3p04", + "g04n0g16", + "g04n2c08", + "g04n3p04", + "g05n0g16", + "g05n2c08", + "g05n3p04", + "g07n0g16", + "g07n2c08", + "g07n3p04", + "g10n0g16", + "g10n2c08", + "g10n3p04", + "g25n0g16", + "g25n2c08", + "g25n3p04" + ] + + private + static let filters:[String] = [ + "f00n0g08", + "f00n2c08", + "f01n0g08", + "f01n2c08", + "f02n0g08", + "f02n2c08", + "f03n0g08", + "f03n2c08", + "f04n0g08", + "f04n2c08", + "f99n0g04" + ] + + private + static let palettes:[String] = [ + "pp0n2c16", + "pp0n6a08", + "ps1n0g08", + "ps1n2c16", + "ps2n0g08", + "ps2n2c16" + ] + + private + static let ancillary:[String] = [ + "ccwn2c08", + "ccwn3p08", + "cdfn2c08", + "cdhn2c08", + "cdsn2c08", + "cdun2c08", + "ch1n3p04", + "ch2n3p08", + "cm0n0g04", + "cm7n0g04", + "cm9n0g04", + "cs3n2c16", + "cs3n3p08", + "cs5n2c08", + "cs5n3p08", + "cs8n2c08", + "cs8n3p08", + "ct0n0g04", + "ct1n0g04", + "cten0g04", + "ctfn0g04", + "ctgn0g04", + "cthn0g04", + "ctjn0g04", + "ctzn0g04" + ] + + private + static let chunkOrdering:[String] = [ + "oi1n0g16", + "oi1n2c16", + "oi2n0g16", + "oi2n2c16", + "oi4n0g16", + "oi4n2c16", + "oi9n0g16", + "oi9n2c16" + ] + + private + static let lz77:[String] = [ + "z00n2c08", + "z03n2c08", + "z06n2c08", + "z09n2c08" + ] + + private + static let iOS:[String] = [ + "PngSuite", + "basi2c08", + "basi6a08", + "basn2c08", + "basn6a08", + "bgan6a08", + "bgwn6a08", + "ccwn2c08", + "cdfn2c08", + "cdhn2c08", + "cdsn2c08", + "cdun2c08", + "cs5n2c08", + "cs8n2c08", + "f00n2c08", + "f01n2c08", + "f02n2c08", + "f03n2c08", + "f04n2c08", + "g03n2c08", + "g04n2c08", + "g05n2c08", + "g07n2c08", + "g10n2c08", + "g25n2c08", + "pp0n6a08", + "tbrn2c08", + "tp0n2c08", + "z00n2c08", + "z03n2c08", + "z06n2c08", + "z09n2c08", + ] +} diff --git a/Sources/PNGIntegrationTests/String.swift b/Sources/PNGIntegrationTests/String.swift deleted file mode 100644 index dd34ee7f..00000000 --- a/Sources/PNGIntegrationTests/String.swift +++ /dev/null @@ -1,46 +0,0 @@ -// import PNG - -// extension String -// { -// // prints an image using terminal colors -// static -// func terminal(image rgb:[PNG.RGBA], size:(x:Int, y:Int)) -> String -// where T:FixedWidthInteger & UnsignedInteger -// { -// let downsample:Int = Swift.min(Swift.max(1, size.x / 16), Swift.max(1, size.y / 16)) -// return stride(from: 0, to: size.y, by: downsample).map -// { -// (i:Int) -> String in -// stride(from: 0, to: size.x, by: downsample).map -// { -// (j:Int) in - -// // downsampling -// var r:Int = 0, -// g:Int = 0, -// b:Int = 0 -// for y:Int in i ..< Swift.min(i + downsample, size.y) -// { -// for x:Int in j ..< Swift.min(j + downsample, size.x) -// { -// let c:PNG.RGBA = rgb[x + y * size.x] -// r += .init(c.r) -// g += .init(c.g) -// b += .init(c.b) -// } -// } - -// let count:Int = -// (Swift.min(i + downsample, size.y) - i) * -// (Swift.min(j + downsample, size.x) - j) -// let color:(r:Double, g:Double, b:Double) = -// ( -// .init(r) / (.init(T.max) * .init(count)), -// .init(g) / (.init(T.max) * .init(count)), -// .init(b) / (.init(T.max) * .init(count)) -// ) -// return .highlight(" ", bg: color) -// }.joined() -// }.joined(separator: "\n") -// } -// } diff --git a/Sources/PNGTests/Filtering.swift b/Sources/PNGTests/Filtering.swift new file mode 100644 index 00000000..d7a53966 --- /dev/null +++ b/Sources/PNGTests/Filtering.swift @@ -0,0 +1,66 @@ +#if DEBUG +@testable +import PNG +import Testing + +@Suite +enum Filtering +{ + @Test(arguments: [1, 2, 3, 4, 5, 6, 7, 8]) + static func Delay(_ delay:Int) + { + let size:(x:Int, y:Int) = (24, 16) + let original:[[UInt8]] = (0 ..< size.y).map + { + _ in [0] + (0 ..< size.x).map{ _ in UInt8.random(in: .min ... .max) } + } + + let filtered:[[UInt8]] = .init(unsafeUninitializedCapacity: size.y) + { + $1 = $0.count + guard + let base:UnsafeMutablePointer<[UInt8]> = $0.baseAddress + else + { + return + } + + var last:[UInt8] = .init(repeating: 0, count: size.x + 1) + for (i, line):(Int, [UInt8]) in zip($0.indices, original) + { + (base + i).initialize(to: PNG.Encoder.filter(line, + last: last, + delay: delay)) + last = line + } + } + + let unfiltered:[[UInt8]] = .init(unsafeUninitializedCapacity: size.y) + { + $1 = $0.count + guard + let base:UnsafeMutablePointer<[UInt8]> = $0.baseAddress + else + { + return + } + + var last:[UInt8] = .init(repeating: 0, count: size.x + 1) + for (i, line):(Int, [UInt8]) in zip($0.indices, filtered) + { + var line:[UInt8] = line + PNG.Decoder.defilter(&line, last: last, delay: delay) + last = line + + line[line.startIndex] = 0 + (base + i).initialize(to: line) + } + } + + for (a, b):([UInt8], [UInt8]) in zip(original, unfiltered) + { + #expect(a == b) + } + } +} +#endif diff --git a/Sources/PNGTests/Main.Filtering.swift b/Sources/PNGTests/Main.Filtering.swift deleted file mode 100644 index a47ae1a8..00000000 --- a/Sources/PNGTests/Main.Filtering.swift +++ /dev/null @@ -1,81 +0,0 @@ -#if DEBUG -@testable -import PNG -import Testing_ - -extension Main -{ - enum Filtering - { - } -} -extension Main.Filtering:TestBattery -{ - static - func run(tests:TestGroup) - { - for delay:Int in [1, 2, 3, 4, 5, 6, 7, 8] - { - guard - let tests:TestGroup = tests / "\(delay)" - else - { - continue - } - - let size:(x:Int, y:Int) = (24, 16) - let original:[[UInt8]] = (0 ..< size.y).map - { - _ in [0] + (0 ..< size.x).map{ _ in UInt8.random(in: .min ... .max) } - } - - let filtered:[[UInt8]] = .init(unsafeUninitializedCapacity: size.y) - { - $1 = $0.count - guard - let base:UnsafeMutablePointer<[UInt8]> = $0.baseAddress - else - { - return - } - - var last:[UInt8] = .init(repeating: 0, count: size.x + 1) - for (i, line):(Int, [UInt8]) in zip($0.indices, original) - { - (base + i).initialize(to: PNG.Encoder.filter(line, - last: last, - delay: delay)) - last = line - } - } - - let unfiltered:[[UInt8]] = .init(unsafeUninitializedCapacity: size.y) - { - $1 = $0.count - guard - let base:UnsafeMutablePointer<[UInt8]> = $0.baseAddress - else - { - return - } - - var last:[UInt8] = .init(repeating: 0, count: size.x + 1) - for (i, line):(Int, [UInt8]) in zip($0.indices, filtered) - { - var line:[UInt8] = line - PNG.Decoder.defilter(&line, last: last, delay: delay) - last = line - - line[line.startIndex] = 0 - (base + i).initialize(to: line) - } - } - - for (a, b):([UInt8], [UInt8]) in zip(original, unfiltered) - { - tests.expect(a ..? b) - } - } - } -} -#endif diff --git a/Sources/PNGTests/Main.Premultiplication.swift b/Sources/PNGTests/Main.Premultiplication.swift deleted file mode 100644 index d8bdf51a..00000000 --- a/Sources/PNGTests/Main.Premultiplication.swift +++ /dev/null @@ -1,55 +0,0 @@ -import PNG -import Testing_ - -extension Main -{ - enum Premultiplication - { - } -} -extension Main.Premultiplication:TestBattery -{ - static - func run(tests:TestGroup) - { - if let tests:TestGroup = tests / "UInt8" - { - Self.run(tests: tests, - color: UInt8.min ... UInt8.max, - alpha: UInt8.min ... UInt8.max) - } - if let tests:TestGroup = tests / "UInt16" - { - // exhaustive 16-bit premultiplication tests take way too long to run, so we - // sample a select subset of the input space - Self.run(tests: tests, - color: (0 ..< 512).map{ _ in UInt16.random(in: .min ... .max) }, - alpha: (0 ..< 512).map{ _ in UInt16.random(in: .min ... .max) }) - } - } - - private static - func run(tests:TestGroup, color:some Sequence, alpha:some Sequence) - where T:FixedWidthInteger, T:UnsignedInteger - { - for color:T in color - { - for alpha:T in alpha - { - let direct:PNG.VA = .init(color, alpha), - premultiplied:PNG.VA = direct.premultiplied - - let unquantized:Double = (.init(alpha) * .init(color) / .init(T.max)), - quantized:T = .init(unquantized.rounded()) - - // the order is important here,, the short circuiting protects us from - // overflow when `quantized` == 255 - tests.expect(premultiplied.v ==? quantized) - - let repremultiplied:PNG.VA = premultiplied.straightened.premultiplied - - tests.expect(premultiplied ==? repremultiplied) - } - } - } -} diff --git a/Sources/PNGTests/Main.swift b/Sources/PNGTests/Main.swift deleted file mode 100644 index 2c42e279..00000000 --- a/Sources/PNGTests/Main.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Testing_ - -@main -enum Main:TestMain -{ - #if DEBUG - static - let all:[any TestBattery.Type] = - [ - Premultiplication.self, - Filtering.self, - ] - #else - static - let all:[any TestBattery.Type] = - [ - Premultiplication.self, - ] - #endif -} diff --git a/Sources/PNGTests/Premultiplication.swift b/Sources/PNGTests/Premultiplication.swift new file mode 100644 index 00000000..6ad9879c --- /dev/null +++ b/Sources/PNGTests/Premultiplication.swift @@ -0,0 +1,48 @@ +import PNG +import Testing + +@Suite +enum Premultiplication +{ + @Test + static func VA8() + { + Self.test( + color: UInt8.min ... UInt8.max, + alpha: UInt8.min ... UInt8.max) + } + @Test + static func VA16() + { + // exhaustive 16-bit premultiplication tests take way too long to run, so we + // sample a select subset of the input space + Self.test( + color: (0 ..< 512).map{ _ in UInt16.random(in: .min ... .max) }, + alpha: (0 ..< 512).map{ _ in UInt16.random(in: .min ... .max) }) + } + + private + static func test(color:some Sequence, alpha:some Sequence) + where T:FixedWidthInteger, T:UnsignedInteger + { + for color:T in color + { + for alpha:T in alpha + { + let direct:PNG.VA = .init(color, alpha), + premultiplied:PNG.VA = direct.premultiplied + + let unquantized:Double = (.init(alpha) * .init(color) / .init(T.max)), + quantized:T = .init(unquantized.rounded()) + + // the order is important here,, the short circuiting protects us from + // overflow when `quantized` == 255 + #expect(premultiplied.v == quantized) + + let repremultiplied:PNG.VA = premultiplied.straightened.premultiplied + + #expect(premultiplied == repremultiplied) + } + } + } +}