Skip to content

feat(mobile): add support for encoded image requests in local/remote image APIs#26584

Merged
mertalev merged 8 commits intoimmich-app:mainfrom
LeLunZ:native-encoded-image-pipeline
Feb 28, 2026
Merged

feat(mobile): add support for encoded image requests in local/remote image APIs#26584
mertalev merged 8 commits intoimmich-app:mainfrom
LeLunZ:native-encoded-image-pipeline

Conversation

@LeLunZ
Copy link
Collaborator

@LeLunZ LeLunZ commented Feb 27, 2026

This PR adds support for loading encoded images. This is also part of #26148

The "Call" pipeline looks like that:

  • image_provider.dart - loadCodecRequest calls loadCoded on ImageRequests
  • loadCoded is implemented by RemoteImageRequest and LocalImageRequest these call -> _codecFromEncodedPlatformImage which is implemented in the base ImageRequest.dart
  • then there is a encoded parameter which tells the native side to not return decoded images. The RemoteImage implementation on android already returns encoded images, so there the parameter is ignored. But for Local Images on IOS and Android and Remote Images on IOS there is a new code path that returns different values if the bool is set.

This PR adds the foundation of supporting animated images, because without the full coded we can't use the MultiFrameImageStreamCompleter.

when #26541 is merged, I then can:

  • Persist playbackStyle in local asset database

And if this here gets merged we then can based on the playbackStyle decide if we want to show normal images like its currently implemented, or load the codec and show animated images.

Copilot AI review requested due to automatic review settings February 27, 2026 23:39
@LeLunZ
Copy link
Collaborator Author

LeLunZ commented Feb 27, 2026

@mertalev another part of the code which I could extract

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an “encoded image” request mode to the local/remote image platform APIs and introduces a loadCodec() pathway in Flutter to enable future animated-image rendering (multi-frame codec support).

Changes:

  • Extend Pigeon LocalImageApi / RemoteImageApi with a required encoded boolean flag and propagate it through generated bindings.
  • Add ImageRequest.loadCodec() plus shared _codecFromEncodedPlatformImage(...) helper, and implement codec loading for local/remote requests.
  • Implement native encoded-return paths on iOS (local + remote) and Android (local), while Android remote keeps returning encoded bytes.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
mobile/pigeon/remote_image_api.dart Adds encoded flag to remote request API definition.
mobile/pigeon/local_image_api.dart Adds encoded flag to local request API definition.
mobile/lib/platform/remote_image_api.g.dart Regenerates Flutter-side channel call to include encoded.
mobile/lib/platform/local_image_api.g.dart Regenerates Flutter-side channel call to include encoded.
mobile/lib/presentation/widgets/images/image_provider.dart Adds loadCodecRequest(...) helper to fetch a ui.Codec.
mobile/lib/infrastructure/loaders/image_request.dart Adds loadCodec() to the abstraction and shared encoded->codec helper.
mobile/lib/infrastructure/loaders/remote_image_request.dart Implements codec loading via encoded bytes for remote images.
mobile/lib/infrastructure/loaders/local_image_request.dart Implements codec loading via encoded bytes for local assets.
mobile/lib/infrastructure/loaders/thumbhash_image_request.dart Adds loadCodec() override (currently throws).
mobile/ios/Runner/Images/RemoteImagesImpl.swift Adds encoded handling for remote requests (raw bytes vs decoded RGBA).
mobile/ios/Runner/Images/RemoteImages.g.swift Regenerates iOS Pigeon protocol/signature to include encoded.
mobile/ios/Runner/Images/LocalImagesImpl.swift Adds encoded handling for local requests (raw bytes vs decoded RGBA).
mobile/ios/Runner/Images/LocalImages.g.swift Regenerates iOS Pigeon protocol/signature to include encoded.
mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt Updates signature to accept encoded (ignored on Android remote).
mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt Regenerates Android Pigeon interface to include encoded.
mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt Adds encoded local-image code path returning pointer/length.
mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt Regenerates Android Pigeon interface to include encoded.
Comments suppressed due to low confidence (4)

mobile/lib/infrastructure/loaders/thumbhash_image_request.dart:21

  • ThumbhashImageRequest.loadCodec() throws UnsupportedError, but the ImageRequest.loadCodec() contract is already nullable. Throwing here will surface as an exception if a caller accidentally requests a codec for a thumbhash (e.g., via a shared code path for animated images). Prefer returning null (or a no-op codec strategy) to keep callers from crashing unexpectedly.
  @override
  Future<ui.Codec?> loadCodec() => throw UnsupportedError('Thumbhash does not support codec loading');

mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt:164

  • The encoded path reads the entire original asset into a ByteArray (readBytes()), then copies it into NativeBuffer, and then Dart copies again into an ImmutableBuffer. For large images this can cause significant peak memory and jank/OOM. Consider streaming into the native buffer (chunked reads) or using an AssetFileDescriptor/file-channel mapping to avoid multiple full-size copies, and/or enforcing a size limit for this path.
    signal.throwIfCanceled()
    val bytes = resolver.openInputStream(uri)?.use { it.readBytes() }
      ?: throw IOException("Could not read image data for $assetId")

    signal.throwIfCanceled()
    val pointer = NativeBuffer.allocate(bytes.size)
    try {
      val buffer = NativeBuffer.wrap(pointer, bytes.size)
      buffer.put(bytes)
      signal.throwIfCanceled()
      callback(Result.success(mapOf(
        "pointer" to pointer,
        "length" to bytes.size.toLong()
      )))

mobile/ios/Runner/Images/RemoteImagesImpl.swift:102

  • In the encoded branch, malloc(length)! can crash when data.count == 0 because malloc(0) is allowed to return nil. Add an explicit length > 0 check (return an error for empty responses) or handle zero-length allocation safely before force-unwrapping.
      // Return raw encoded bytes when requested (for animated images)
      if encoded {
        let length = data.count
        let pointer = malloc(length)!
        data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length)

        if request.isCancelled {
          free(pointer)
          return request.completion(ImageProcessing.cancelledResult)
        }

        return request.completion(
          .success([
            "pointer": Int64(Int(bitPattern: pointer)),
            "length": Int64(length),
          ]))

mobile/ios/Runner/Images/LocalImagesImpl.swift:132

  • In the encoded branch, malloc(length)! can crash when the asset data is empty (length == 0) because malloc(0) may return nil. Guard against zero-length data (fail the request) or handle zero-length allocation safely before force-unwrapping.
        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),
        ]))

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@LeLunZ
Copy link
Collaborator Author

LeLunZ commented Feb 28, 2026

Fixed the formatting issue

LeLunZ and others added 3 commits February 28, 2026 11:31
Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
@LeLunZ
Copy link
Collaborator Author

LeLunZ commented Feb 28, 2026

@mertalev added the changes, loadCodec now doesn't destroy the Codec and lets GC handle it

@mertalev mertalev merged commit ac5ef6a into immich-app:main Feb 28, 2026
44 checks passed
babbitt pushed a commit to babbitt/immich that referenced this pull request Mar 1, 2026
…image APIs (immich-app#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants