Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions Tests/CoreRasterTests/InflateTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// InflateTests.swift: Inflate against DEFLATE streams derived by hand from
// RFC 1951 (stored, fixed and dynamic Huffman blocks; the bit layouts are in
// the comments) and zlib wrappers from RFC 1950; damaged streams and the
// output limit; Deflate then Inflate on seeded buffers. Disabled until IC-3.
@testable import CoreRaster
import Testing
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

@Suite struct InflateTests {
typealias F = PNGFixtures
/// Fixed block (3.2.6): BFINAL 1, BTYPE 01, literal 'a' (0x91, 8 bits), end of block (7 zero bits).
static let fixedA: [UInt8] = [0x4B, 0x04, 0x00]
/// Fixed block: 'a' 'b' 'c', length 6 (code 260) distance 3 (code 2), 'x', length 9 (code 263) distance 1
/// (code 0, a copy that overlaps its own output), end of block.
static let fixedRepeats: [UInt8] = [0x4B, 0x4C, 0x4A, 0x86, 0xA0, 0x0A, 0x38, 0x00, 0x00]
static let repeatsOut = Array("abcabcabc".utf8) + [UInt8](repeating: 0x78, count: 10)
/// Dynamic block (3.2.7): HLIT 28, HDIST 1, HCLEN 14; code-length code 18 = 0, 1 = 10, 2 = 11; literal/length
/// lengths 'x' 2, end 2, 284 1; two 1-bit distance codes. Data: 'x', 284 + extra 23 (length 250) distance 1,
/// 284 + extra 0 (length 227) distance 1, end: 478 bytes of 'x'.
static let dynamic: [UInt8] = [0xE5, 0xC1, 0x81, 0x00, 0x00, 0x00, 0x00, 0x80, 0x20, 0xED, 0xF1, 0x07, 0xA9, 0x72, 0x01, 0x06]
static let dynamicOut = [UInt8](repeating: 0x78, count: 478)

func raw(_ bytes: [UInt8], limit: Int = 1 << 20) throws -> [UInt8] { [UInt8](try Inflate.raw(Data(bytes), limit: limit)) }
func zlib(_ bytes: [UInt8], limit: Int = 1 << 20) throws -> [UInt8] { [UInt8](try Inflate.zlib(Data(bytes), limit: limit)) }

@Test(.disabled("waits on IC-3")) func storedBlocks() throws {
#expect(try raw([0x00, 0x02, 0x00, 0xFD, 0xFF, 0x61, 0x62, 0x01, 0x01, 0x00, 0xFE, 0xFF, 0x63]) == Array("abc".utf8))
#expect(try zlib([0x78, 0x01, 0x01, 0x03, 0x00, 0xFC, 0xFF, 0x61, 0x62, 0x63, 0x02, 0x4D, 0x01, 0x27]) == Array("abc".utf8))
#expect(try zlib(F.stored([])).isEmpty)
let long = (0..<70_000).map { UInt8(truncatingIfNeeded: $0 * 7) }
#expect(try zlib(F.stored(long)) == long, "two stored blocks, the first 65,535 bytes")
}

@Test(.disabled("waits on IC-3")) func fixedBlocks() throws {
#expect(try raw(Self.fixedA) == [0x61])
#expect(try raw(Self.fixedRepeats) == Self.repeatsOut)
#expect(try zlib(F.zlib(Self.fixedRepeats, of: Self.repeatsOut)) == Self.repeatsOut)
}

@Test(.disabled("waits on IC-3")) func dynamicBlocks() throws {
#expect(try raw(Self.dynamic) == Self.dynamicOut)
#expect(try zlib(F.zlib(Self.dynamic, of: Self.dynamicOut)) == Self.dynamicOut)
}

@Test(.disabled("waits on IC-3")) func adlerChecked() throws {
var bytes = F.zlib(Self.dynamic, of: Self.dynamicOut)
bytes[bytes.count - 1] ^= 0x01
#expect(F.thrown { try zlib(bytes) } == "damaged")
#expect(F.thrown { try zlib(Array(bytes.dropLast(4))) } == "damaged", "the checksum missing")
}

@Test(.disabled("waits on IC-3")) func damagedStreams() throws {
let a = F.zlib(Self.fixedA, of: [0x61])
let zlibCases: [(String, [UInt8])] = [
("method 9", [0x79, 0x18] + a.dropFirst(2)), ("window over 32 KiB", [0x88, 0x1C] + a.dropFirst(2)),
("check bits", [0x78, 0x02] + a.dropFirst(2)), ("preset dictionary", [0x78, 0x20] + a.dropFirst(2)),
("empty", []),
]
for (name, bytes) in zlibCases { #expect(F.thrown { try zlib(bytes) } == "damaged", "\(name)") }
let rawCases: [(String, [UInt8])] = [
("distance 2 after one byte", [0x4B, 0x04, 0x42, 0x00]),
("over-subscribed code-length code", Array(Self.dynamic.prefix(8)) + [0x10] + Self.dynamic.dropFirst(9)),
("reserved block type", [0x07]), ("LEN and NLEN disagree", [0x01, 0x03, 0x00, 0xFC, 0xFE, 0x61, 0x62, 0x63]),
("no final block", [0x00, 0x03, 0x00, 0xFC, 0xFF, 0x61, 0x62, 0x63]), ("cut mid-block", Array(Self.dynamic.prefix(12))),
]
for (name, bytes) in rawCases { #expect(F.thrown { try raw(bytes) } == "damaged", "\(name)") }
}

/// The limit is checked before the output grows: a 6-byte stored block then a reserved block type
/// throws `tooLarge` at limit 5 (not `damaged` after the bytes were kept), `damaged` at limit 6.
@Test(.disabled("waits on IC-3")) func limitBeforeGrowth() throws {
let six: [UInt8] = [0x00, 0x06, 0x00, 0xF9, 0xFF] + Array("abcdef".utf8) + [0x07]
#expect(F.thrown { try raw(six, limit: 5) } == "tooLarge")
#expect(F.thrown { try raw(six, limit: 6) } == "damaged")
#expect(F.thrown { try raw(Self.dynamic, limit: 477) } == "tooLarge")
#expect(F.thrown { try raw(Self.dynamic, limit: 0) } == "tooLarge")
#expect(try raw(Self.dynamic, limit: 478) == Self.dynamicOut)
#expect(F.thrown { try zlib(F.stored([UInt8](repeating: 1, count: 1_000)), limit: 999) } == "tooLarge")
}

@Test(.disabled("waits on IC-3")) func deflateRoundTrip() throws {
var rng = SplitMix64(seed: 0x1C3)
for i in 0..<1_000 {
var bytes: [UInt8] = []
for _ in 0..<Int.random(in: 0...6, using: &rng) {
let n = Int.random(in: 0...400, using: &rng), run = UInt8.random(in: 0...255, using: &rng)
bytes += Bool.random(using: &rng) ? (0..<n).map { _ in UInt8.random(in: 0...255, using: &rng) } : [UInt8](repeating: run, count: n)
}
let level = i % 10, data = Data(bytes)
#expect(try Inflate.zlib(Deflate.zlib(data, level: level), limit: bytes.count) == data, "case \(i) level \(level)")
}
let big = Data((0..<200_000).map { UInt8(truncatingIfNeeded: $0 / 3) })
#expect(try Inflate.zlib(Deflate.zlib(big, level: 6), limit: big.count) == big)
}

@Test(.disabled("waits on IC-3")) func deflateDeterministic() {
let text = Data((0..<5_000).map { UInt8(truncatingIfNeeded: ($0 % 97) * 3) })
for level in 0...9 { #expect(Deflate.zlib(text, level: level) == Deflate.zlib(text, level: level)) }
let fixed = [UInt8](Deflate.zlib(text, level: 6))
#expect(fixed.count > 2 && fixed[2] & 0x06 == 0x02, "levels 1...9 write fixed-Huffman blocks (BTYPE 01)")
#expect(fixed.count < Deflate.zlib(text, level: 0).count)
}

@Test(.disabled("waits on IC-3")) func fuzzed() {
let seeds = [F.zlib(Self.dynamic, of: Self.dynamicOut), F.zlib(Self.fixedRepeats, of: Self.repeatsOut), F.stored(Array("abcdef".utf8), block: 2)]
let cases = mutations(of: seeds.map { Data($0) }, count: 1_000, seed: 0x1C4)
fuzz(cases) { _ = try Inflate.zlib($0, limit: 1 << 16) }
fuzz(cases.map { $0.dropFirst(2) }) { _ = try Inflate.raw($0, limit: 1 << 16) }
}
}
114 changes: 114 additions & 0 deletions Tests/CoreRasterTests/PNGWriteTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// PNGWriteTests.swift: PNG.write and PNGCodec.encodePNG checked against the
// W3C PNG specification: rereads equal at 8 bits and within one step at 16,
// IHDR depth, big-endian samples, chunk order and CRCs, IDATs of at most
// 64 KiB, the least-sum filter choice (12.8), and an ORA round trip through
// PNGCodec. Written files are parsed with PNGFixtures. Disabled until IC-5.
@testable import CoreRaster
import CoreColor
import Testing
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

@Suite struct PNGWriteTests {
typealias F = PNGFixtures

/// Seeded pixels an 8-bit file holds exactly: sRGB bytes with alpha 0 (and colour 0), 128 or 255.
static func bytePixels(_ seed: UInt64, _ width: Int, _ height: Int) throws -> PixelBuffer {
var rng = SplitMix64(seed: seed)
let bytes = (0..<(width * height)).flatMap { _ -> [UInt8] in
let alpha = [UInt8(0), 128, 255][Int.random(in: 0...2, using: &rng)]
return alpha == 0 ? [0, 0, 0, 0] : (0..<3).map { _ in UInt8.random(in: 0...255, using: &rng) } + [alpha]
}
return try PixelBuffer(rgba8: bytes, width: width, height: height)
}

/// The inflated IDAT stream of a written PNG.
static func scanlines(_ data: Data) throws -> [UInt8] {
let idat = F.chunks(data).filter { $0.type == "IDAT" }.flatMap(\.body)
return [UInt8](try Inflate.zlib(Data(idat), limit: 1 << 24))
}

@Test(.disabled("waits on IC-5")) func eightBitRoundTrip() throws {
let source = try Self.bytePixels(0x5E1, 13, 7)
for level in [0, 6, 9] { #expect(try PNG.read(PNG.write(source, level: level)) == source, "level \(level)") }
#expect(try PNGCodec().decodePNG(PNGCodec().encodePNG(source, bitsPerChannel: 8)) == source)
}

@Test(.disabled("waits on IC-5")) func sixteenBitWithinOneStep() throws {
var rng = SplitMix64(seed: 0x5E2), source = try PixelBuffer(width: 11, height: 6)
for y in 0..<6 {
for x in 0..<11 {
let c = (0..<3).map { _ in Float.random(in: 0...1, using: &rng) }
try source.setPixel(x: x, y: y, RGBA(r: c[0], g: c[1], b: c[2], a: .random(in: 0.25...1, using: &rng)).premultiplied())
}
}
let back = try PNG.read(PNG.write(source, bitsPerChannel: 16))
for y in 0..<6 {
for x in 0..<11 {
let p = try source.pixel(x: x, y: y).unpremultiplied(), q = try back.pixel(x: x, y: y).unpremultiplied()
let encoded = { (v: Float) in TransferCurve.encode(Double(v)) }
for (u, v) in [(p.r, q.r), (p.g, q.g), (p.b, q.b)] { #expect(abs(encoded(u) - encoded(v)) <= 1.01 / 65_535) }
#expect(abs(p.a - q.a) <= 0.51 / 65_535)
}
}
}

/// 7.1: 16-bit samples are written high byte first; encoded 0x1234 is the bytes 0x12 0x34.
@Test(.disabled("waits on IC-5")) func bigEndian16() throws {
let v = Float(TransferCurve.decode(Double(0x1234) / 65_535))
let lines = try Self.scanlines(PNG.write(PixelBuffer(width: 1, height: 1, fill: RGBA(r: v, g: v, b: v, a: 1)), bitsPerChannel: 16))
#expect(lines.count == 9 && lines[0] <= 4)
#expect(Array(lines.dropFirst()) == [0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0xFF, 0xFF])
}

@Test(.disabled("waits on IC-5")) func ihdrDepthRecorded() throws {
let source = try Self.bytePixels(0x5E3, 3, 2)
for depth in [8, 16] {
let data = try PNG.write(source, bitsPerChannel: depth), bytes = [UInt8](data)
#expect(bytes.count > 33 && bytes[24] == depth && bytes[25] == 6 && bytes[28] == 0, "IHDR depth, RGBA, not interlaced")
#expect(try PNG.info(data) == PNGInfo(width: 3, height: 2, bitDepth: depth, colorType: 6, interlaced: false))
}
}

@Test(.disabled("waits on IC-5")) func chunkCRCsValid() throws {
let data = try PNG.write(Self.bytePixels(0x5E4, 200, 200), level: 0)
let chunks = F.chunks(data)
#expect(Array(data.prefix(8)) == F.signature && chunks.first?.type == "IHDR")
#expect(chunks.last.map { $0.type == "IEND" && $0.body.isEmpty } == true)
#expect(chunks.allSatisfy(\.crcOK) && chunks.map { 12 + $0.body.count }.reduce(8, +) == data.count)
let idats = chunks.filter { $0.type == "IDAT" }
#expect(idats.count >= 3 && idats.allSatisfy { $0.body.count <= 65_536 }, "160,200 stored bytes split by 64 KiB")
}

/// 12.8: each row takes the filter with the least sum of its bytes read as signed. A 16-pixel horizontal
/// ramp (sRGB 4x + 4, opaque) with equal rows makes None the worst choice on every row.
@Test(.disabled("waits on IC-5")) func adaptiveFilterSmaller() throws {
let row: [UInt8] = (0..<16).flatMap { (x: Int) -> [UInt8] in let v = UInt8(4 * x + 4); return [v, v, v, 255] }
let image = try PixelBuffer(rgba8: row + row + row + row, width: 16, height: 4)
let lines = try Self.scanlines(PNG.write(image))
#expect(lines.count == 4 * 65)
func cost(_ filtered: [UInt8]) -> Int { filtered.dropFirst().reduce(0) { $0 + abs(Int(Int8(bitPattern: $1))) } }
for y in 0..<4 {
let written = Array(lines[(y * 65)..<(y * 65 + 65)]), prior = y == 0 ? nil : row
let costs = (0...4).map { cost(F.filtered(row, prior: prior, filter: $0, bpp: 4)) }
#expect(written[0] != 0 && written[0] <= 4, "row \(y)")
#expect(written == F.filtered(row, prior: prior, filter: Int(min(written[0], 4)), bpp: 4) && cost(written) == costs.min())
}
}

@Test(.disabled("waits on IC-5")) func oraRoundTripThroughPNGCodec() throws {
let layers = try (0..<3).map { PixelLayer(name: "layer \($0)", pixels: try Self.bytePixels(0x5E5 + UInt64($0), 5 + $0, 4), x: $0, y: -$0) }
let image = LayeredImage(width: 9, height: 6, nodes: layers.map { .pixels($0) }, composite: try Self.bytePixels(0x5E9, 9, 6))
#expect(try OpenRaster.read(OpenRaster.write(image, pixels: PNGCodec()), pixels: PNGCodec()) == image)
}

@Test(.disabled("waits on IC-5")) func otherDepthsRefused() throws {
let source = try Self.bytePixels(0x5EA, 2, 2)
#expect(F.thrown { try PNG.write(source, bitsPerChannel: 12) } == "unsupported")
#expect(F.thrown { try PNGCodec().encodePNG(source, bitsPerChannel: 12) } == "pixelCodec")
#expect(F.thrown { try PNG.write(PixelBuffer(width: PNG.maxSide + 1, height: 1)) } == "tooLarge")
}
}
Loading