damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }