mirror of https://github.com/lukechilds/damus.git
Browse Source
Changelog-Changed: Use an optimized library for image loading Signed-off-by: William Casarin <jb55@jb55.com>profile-edit
William Casarin
2 years ago
17 changed files with 37 additions and 263 deletions
@ -1,193 +0,0 @@ |
|||||
// |
|
||||
// ImageCache.swift |
|
||||
// damus |
|
||||
// |
|
||||
// Created by William Casarin on 2022-05-04. |
|
||||
// |
|
||||
|
|
||||
import Foundation |
|
||||
import SwiftUI |
|
||||
import UIKit |
|
||||
|
|
||||
enum ImageProcessingStatus { |
|
||||
case processing |
|
||||
case done |
|
||||
} |
|
||||
|
|
||||
class ImageCache { |
|
||||
private let lock = NSLock() |
|
||||
private var state: [String: ImageProcessingStatus] = [:] |
|
||||
|
|
||||
private func get_state(_ key: String) -> ImageProcessingStatus? { |
|
||||
lock.lock(); defer { lock.unlock() } |
|
||||
|
|
||||
return state[key] |
|
||||
} |
|
||||
|
|
||||
private func set_state(_ key: String, new_state: ImageProcessingStatus) { |
|
||||
lock.lock(); defer { lock.unlock() } |
|
||||
|
|
||||
state[key] = new_state |
|
||||
} |
|
||||
|
|
||||
lazy var cache: NSCache<NSString, UIImage> = { |
|
||||
let cache = NSCache<NSString, UIImage>() |
|
||||
cache.totalCostLimit = 1024 * 1024 * 100 // 100MB |
|
||||
return cache |
|
||||
}() |
|
||||
|
|
||||
// simple polling until I can figure out a better way to do this |
|
||||
func wait_for_image(_ key: String) async { |
|
||||
while true { |
|
||||
let why_would_this_happen: ()? = try? await Task.sleep(nanoseconds: 100_000_000) // 100ms |
|
||||
if why_would_this_happen == nil { |
|
||||
return |
|
||||
} |
|
||||
if get_state(key) == .done { |
|
||||
return |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func lookup_sync(key: String) -> UIImage? { |
|
||||
let status = get_state(key) |
|
||||
|
|
||||
switch status { |
|
||||
case .done: |
|
||||
break |
|
||||
case .processing: |
|
||||
return nil |
|
||||
case .none: |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
if let decoded = cache.object(forKey: NSString(string: key)) { |
|
||||
return decoded |
|
||||
} |
|
||||
|
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
func lookup_or_load_image(key: String, url: URL?) async -> UIImage? { |
|
||||
if let img = await lookup(key: key) { |
|
||||
return img |
|
||||
} |
|
||||
|
|
||||
guard let url = url else { |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
return await load_image(cache: self, from: url, key: key) |
|
||||
} |
|
||||
|
|
||||
func get_cache_url(key: String, suffix: String, ext: String = "png") -> URL? { |
|
||||
let urls = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) |
|
||||
|
|
||||
guard let root = urls.first else { |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
return root.appendingPathComponent("\(key)\(suffix).\(ext)") |
|
||||
} |
|
||||
|
|
||||
private func lookup_file_cache(key: String, suffix: String = "_pfp") -> UIImage? { |
|
||||
guard let img_file = get_cache_url(key: key, suffix: suffix) else { |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
guard let img = UIImage(contentsOfFile: img_file.path) else { |
|
||||
//print("failed to load \(key)\(suffix).png from file cache") |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
save_to_memory_cache(key: key, img: img) |
|
||||
|
|
||||
return img |
|
||||
} |
|
||||
|
|
||||
func lookup(key: String) async -> UIImage? { |
|
||||
let status = get_state(key) |
|
||||
|
|
||||
switch status { |
|
||||
case .done: |
|
||||
break |
|
||||
case .processing: |
|
||||
await wait_for_image(key) |
|
||||
case .none: |
|
||||
return lookup_file_cache(key: key) |
|
||||
} |
|
||||
|
|
||||
if let decoded = cache.object(forKey: NSString(string: key)) { |
|
||||
return decoded |
|
||||
} |
|
||||
|
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
func remove(key: String) { |
|
||||
lock.lock(); defer { lock.unlock() } |
|
||||
cache.removeObject(forKey: NSString(string: key)) |
|
||||
} |
|
||||
|
|
||||
func insert(_ image: UIImage, key: String) async -> UIImage? { |
|
||||
let scale = await UIScreen.main.scale |
|
||||
let size = CGSize(width: PFP_SIZE * scale, height: PFP_SIZE * scale) |
|
||||
|
|
||||
set_state(key, new_state: .processing) |
|
||||
|
|
||||
let decoded_image = await image.byPreparingThumbnail(ofSize: size) |
|
||||
|
|
||||
save_to_memory_cache(key: key, img: decoded_image ?? UIImage()) |
|
||||
if let img = decoded_image { |
|
||||
if !save_to_file_cache(key: key, img: img) { |
|
||||
print("failed saving \(key) pfp to file cache") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return decoded_image |
|
||||
} |
|
||||
|
|
||||
func save_to_file_cache(key: String, img: UIImage, suffix: String = "_pfp") -> Bool { |
|
||||
guard let url = get_cache_url(key: key, suffix: suffix) else { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
guard let data = img.pngData() else { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
return (try? data.write(to: url)) != nil |
|
||||
} |
|
||||
|
|
||||
func save_to_memory_cache(key: String, img: UIImage) { |
|
||||
lock.lock() |
|
||||
cache.setObject(img, forKey: NSString(string: key)) |
|
||||
state[key] = .done |
|
||||
lock.unlock() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func load_image(cache: ImageCache, from url: URL, key: String) async -> UIImage? { |
|
||||
guard let (data, _) = try? await URLSession.shared.data(from: url) else { |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
guard let img = UIImage(data: data) else { |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
return await cache.insert(img, key: key) |
|
||||
} |
|
||||
|
|
||||
|
|
||||
func hashed_hexstring(_ str: String) -> String { |
|
||||
guard let data = str.data(using: .utf8) else { |
|
||||
return str |
|
||||
} |
|
||||
|
|
||||
return hex_encode(sha256(data)) |
|
||||
} |
|
||||
|
|
||||
func pfp_cache_key(url: URL) -> String { |
|
||||
return hashed_hexstring(url.absoluteString) |
|
||||
} |
|
Loading…
Reference in new issue