mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat(mobile): add support for encoded image requests in local/remote image APIs (#26584)
* feat(mobile): add support for encoded image requests in local and remote image APIs * fix(mobile): handle memory cleanup for cancelled image requests * refactor(mobile): simplify memory management and response handling for encoded image requests * fix(mobile): correct formatting in cancellation check for image requests * Apply suggestion from @mertalev Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com> * refactor(mobile): rename 'encoded' parameter to 'preferEncoded' for clarity in image request APIs * fix(mobile): ensure proper resource cleanup for cancelled image requests * refactor(mobile): streamline codec handling by removing unnecessary descriptor disposal in loadCodec request --------- Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
@@ -7,7 +7,7 @@ class LocalImageRequest {
|
||||
weak var workItem: DispatchWorkItem?
|
||||
var isCancelled = false
|
||||
let callback: (Result<[String: Int64]?, any Error>) -> Void
|
||||
|
||||
|
||||
init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||
self.callback = callback
|
||||
}
|
||||
@@ -30,11 +30,11 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
requestOptions.version = .current
|
||||
return requestOptions
|
||||
}()
|
||||
|
||||
|
||||
private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated)
|
||||
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
|
||||
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
|
||||
|
||||
|
||||
private static var rgbaFormat = vImage_CGImageFormat(
|
||||
bitsPerComponent: 8,
|
||||
bitsPerPixel: 32,
|
||||
@@ -48,12 +48,12 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
assetCache.countLimit = 10000
|
||||
return assetCache
|
||||
}()
|
||||
|
||||
|
||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
|
||||
ImageProcessing.queue.async {
|
||||
guard let data = Data(base64Encoded: thumbhash)
|
||||
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
|
||||
|
||||
|
||||
let (width, height, pointer) = thumbHashToRGBA(hash: data)
|
||||
completion(.success([
|
||||
"pointer": Int64(Int(bitPattern: pointer.baseAddress)),
|
||||
@@ -63,34 +63,77 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||
let request = LocalImageRequest(callback: completion)
|
||||
let item = DispatchWorkItem {
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
ImageProcessing.semaphore.wait()
|
||||
defer {
|
||||
ImageProcessing.semaphore.signal()
|
||||
}
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
guard let asset = Self.requestAsset(assetId: assetId)
|
||||
else {
|
||||
Self.remove(requestId: requestId)
|
||||
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
if preferEncoded {
|
||||
let dataOptions = PHImageRequestOptions()
|
||||
dataOptions.isNetworkAccessAllowed = true
|
||||
dataOptions.isSynchronous = true
|
||||
dataOptions.version = .current
|
||||
|
||||
var imageData: Data?
|
||||
Self.imageManager.requestImageDataAndOrientation(
|
||||
for: asset,
|
||||
options: dataOptions,
|
||||
resultHandler: { (data, _, _, _) in
|
||||
imageData = data
|
||||
}
|
||||
)
|
||||
|
||||
if request.isCancelled {
|
||||
Self.remove(requestId: requestId)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
guard let data = imageData else {
|
||||
Self.remove(requestId: requestId)
|
||||
return completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil)))
|
||||
}
|
||||
|
||||
let length = data.count
|
||||
let pointer = malloc(length)!
|
||||
data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)
|
||||
|
||||
if request.isCancelled {
|
||||
free(pointer)
|
||||
Self.remove(requestId: requestId)
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
request.callback(.success([
|
||||
"pointer": Int64(Int(bitPattern: pointer)),
|
||||
"length": Int64(length),
|
||||
]))
|
||||
Self.remove(requestId: requestId)
|
||||
return
|
||||
}
|
||||
|
||||
var image: UIImage?
|
||||
Self.imageManager.requestImage(
|
||||
for: asset,
|
||||
@@ -101,29 +144,29 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
image = _image
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
guard let image = image,
|
||||
let cgImage = image.cgImage else {
|
||||
Self.remove(requestId: requestId)
|
||||
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
|
||||
}
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let buffer = try vImage_Buffer(cgImage: cgImage, format: Self.rgbaFormat)
|
||||
|
||||
|
||||
if request.isCancelled {
|
||||
buffer.free()
|
||||
return completion(ImageProcessing.cancelledResult)
|
||||
}
|
||||
|
||||
|
||||
request.callback(.success([
|
||||
"pointer": Int64(Int(bitPattern: buffer.data)),
|
||||
"width": Int64(buffer.width),
|
||||
@@ -136,24 +179,24 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
request.workItem = item
|
||||
Self.add(requestId: requestId, request: request)
|
||||
ImageProcessing.queue.async(execute: item)
|
||||
}
|
||||
|
||||
|
||||
func cancelRequest(requestId: Int64) {
|
||||
Self.cancel(requestId: requestId)
|
||||
}
|
||||
|
||||
|
||||
private static func add(requestId: Int64, request: LocalImageRequest) -> Void {
|
||||
requestQueue.sync { requests[requestId] = request }
|
||||
}
|
||||
|
||||
|
||||
private static func remove(requestId: Int64) -> Void {
|
||||
requestQueue.sync { requests[requestId] = nil }
|
||||
}
|
||||
|
||||
|
||||
private static func cancel(requestId: Int64) -> Void {
|
||||
requestQueue.async {
|
||||
guard let request = requests.removeValue(forKey: requestId) else { return }
|
||||
@@ -164,12 +207,12 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static func requestAsset(assetId: String) -> PHAsset? {
|
||||
var asset: PHAsset?
|
||||
assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) }
|
||||
if asset != nil { return asset }
|
||||
|
||||
|
||||
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
|
||||
else { return nil }
|
||||
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
|
||||
|
||||
Reference in New Issue
Block a user