ImageLoader: add method for scaling images, cache unscaled images, and

set image early.
This commit was merged in pull request #17.
This commit is contained in:
2026-07-14 17:48:15 +02:00
parent 554a4022c9
commit 9acb6a3e3c
+77 -63
View File
@@ -1549,6 +1549,7 @@ enum Rotation {
}
class ImageLoader {
private let cache = NSCache<NSString, UIImage>()
private let portraitFitCache = NSCache<NSString, UIImage>()
private let landscapeFitCache = NSCache<NSString, UIImage>()
private let portraitFillCache = NSCache<NSString, UIImage>()
@@ -1559,6 +1560,7 @@ class ImageLoader {
init() {
// 128 MiB
cache.totalCostLimit = 128 * 1024 * 1024
portraitFitCache.totalCostLimit = 128 * 1024 * 1024
landscapeFitCache.totalCostLimit = 128 * 1024 * 1024
portraitFillCache.totalCostLimit = 128 * 1024 * 1024
@@ -1594,13 +1596,9 @@ class ImageLoader {
if rotation == .horizontal, let cached = landscapeFillCache.object(forKey: key as NSString) {
completion?(cached); return
}
} else {
print("this should not be reachable")
return
}
} else { assertionFailure() }
if let completion = completion {
assert(loadingTasks[key] == nil)
loadingTasks[key] = completion
}
if let existing = activeTokens[key], !existing.isCancelled {
@@ -1613,86 +1611,102 @@ class ImageLoader {
queue.async { [weak self] in
guard let self = self else { return }
func finish(_ image: UIImage?) {
func finish() {
DispatchQueue.main.async {
guard self.activeTokens[key] === token else { return }
self.activeTokens.removeValue(forKey: key)
if let image = image {
if let callback = self.loadingTasks.removeValue(forKey: key) {
callback(image)
}
self.loadingTasks.removeValue(forKey: key)
}
}
func setImage(_ image: UIImage) {
DispatchQueue.main.async {
if let callback = self.loadingTasks[key] {
callback(image)
}
}
}
if token.isCancelled { finish(nil); return }
if token.isCancelled { finish(); return }
var extractedData = Data()
if self.archives.object(forKey: archiveURL.lastPathComponent as NSString) == nil {
let archive = try! Archive(url: archiveURL, accessMode: .read)
self.archives.setObject(archive, forKey: archiveURL.lastPathComponent as NSString)
}
let archive = self.archives.object(forKey: archiveURL.lastPathComponent as NSString)!
_ = try! archive.extract(archive[filename]!) { data in
extractedData.append(data)
func extractData() -> Data {
var extractedData = Data()
if self.archives.object(forKey: archiveURL.lastPathComponent as NSString) == nil {
let archive = try! Archive(url: archiveURL, accessMode: .read)
self.archives.setObject(archive, forKey: archiveURL.lastPathComponent as NSString)
}
let archive = self.archives.object(forKey: archiveURL.lastPathComponent as NSString)!
_ = try! archive.extract(archive[filename]!) { data in
extractedData.append(data)
}
return extractedData
}
if token.isCancelled { finish(nil); return }
if token.isCancelled { finish(); return }
guard let image = UIImage(data: extractedData) else {
finish(nil)
guard let image = cache.object(forKey: key as NSString) ?? UIImage(data: extractData()) else {
finish()
return
}
var finalImage: UIImage!
self.cache.setObject(image, forKey: filename as NSString, cost: imageByteSize(image)!)
if token.isCancelled { finish(nil); return }
if token.isCancelled { finish(); return }
// If you turn pages fast, completion will not be nil and as such only the needed scaled image should be prepared
if completion == nil || rotation == .vertical {
let vertical = CGSize(
width: min(screenSize.width, screenSize.height),
height: max(screenSize.height, screenSize.width)
)
let vScaledSize = aspectSize(for: image.size, in: vertical, scaling: scaling)
let vScaleImage = resizeImage(image, to: vScaledSize)
let cost = imageByteSize(vScaleImage)!
if scaling == .scaleAspectFit {
self.portraitFitCache.setObject(vScaleImage, forKey: filename as NSString, cost: cost)
} else if scaling == .scaleAspectFill {
self.portraitFillCache.setObject(vScaleImage, forKey: filename as NSString, cost: cost)
} else {
print("this should be unreachable")
return
}
if rotation == .vertical { finalImage = vScaleImage }
}
let scaledImage = scaleAndCacheImage(
image: image,
orientation: rotation,
screenSize: screenSize,
scaling: scaling,
filename: key,
)
if token.isCancelled { finish(nil); return }
if token.isCancelled { finish(); return }
if completion == nil || rotation == .horizontal {
let horizontal = CGSize(
width: max(screenSize.width, screenSize.height),
height: min(screenSize.height, screenSize.width)
)
let hScaledSize = aspectSize(for: image.size, in: horizontal, scaling: scaling)
let hScaleImage = resizeImage(image, to: hScaledSize)
let cost = imageByteSize(hScaleImage)!
if scaling == .scaleAspectFit {
self.landscapeFitCache.setObject(hScaleImage, forKey: filename as NSString, cost: cost)
} else if scaling == .scaleAspectFill {
self.landscapeFillCache.setObject(hScaleImage, forKey: filename as NSString, cost: cost)
} else {
print("this should be unreachable")
return
}
if rotation == .horizontal { finalImage = hScaleImage }
}
setImage(scaledImage)
finish(finalImage)
let otherOrientation: Rotation = rotation == .vertical ? .horizontal : .vertical
_ = scaleAndCacheImage(
image: image,
orientation: otherOrientation,
screenSize: screenSize,
scaling: scaling,
filename: key,
)
finish()
}
}
func scaleAndCacheImage(image: UIImage, orientation: Rotation, screenSize: CGSize, scaling: UIView.ContentMode, filename: String) -> UIImage {
let resizeTo: CGSize = if orientation == .vertical {
CGSize(
width: min(screenSize.width, screenSize.height),
height: max(screenSize.height, screenSize.width)
)
} else {
CGSize(
width: max(screenSize.width, screenSize.height),
height: min(screenSize.height, screenSize.width)
)
}
let scaledSize = aspectSize(for: image.size, in: resizeTo, scaling: scaling)
let scaledImage = resizeImage(image, to: scaledSize)
let cost = imageByteSize(scaledImage)!
switch ((orientation, scaling)) {
case (.vertical, .scaleAspectFit):
self.portraitFitCache.setObject(scaledImage, forKey: filename as NSString, cost: cost)
case (.vertical, .scaleAspectFill):
self.portraitFillCache.setObject(scaledImage, forKey: filename as NSString, cost: cost)
case (.horizontal, .scaleAspectFit):
self.landscapeFitCache.setObject(scaledImage, forKey: filename as NSString, cost: cost)
case (.horizontal, .scaleAspectFill):
self.landscapeFillCache.setObject(scaledImage, forKey: filename as NSString, cost: cost)
default:
assertionFailure()
}
return scaledImage
}
func setActiveWindow(_ paths: [String]) {
let desired = Set(paths)
for (key, token) in activeTokens where !desired.contains(key) {