ImageMetadata.swift (5528B)
1 // 2 // ImageMetadata.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-04-25. 6 // 7 8 import Foundation 9 import UIKit 10 import Kingfisher 11 12 struct ImageMetaDim: Equatable, StringCodable { 13 init(width: Int, height: Int) { 14 self.width = width 15 self.height = height 16 } 17 18 init?(from string: String) { 19 guard let dim = parse_image_meta_dim(string) else { 20 return nil 21 } 22 self = dim 23 } 24 25 func to_string() -> String { 26 "\(width)x\(height)" 27 } 28 29 var size: CGSize { 30 return CGSize(width: CGFloat(self.width), height: CGFloat(self.height)) 31 } 32 33 let width: Int 34 let height: Int 35 } 36 37 struct ImageMetadata: Equatable { 38 let url: URL 39 let blurhash: String? 40 let dim: ImageMetaDim? 41 42 init(url: URL, blurhash: String? = nil, dim: ImageMetaDim? = nil) { 43 self.url = url 44 self.blurhash = blurhash 45 self.dim = dim 46 } 47 48 init?(tag: [String]) { 49 guard let meta = decode_image_metadata(tag) else { 50 return nil 51 } 52 53 self = meta 54 } 55 56 func to_tag() -> [String] { 57 return image_metadata_to_tag(self) 58 } 59 } 60 61 func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? { 62 let res = Task.detached(priority: .low) { 63 let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0)) 64 guard let img = UIImage.init(blurHash: blurhash, size: size) else { 65 let noimg: UIImage? = nil 66 return noimg 67 } 68 return img 69 } 70 71 return await res.value 72 } 73 74 func image_metadata_to_tag(_ meta: ImageMetadata) -> [String] { 75 var tags = ["imeta", "url \(meta.url.absoluteString)"] 76 if let blurhash = meta.blurhash { 77 tags.append("blurhash \(blurhash)") 78 } 79 if let dim = meta.dim { 80 tags.append("dim \(dim.to_string())") 81 } 82 return tags 83 } 84 85 func decode_image_metadata(_ parts: [String]) -> ImageMetadata? { 86 var url: URL? = nil 87 var blurhash: String? = nil 88 var dim: ImageMetaDim? = nil 89 90 for part in parts { 91 if part == "imeta" { 92 continue 93 } 94 95 let ps = part.split(separator: " ") 96 97 guard ps.count == 2 else { 98 return nil 99 } 100 let pname = ps[0] 101 let pval = ps[1] 102 103 if pname == "blurhash" { 104 blurhash = String(pval) 105 } else if pname == "dim" { 106 dim = parse_image_meta_dim(String(pval)) 107 } else if pname == "url" { 108 url = URL(string: String(pval)) 109 } 110 } 111 112 guard let url else { 113 return nil 114 } 115 116 return ImageMetadata(url: url, blurhash: blurhash, dim: dim) 117 } 118 119 func parse_image_meta_dim(_ pval: String) -> ImageMetaDim? { 120 let parts = pval.split(separator: "x") 121 guard parts.count == 2, 122 let width = Int(parts[0]), 123 let height = Int(parts[1]) else { 124 return nil 125 } 126 127 return ImageMetaDim(width: width, height: height) 128 } 129 130 extension UIImage { 131 func resized(to size: CGSize) -> UIImage { 132 return UIGraphicsImageRenderer(size: size).image { _ in 133 draw(in: CGRect(origin: .zero, size: size)) 134 } 135 } 136 } 137 138 func get_blurhash_size(img_size: CGSize) -> CGSize { 139 return CGSize(width: 100.0, height: (100.0/img_size.width) * img_size.height) 140 } 141 142 func calculate_blurhash(img: UIImage) async -> String? { 143 guard img.size.height > 0 else { 144 return nil 145 } 146 147 let res = Task.detached(priority: .low) { 148 let bhs = get_blurhash_size(img_size: img.size) 149 let smaller = img.resized(to: bhs) 150 151 guard let blurhash = smaller.blurHash(numberOfComponents: (5,5)) else { 152 let meta: String? = nil 153 return meta 154 } 155 156 return blurhash 157 } 158 159 return await res.value 160 } 161 162 func calculate_image_metadata(url: URL, img: UIImage, blurhash: String) -> ImageMetadata { 163 let width = Int(img.size.width) 164 let height = Int(img.size.height) 165 let dim = ImageMetaDim(width: width, height: height) 166 167 return ImageMetadata(url: url, blurhash: blurhash, dim: dim) 168 } 169 170 171 func event_image_metadata(ev: NostrEvent) -> [ImageMetadata] { 172 return ev.tags.reduce(into: [ImageMetadata]()) { meta, tag in 173 guard tag.count >= 2, tag[0].matches_str("imeta"), 174 let data = ImageMetadata(tag: tag.strings()) else { 175 return 176 } 177 178 meta.append(data) 179 } 180 } 181 182 func process_image_metadatas(cache: EventCache, ev: NostrEvent) { 183 for meta in event_image_metadata(ev: ev) { 184 guard cache.lookup_img_metadata(url: meta.url) == nil else { 185 continue 186 } 187 188 // We don't need blurhash if we already have the source image cached 189 if ImageCache.default.isCached(forKey: meta.url.absoluteString) { 190 continue 191 } 192 193 let state = ImageMetadataState(state: meta.blurhash == nil ? .not_needed : .processing, meta: meta) 194 cache.store_img_metadata(url: meta.url, meta: state) 195 196 guard let blurhash = state.meta.blurhash else { 197 return 198 } 199 200 Task { 201 let img = await process_blurhash(blurhash: blurhash, size: state.meta.dim?.size) 202 203 Task { @MainActor in 204 if let img { 205 state.state = .processed(img) 206 } else { 207 state.state = .failed 208 } 209 } 210 } 211 } 212 }