Fixes for some scaling issues and code cleanup #17

Merged
vegardbm merged 7 commits from scale-issues into master 2026-07-14 23:30:07 +02:00
+131 -137
View File
@@ -193,8 +193,7 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
metadataList[dir] = metadata
loadLocalState()
let volCovP = ProgressIndices(v: progress.v, c: 0, i: 0)
let volumeDir = currentPath.appendingPathComponent("volumes")
let archivePath = volumeDir.appendingPathComponent(metadata.volumes[volCovP.v].archive)
let archivePath = getArchiveURL(volCovP.v)
let archive = try! Archive(url: archivePath, accessMode: .read)
let filename = metadata.volumes[volCovP.v].chapters[volCovP.c].images[volCovP.i].filename!
let targetEntry = archive[filename]!
@@ -697,67 +696,31 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
func readComic(name: String) {
imageView.image = nil
readerView.isHidden = false
homeView.isHidden = true
setNeedsStatusBarAppearanceUpdate()
if let path = getPathFromComicName(name: name) {
currentPath = path
metadata = metadataList[path]
globalState.comicName = metadata.title
saveGlobalState()
loadLocalState()
if mode != .scroll {
scrollingCollectionView.isHidden = true
leftView.isHidden = false
rightView.isHidden = false
setImages(path: path)
scrollingCollectionView.isHidden = mode != .scroll
leftView.isHidden = mode == .scroll
rightView.isHidden = mode == .scroll
imageView.isHidden = mode == .scroll
if mode == .scroll {
scrollingCollectionView.reloadData()
} else {
setupScrolling()
setImages()
}
} else {
print("path not found")
}
}
func setupScrolling() {
leftView.isHidden = true
rightView.isHidden = true
scrollingCollectionView.isHidden = false
scrollingCollectionView.reloadData()
}
func countFiles() -> Int {
var count = 0
if let enumerator = fileManager.enumerator(
at: currentPath, includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
) {
for case let dir as URL in enumerator {
do {
if let enumerator = fileManager.enumerator(
at: dir, includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]
) {
for case let file as URL in enumerator {
let resourceValues = try file.resourceValues(forKeys: [
.isRegularFileKey,
])
if resourceValues.isRegularFile == true {
count += 1
}
}
}
} catch {
print("Error reading file attributes for \(dir):", error)
}
}
}
return count
}
func setupScrollingCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 0
@@ -1044,13 +1007,11 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
default: break
}
if prev == mode { return }
if mode == .scroll {
setupScrolling()
} else {
let archiveURL = currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive)
if mode != .scroll {
let archiveURL = getArchiveURL(progress.v)
imageLoader.loadImage(
archiveURL: archiveURL,
filename: getImagePath(progress: progress),
filename: getImagePath(progress),
scaling: .scaleAspectFit,
screenSize: UIScreen.main.bounds.size
) { [weak self] image in
@@ -1062,6 +1023,10 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
leftView.isHidden = mode == .scroll
rightView.isHidden = mode == .scroll
imageView.isHidden = mode == .scroll
// This does not optmize for the case where you switch between scrolling and non-scrolling in the same comic. However this badly supported because of the lac of state conversion.
if mode == .scroll {
scrollingCollectionView.reloadData()
}
togglePageTurnDropdown()
saveLocalState()
}
@@ -1190,21 +1155,27 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
}
progress = newProgress
updateInfo()
let path = getImagePath(progress: progress)
let path = getImagePath(progress)
imageLoader.setActiveWindow([path])
let archiveURL = currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive)
imageLoader.loadImage(archiveURL: archiveURL, filename: getImagePath(progress: progress), scaling: scaling, screenSize: UIScreen.main.bounds.size) { [weak self] image in
imageLoader.loadImage(
archiveURL: getArchiveURL(progress.v),
filename: getImagePath(progress),
scaling: scaling,
screenSize: UIScreen.main.bounds.size
) { [weak self] image in
self?.imageView.image = image
}
for _ in 0 ..< preloadCount {
newProgress = getProgressIndicesFromTurn(turn: .next, progress: newProgress)
let archive = metadata.volumes[newProgress.v].archive!
let filename = getImagePath(progress: newProgress)
let archiveURL = currentPath.appendingPathComponent("volumes").appendingPathComponent(archive)
imageLoader.preloadImage(archiveURL: archiveURL, filename: filename, scaling: scaling, screenSize: UIScreen.main.bounds.size)
imageLoader.preloadImage(
archiveURL: getArchiveURL(newProgress.v),
filename: getImagePath(newProgress),
scaling: scaling,
screenSize: UIScreen.main.bounds.size
)
}
saveLocalState()
@@ -1273,7 +1244,11 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
info.text = text
}
func getImagePath(progress: ProgressIndices) -> String {
func getArchiveURL(_ volumeProgress: Int) -> URL {
return currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[volumeProgress].archive)
}
func getImagePath(_ progress: ProgressIndices) -> String {
let (v, c, i) = (progress.v, progress.c, progress.i)
let modernPath = metadata.volumes[v].chapters[c].images[i].filename!
// if fileManager.fileExists(atPath: modernPath.path) {
@@ -1323,11 +1298,11 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
return metadata
}
func setImages(path _: URL) {
func setImages() {
let scaling: UIView.ContentMode = mode == .scroll ? .scaleAspectFill : .scaleAspectFit
imageLoader.loadImage(
archiveURL: currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive),
filename: getImagePath(progress: progress),
archiveURL: getArchiveURL(progress.v),
filename: getImagePath(progress),
scaling: scaling,
screenSize: UIScreen.main.bounds.size
) { [weak self] image in
@@ -1338,7 +1313,12 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
var newProgress = progress
for _ in 0 ..< preloadCount {
newProgress = getProgressIndicesFromTurn(turn: .next, progress: newProgress)
imageLoader.preloadImage(archiveURL: currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive), filename: getImagePath(progress: newProgress), scaling: scaling, screenSize: UIScreen.main.bounds.size)
imageLoader.preloadImage(
archiveURL: getArchiveURL(newProgress.v),
filename: getImagePath(newProgress),
scaling: scaling,
screenSize: UIScreen.main.bounds.size,
)
}
}
@@ -1357,15 +1337,14 @@ class ViewController: UIViewController, UIGestureRecognizerDelegate {
super.viewWillTransition(to: size, with: coordinator)
if mode != .scroll {
imageLoader.loadImage(
archiveURL: currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive),
filename: getImagePath(progress: progress),
archiveURL: getArchiveURL(progress.v),
filename: getImagePath(progress),
scaling: .scaleAspectFit,
screenSize: size
) { image in
self.imageView.image = image
}
} else {
scrollingCollectionView.reloadData()
changedOrientation = true
}
}
@@ -1442,6 +1421,7 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
withReuseIdentifier: "ScrollingImageCell", for: indexPath
)
as! ScrollingImageCell
if mode != .scroll { return cell }
if metadata == nil {
print("metadata is nil, should probably not be the case")
return cell
@@ -1453,9 +1433,10 @@ extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFl
}
}
imageLoader.loadImage(
archiveURL: currentPath.appendingPathComponent("volumes").appendingPathComponent(metadata.volumes[progress.v].archive),
filename: getImagePath(progress: newProgress),
scaling: scaling, screenSize: UIScreen.main.bounds.size) { image in
archiveURL: getArchiveURL(progress.v),
filename: getImagePath(newProgress),
scaling: scaling, screenSize: UIScreen.main.bounds.size
) { image in
cell.imageView.image = image
}
return cell
@@ -1568,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>()
@@ -1578,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
@@ -1613,21 +1596,14 @@ class ImageLoader {
if rotation == .horizontal, let cached = landscapeFillCache.object(forKey: key as NSString) {
completion?(cached); return
}
} else {
print("this should not be reachable")
return
}
if let existing = activeTokens[key], !existing.isCancelled {
if let completion = completion {
loadingTasks[key] = completion
}
return
}
} else { assertionFailure() }
if let completion = completion {
loadingTasks[key] = completion
}
if let existing = activeTokens[key], !existing.isCancelled {
return
}
let token = CancellationToken()
activeTokens[key] = token
@@ -1635,84 +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 var image = UIImage(data: extractedData) else {
finish(nil)
guard let image = cache.object(forKey: key as NSString) ?? UIImage(data: extractData()) else {
finish()
return
}
if token.isCancelled { finish(nil); return }
self.cache.setObject(image, forKey: filename as NSString, cost: imageByteSize(image)!)
// 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 { image = vScaleImage }
}
if token.isCancelled { finish(); return }
if token.isCancelled { finish(nil); return }
let scaledImage = scaleAndCacheImage(
image: image,
orientation: rotation,
screenSize: screenSize,
scaling: scaling,
filename: key,
)
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 { image = hScaleImage }
}
if token.isCancelled { finish(); return }
finish(image)
setImage(scaledImage)
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) {