eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2import CoreGraphics
3@testable import LChatCore
4
5// 🤖 Pins the CGImage adaptation of the reference BlurHash (woltapp/blurhash's
6// Swift files are UIImage-only and copy-in by design — no package): structure,
7// round-trip colour fidelity, and malformed-hash rejection. A DCT port's bugs
8// are subtle; these tests are the confidence the adaptation didn't drift.
9final class BlurhashTests: XCTestCase {
10 private func solid(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat,
11 w: Int = 32, h: Int = 24) -> CGImage {
12 let cs = CGColorSpaceCreateDeviceRGB()
13 let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8,
14 bytesPerRow: w * 4, space: cs,
15 bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)!
16 // Components in the context's OWN space — the CGColor(red:green:blue:)
17 // convenience creates a generic-space colour that CG converts on draw
18 // (0.3 arrived as 0.405), which read as a codec bug.
19 ctx.setFillColor(CGColor(colorSpace: cs, components: [r, g, b, 1])!)
20 ctx.fill(CGRect(x: 0, y: 0, width: w, height: h))
21 return ctx.makeImage()!
22 }
23
24 private func averageRGB(_ image: CGImage) -> (Double, Double, Double) {
25 let w = image.width, h = image.height
26 var data = [UInt8](repeating: 0, count: w * h * 4)
27 let cs = CGColorSpaceCreateDeviceRGB()
28 let ctx = CGContext(data: &data, width: w, height: h, bitsPerComponent: 8,
29 bytesPerRow: w * 4, space: cs,
30 bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)!
31 ctx.draw(image, in: CGRect(x: 0, y: 0, width: w, height: h))
32 var r = 0.0, g = 0.0, b = 0.0
33 for i in stride(from: 0, to: data.count, by: 4) {
34 r += Double(data[i]); g += Double(data[i + 1]); b += Double(data[i + 2])
35 }
36 let n = Double(w * h)
37 return (r / n, g / n, b / n)
38 }
39
40 func testHashShape() throws {
41 let hash = try XCTUnwrap(Blurhash.encode(solid(0.8, 0.3, 0.2)))
42 // 4Ă—3 components: 1 (size) + 1 (max AC) + 4 (DC) + 2Ă—11 (AC) = 28 chars.
43 XCTAssertEqual(hash.count, 28)
44 }
45
46 // The shared vector grid: 8×6 RGBA, red ramps left→right, green top→bottom,
47 // blue fixed at 64 — keep IDENTICAL to vectorGrid() in test/blurhash.test.mjs.
48 private func vectorImage() -> CGImage {
49 let w = 8, h = 6
50 var px = [UInt8](repeating: 0, count: w * h * 4)
51 for y in 0..<h {
52 for x in 0..<w {
53 let o = (y * w + x) * 4
54 px[o] = UInt8((x * 255 + (w - 1) / 2) / (w - 1))
55 px[o + 1] = UInt8((y * 255 + (h - 1) / 2) / (h - 1))
56 px[o + 2] = 64
57 px[o + 3] = 255
58 }
59 }
60 let cs = CGColorSpaceCreateDeviceRGB()
61 let provider = CGDataProvider(data: Data(px) as CFData)!
62 return CGImage(width: w, height: h, bitsPerComponent: 8, bitsPerPixel: 32,
63 bytesPerRow: w * 4, space: cs,
64 bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue),
65 provider: provider, decode: nil, shouldInterpolate: false,
66 intent: .defaultIntent)!
67 }
68
69 func testCrossImplementationVector() throws {
70 // The one true hash for the vector grid — test/blurhash.test.mjs pins
71 // the same string. A change here without the twin is a platform drift.
72 XCTAssertEqual(try XCTUnwrap(Blurhash.encode(vectorImage())),
73 "LyI5d=39a|%0z2NKfQnSeXf7fQf7")
74 }
75
76 func testRoundTripPreservesColour() throws {
77 for (r, g, b) in [(1.0, 0.2, 0.2), (0.1, 0.7, 0.3), (0.2, 0.3, 0.9)] {
78 let hash = try XCTUnwrap(Blurhash.encode(solid(r, g, b)))
79 let decoded = try XCTUnwrap(Blurhash.decode(hash))
80 let avg = averageRGB(decoded)
81 XCTAssertEqual(avg.0 / 255, r, accuracy: 0.06, "red channel for \(hash)")
82 XCTAssertEqual(avg.1 / 255, g, accuracy: 0.06, "green channel for \(hash)")
83 XCTAssertEqual(avg.2 / 255, b, accuracy: 0.06, "blue channel for \(hash)")
84 }
85 }
86
87 func testGradientRoundTripKeepsOrientation() throws {
88 // Left-red, right-blue: the decode must keep the gradient's direction.
89 let w = 32, h = 24
90 let cs = CGColorSpaceCreateDeviceRGB()
91 let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8,
92 bytesPerRow: w * 4, space: cs,
93 bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)!
94 ctx.setFillColor(CGColor(colorSpace: cs, components: [1, 0, 0, 1])!)
95 ctx.fill(CGRect(x: 0, y: 0, width: w / 2, height: h))
96 ctx.setFillColor(CGColor(colorSpace: cs, components: [0, 0, 1, 1])!)
97 ctx.fill(CGRect(x: w / 2, y: 0, width: w / 2, height: h))
98 let hash = try XCTUnwrap(Blurhash.encode(ctx.makeImage()!))
99 let decoded = try XCTUnwrap(Blurhash.decode(hash, width: 32, height: 24))
100 var left = [UInt8](repeating: 0, count: 4), right = left
101 let dctx = { (px: Int, out: inout [UInt8]) in
102 let c = CGContext(data: &out, width: 1, height: 1, bitsPerComponent: 8,
103 bytesPerRow: 4, space: cs,
104 bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)!
105 c.draw(decoded, in: CGRect(x: -px, y: 0, width: 32, height: 24))
106 }
107 dctx(2, &left)
108 dctx(29, &right)
109 XCTAssertGreaterThan(left[0], left[2], "left edge reads red")
110 XCTAssertGreaterThan(right[2], right[0], "right edge reads blue")
111 }
112
113 func testMalformedHashesRejected() {
114 XCTAssertNil(Blurhash.decode(""))
115 XCTAssertNil(Blurhash.decode("LEHV6")) // truncated
116 XCTAssertNil(Blurhash.decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj!!")) // wrong length
117 XCTAssertNil(Blurhash.decode(String(repeating: "\u{1F600}", count: 28))) // non-base83
118 }
119}
120