mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
fix(mobile): decode remote thumbnails at displayed size (#29965)
* fix(mobile): decode remote thumbnails at displayed size * use nullable decode size and reuse the tapped thumbnail in the viewer * clean up decode size naming and guards
This commit is contained in:
parent
c52bf9995b
commit
9862e50aab
18 changed files with 386 additions and 71 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import Accelerate
|
||||
import Flutter
|
||||
import ImageIO
|
||||
import MobileCoreServices
|
||||
import Photos
|
||||
|
||||
|
|
@ -27,21 +28,21 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
|
||||
renderingIntent: .perceptual
|
||||
)!
|
||||
private static let decodeOptions = [
|
||||
private static let decodeOptions: [NSString: Bool] = [
|
||||
kCGImageSourceShouldCache: false,
|
||||
kCGImageSourceShouldCacheImmediately: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true
|
||||
] as CFDictionary
|
||||
]
|
||||
|
||||
func requestImage(url: String, requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
|
||||
func requestImage(url: String, requestId: Int64, preferEncoded: Bool, width: Int64?, height: Int64?, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) {
|
||||
var urlRequest = URLRequest(url: URL(string: url)!)
|
||||
urlRequest.cachePolicy = .returnCacheDataElseLoad
|
||||
|
||||
let request = RemoteImageRequest(id: requestId, completion: completion)
|
||||
|
||||
let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in
|
||||
Self.handleCompletion(request: request, encoded: preferEncoded, data: data, response: response, error: error)
|
||||
Self.handleCompletion(request: request, encoded: preferEncoded, width: width, height: height, data: data, response: response, error: error)
|
||||
}
|
||||
|
||||
request.task = task
|
||||
|
|
@ -49,7 +50,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||
task.resume()
|
||||
}
|
||||
|
||||
private static func handleCompletion(request: RemoteImageRequest, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) {
|
||||
private static func handleCompletion(request: RemoteImageRequest, encoded: Bool, width: Int64?, height: Int64?, data: Data?, response: URLResponse?, error: Error?) {
|
||||
if request.isCancelled {
|
||||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
|
@ -87,8 +88,17 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||
return request.completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil),
|
||||
let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else {
|
||||
guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else {
|
||||
registry.remove(requestId: request.id)
|
||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil)))
|
||||
}
|
||||
|
||||
var options: [NSString: Any] = decodeOptions
|
||||
if let maxPixelSize = targetThumbnailRenderSize(imageSource: imageSource, width: width, height: height) {
|
||||
options[kCGImageSourceThumbnailMaxPixelSize] = maxPixelSize
|
||||
}
|
||||
|
||||
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else {
|
||||
registry.remove(requestId: request.id)
|
||||
return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil)))
|
||||
}
|
||||
|
|
@ -120,6 +130,34 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the longest rendered edge needed to cover the requested size.
|
||||
private static func targetThumbnailRenderSize(imageSource: CGImageSource, width: Int64?, height: Int64?) -> Int? {
|
||||
guard let width,
|
||||
let height,
|
||||
width > 0,
|
||||
height > 0,
|
||||
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
||||
let pixelWidth = properties[kCGImagePropertyPixelWidth] as? NSNumber,
|
||||
let pixelHeight = properties[kCGImagePropertyPixelHeight] as? NSNumber else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let orientation = (properties[kCGImagePropertyOrientation] as? NSNumber)
|
||||
.flatMap { CGImagePropertyOrientation(rawValue: $0.uint32Value) } ?? .up
|
||||
let swapsDimensions: Bool
|
||||
switch orientation {
|
||||
case .leftMirrored, .right, .rightMirrored, .left:
|
||||
swapsDimensions = true
|
||||
default:
|
||||
swapsDimensions = false
|
||||
}
|
||||
let sourceWidth = swapsDimensions ? pixelHeight.doubleValue : pixelWidth.doubleValue
|
||||
let sourceHeight = swapsDimensions ? pixelWidth.doubleValue : pixelHeight.doubleValue
|
||||
let fillScale = max(Double(width) / sourceWidth, Double(height) / sourceHeight)
|
||||
let scale = min(1, fillScale)
|
||||
return scale < 1 ? Int(ceil(max(sourceWidth * scale, sourceHeight * scale))) : nil
|
||||
}
|
||||
|
||||
func cancelRequest(requestId: Int64) {
|
||||
Self.registry.remove(requestId: requestId)?.cancel()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue