scale image with ImageLoader to offload main thread

This commit is contained in:
2025-08-04 17:33:41 +02:00
parent 1d1e2db9ce
commit cdbc55683e

View File

@@ -729,7 +729,8 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
chapter: currentChapter,
volume: Int(metadata.chapters[currentChapter - 1].volume),
page: currentPage,
path: currentPath)
path: currentPath),
scaling: .scaleAspectFit
) {
[weak self] image in
self?.imageView.image = image
@@ -856,7 +857,8 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
func setupImageView() {
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = UIView.ContentMode.scaleAspectFit
// Scaling is done when the image is loaded to avoid scaling on main thread
imageView.contentMode = .center
imageView.clipsToBounds = true
readerView.addSubview(imageView)
@@ -870,12 +872,13 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
func changeImage(turn: PageTurn) {
let scaling = UIView.ContentMode.scaleAspectFit
var (chapter, page) = getChapterAndPageFromTurn(
chapter: currentChapter, page: currentPage, turn: turn)
if (chapter, page) == (currentChapter, currentPage) { return }
var vol = Int(metadata.chapters[chapter - 1].volume)
if let path = getImagePath(chapter: chapter, volume: vol, page: page, path: currentPath) {
imageLoader.loadImage(at: path) {
imageLoader.loadImage(at: path, scaling: scaling) {
[weak self] image in
self?.imageView.image = image
}
@@ -892,7 +895,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
if let path = getImagePath(
chapter: chapter, volume: vol, page: page, path: currentPath)
{
imageLoader.preloadImage(at: path)
imageLoader.preloadImage(at: path, scaling: scaling)
} else {
print("could not preload image")
}
@@ -918,9 +921,14 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
Chapter \(currentChapter!) of \(Int(metadata.last_chapter))
Page \(currentPage!) of \(metadata.chapters[currentChapter - 1].pages)
"""
if let image = imageView.image {
if let size = imageLoader.size[
getImagePath(
chapter: currentChapter, volume: Int(metadata.chapters[currentChapter - 1].volume),
page: currentPage!, path: currentPath
).path]
{
text =
text + "\nImage size: \(Int(image.size.width))x\(Int(image.size.height))"
text + "\nImage size: \(Int(size.width))x\(Int(size.height))"
}
info.text = text
}
@@ -928,10 +936,10 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
func getImagePath(chapter: Int, volume: Int, page: Int, path: URL) -> URL! {
let modernPath = path.appendingPathComponent(String(format: "volume_%04d", volume))
.appendingPathComponent(String(format: "chapter_%04d_page_%04d.png", chapter, page))
if fileManager.fileExists(atPath: modernPath.path) {
return modernPath
}
return nil
// if fileManager.fileExists(atPath: modernPath.path) {
return modernPath
// }
// return nil
}
func getChapterAndPageFromTurn(chapter: Int, page: Int, turn: PageTurn) -> (Int, Int) {
@@ -977,13 +985,15 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
func setImages(path: URL, metadata: Metadata) {
let scaling = UIView.ContentMode.scaleAspectFit
var directories: [URL] = []
if currentPage != nil && currentChapter != nil {
var vol = Int(metadata.chapters[currentChapter - 1].volume)
imageLoader.loadImage(
at: getImagePath(
chapter: currentChapter, volume: vol, page: currentPage, path: currentPath
)
),
scaling: scaling
) {
[weak self] image in
self?.imageView.image = image
@@ -997,7 +1007,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
if let path = getImagePath(
chapter: chapter, volume: vol, page: page, path: currentPath)
{
imageLoader.preloadImage(at: path)
imageLoader.preloadImage(at: path, scaling: scaling)
} else {
print("could not preload image")
}
@@ -1103,6 +1113,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
cell.imageView.image = comics[indexPath.item].cover
return cell
} else if collectionView == scrollingCollectionView {
let scaling = UIView.ContentMode.scaleAspectFill
let cell =
collectionView.dequeueReusableCell(
withReuseIdentifier: "ScrollingImageCell", for: indexPath)
@@ -1116,7 +1127,9 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
volume: Int(metadata.chapters[chapter - 1].volume),
page: page, path: currentPath)
{
imageLoader.loadImage(at: url) { image in cell.imageView.image = image }
imageLoader.loadImage(at: url, scaling: scaling) { image in
cell.imageView.image = image
}
for _ in 0...preloadCount {
(chapter, page) = getChapterAndPageFromTurn(
chapter: chapter, page: page, turn: .next)
@@ -1125,7 +1138,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
volume: Int(metadata.chapters[chapter - 1].volume),
page: page, path: currentPath)
{
imageLoader.preloadImage(at: url)
imageLoader.preloadImage(at: url, scaling: scaling)
} else {
break
}
@@ -1204,7 +1217,8 @@ class ScrollingImageCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
imageView.contentMode = .scaleAspectFill
// Scaling is done when the image is loaded to avoid scaling on main thread
imageView.contentMode = .center
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(imageView)
@@ -1248,17 +1262,18 @@ class ComicImageCell: UICollectionViewCell {
class ImageLoader {
private let cache = NSCache<NSString, UIImage>()
private var loadingTasks: [String: [((UIImage?) -> Void)]] = [:]
var size: [String: CGSize] = [:]
init() {
// 128 MiB
cache.totalCostLimit = 128 * 1024 * 1024
}
func preloadImage(at path: URL) {
loadImage(at: path, completion: nil)
func preloadImage(at path: URL, scaling: UIView.ContentMode) {
loadImage(at: path, scaling: scaling, completion: nil)
}
func loadImage(at path: URL, completion: ((UIImage?) -> Void)?) {
func loadImage(at path: URL, scaling: UIView.ContentMode, completion: ((UIImage?) -> Void)?) {
if let cached = cache.object(forKey: path.path as NSString) {
completion?(cached)
return
@@ -1275,14 +1290,13 @@ class ImageLoader {
queue.async { [weak self] in
guard let self = self else { return }
let image = UIImage(contentsOfFile: path.path)
if let image = image, let cost = imageByteSize(image) {
self.cache.setObject(image, forKey: path.path as NSString, cost: cost)
} else {
print("no image to cache found")
}
guard var image = UIImage(contentsOfFile: path.path) else { return }
self.size[path.path] = image.size
let scaledSize = aspectSize(
for: image.size, in: UIScreen.main.bounds.size, scaling: scaling)
image = resizeImage(image, to: scaledSize)
guard let cost = imageByteSize(image) else { return }
self.cache.setObject(image, forKey: path.path as NSString, cost: cost)
DispatchQueue.main.async {
self.loadingTasks[path.path]?.forEach { $0(image) }
@@ -1300,3 +1314,29 @@ func imageByteSize(_ image: UIImage) -> Int? {
guard let cgImage = image.cgImage else { return nil }
return cgImage.bytesPerRow * cgImage.height
}
func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
image.draw(in: CGRect(origin: .zero, size: size))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return scaledImage!
}
func aspectSize(for imageSize: CGSize, in boundingSize: CGSize, scaling: UIView.ContentMode)
-> CGSize
{
let widthRatio = boundingSize.width / imageSize.width
let heightRatio = boundingSize.height / imageSize.height
let scale: CGFloat =
switch scaling {
case .scaleAspectFit: min(widthRatio, heightRatio)
case .scaleAspectFill: max(widthRatio, heightRatio)
default: 1
}
return CGSize(
width: imageSize.width * scale,
height: imageSize.height * scale
)
}