eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import CoreGraphics
3
4// 🤖 BlurHash (blurha.sh) — the compact DCT thumbnail that stands in for a
5// photo while its blob downloads + decrypts. The hash is DERIVED FROM IMAGE
6// CONTENT, so it is user content: it rides inside the encrypted payload as
7// `attachments[].bh` (beside w/h/name), never as server metadata. ~30 chars
8// of base83 for a 4×3 component hash.
9//
10// Ported from the reference implementation (github.com/woltapp/blurhash, MIT);
11// trimmed to what the app uses: encode from a CGImage, decode to a CGImage.
12public enum Blurhash {
13 private static let alphabet = Array(
14 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~")
15 private static let charValue: [Character: Int] = {
16 var m: [Character: Int] = [:]
17 for (i, c) in alphabet.enumerated() { m[c] = i }
18 return m
19 }()
20
21 // ---- encode --------------------------------------------------------------
22
23 /// Encode a (small!) CGImage — callers downsample first; DCT cost is
24 /// O(pixels × components). 4×3 components suit chat thumbnails.
25 public static func encode(_ image: CGImage, componentsX: Int = 4, componentsY: Int = 3) -> String? {
26 guard (1...9).contains(componentsX), (1...9).contains(componentsY) else { return nil }
27 let w = image.width, h = image.height
28 guard w > 0, h > 0 else { return nil }
29 guard let data = rgbaPixels(image) else { return nil }
30
31 var factors: [(Double, Double, Double)] = []
32 for y in 0..<componentsY {
33 for x in 0..<componentsX {
34 let normalisation = (x == 0 && y == 0) ? 1.0 : 2.0
35 var r = 0.0, g = 0.0, b = 0.0
36 for py in 0..<h {
37 for px in 0..<w {
38 let basis = normalisation
39 * cos(Double.pi * Double(x) * Double(px) / Double(w))
40 * cos(Double.pi * Double(y) * Double(py) / Double(h))
41 let i = (py * w + px) * 4
42 r += basis * sRGBToLinear(Int(data[i]))
43 g += basis * sRGBToLinear(Int(data[i + 1]))
44 b += basis * sRGBToLinear(Int(data[i + 2]))
45 }
46 }
47 let scale = 1.0 / Double(w * h)
48 factors.append((r * scale, g * scale, b * scale))
49 }
50 }
51
52 let dc = factors[0]
53 let ac = factors.dropFirst()
54 var hash = ""
55 hash += base83(componentsX - 1 + (componentsY - 1) * 9, length: 1)
56 let maxAC = ac.reduce(0.0) { m, f in max(m, abs(f.0), abs(f.1), abs(f.2)) }
57 let quantMax = ac.isEmpty ? 0 : max(0, min(82, Int(floor(maxAC * 166 - 0.5))))
58 let acNorm = Double(quantMax + 1) / 166
59 hash += base83(quantMax, length: 1)
60 hash += base83(encodeDC(dc), length: 4)
61 for f in ac { hash += base83(encodeAC(f, maximum: acNorm), length: 2) }
62 return hash
63 }
64
65 // ---- decode --------------------------------------------------------------
66
67 /// Decode to a tiny CGImage (the renderer scales it up; blurriness IS the
68 /// look, so 32px on the long edge is plenty). Nil on a malformed hash.
69 public static func decode(_ hash: String, width: Int = 32, height: Int = 24,
70 punch: Double = 1) -> CGImage? {
71 let chars = Array(hash)
72 guard chars.count >= 6, let sizeFlag = value(chars[0]) else { return nil }
73 let cy = (sizeFlag / 9) + 1
74 let cx = (sizeFlag % 9) + 1
75 guard chars.count == 4 + 2 * cx * cy, let quantMax = value(chars[1]) else { return nil }
76 let maxAC = Double(quantMax + 1) / 166
77
78 var colours: [(Double, Double, Double)] = []
79 guard let dcValue = value(String(chars[2..<6])) else { return nil }
80 colours.append(decodeDC(dcValue))
81 for i in 1..<(cx * cy) {
82 guard let acValue = value(String(chars[(4 + i * 2)..<(6 + i * 2)])) else { return nil }
83 colours.append(decodeAC(acValue, maximum: maxAC * punch))
84 }
85
86 var pixels = [UInt8](repeating: 255, count: width * height * 4)
87 for py in 0..<height {
88 for px in 0..<width {
89 var r = 0.0, g = 0.0, b = 0.0
90 for j in 0..<cy {
91 for i in 0..<cx {
92 let basis = cos(Double.pi * Double(px) * Double(i) / Double(width))
93 * cos(Double.pi * Double(py) * Double(j) / Double(height))
94 let c = colours[i + j * cx]
95 r += c.0 * basis
96 g += c.1 * basis
97 b += c.2 * basis
98 }
99 }
100 let o = (py * width + px) * 4
101 pixels[o] = UInt8(linearToSRGB(r))
102 pixels[o + 1] = UInt8(linearToSRGB(g))
103 pixels[o + 2] = UInt8(linearToSRGB(b))
104 }
105 }
106 let cs = CGColorSpaceCreateDeviceRGB()
107 guard let provider = CGDataProvider(data: Data(pixels) as CFData) else { return nil }
108 return CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 32,
109 bytesPerRow: width * 4, space: cs,
110 bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue),
111 provider: provider, decode: nil, shouldInterpolate: true,
112 intent: .defaultIntent)
113 }
114
115 // ---- pieces ---------------------------------------------------------------
116
117 private static func rgbaPixels(_ image: CGImage) -> [UInt8]? {
118 let w = image.width, h = image.height
119 var data = [UInt8](repeating: 0, count: w * h * 4)
120 let cs = CGColorSpaceCreateDeviceRGB()
121 guard let ctx = CGContext(data: &data, width: w, height: h, bitsPerComponent: 8,
122 bytesPerRow: w * 4, space: cs,
123 bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
124 else { return nil }
125 ctx.draw(image, in: CGRect(x: 0, y: 0, width: w, height: h))
126 return data
127 }
128
129 private static func encodeDC(_ c: (Double, Double, Double)) -> Int {
130 (linearToSRGB(c.0) << 16) + (linearToSRGB(c.1) << 8) + linearToSRGB(c.2)
131 }
132
133 private static func decodeDC(_ value: Int) -> (Double, Double, Double) {
134 (sRGBToLinear((value >> 16) & 255), sRGBToLinear((value >> 8) & 255),
135 sRGBToLinear(value & 255))
136 }
137
138 private static func encodeAC(_ c: (Double, Double, Double), maximum: Double) -> Int {
139 func q(_ v: Double) -> Int {
140 max(0, min(18, Int(floor(signPow(v / maximum, 0.5) * 9 + 9.5))))
141 }
142 return q(c.0) * 19 * 19 + q(c.1) * 19 + q(c.2)
143 }
144
145 private static func decodeAC(_ value: Int, maximum: Double) -> (Double, Double, Double) {
146 func d(_ v: Int) -> Double { signPow((Double(v) - 9) / 9, 2) * maximum }
147 return (d(value / (19 * 19)), d((value / 19) % 19), d(value % 19))
148 }
149
150 private static func signPow(_ v: Double, _ exp: Double) -> Double {
151 (v < 0 ? -1 : 1) * pow(abs(v), exp)
152 }
153
154 private static func sRGBToLinear(_ v: Int) -> Double {
155 let f = Double(v) / 255
156 return f <= 0.04045 ? f / 12.92 : pow((f + 0.055) / 1.055, 2.4)
157 }
158
159 private static func linearToSRGB(_ v: Double) -> Int {
160 let f = max(0, min(1, v))
161 let s = f <= 0.0031308 ? f * 12.92 : 1.055 * pow(f, 1 / 2.4) - 0.055
162 return Int(s * 255 + 0.5)
163 }
164
165 private static func base83(_ value: Int, length: Int) -> String {
166 var out = ""
167 for i in 1...length {
168 let digit = (value / Int(pow(83.0, Double(length - i)))) % 83
169 out.append(alphabet[digit])
170 }
171 return out
172 }
173
174 private static func value(_ c: Character) -> Int? { charValue[c] }
175
176 private static func value(_ s: any StringProtocol) -> Int? {
177 var out = 0
178 for c in s {
179 guard let v = charValue[c] else { return nil }
180 out = out * 83 + v
181 }
182 return out
183 }
184}
185