Skip to content

feat: support loading local images#588

Open
eszlamczyk wants to merge 5 commits into
mainfrom
feat/377/local-images
Open

feat: support loading local images#588
eszlamczyk wants to merge 5 commits into
mainfrom
feat/377/local-images

Conversation

@eszlamczyk

@eszlamczyk eszlamczyk commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What/Why?

Fixes #377.

Bundled/local images never rendered outside dev mode. In dev, require()'d assets resolve to Metro http URLs, which go through the network loaders and work. In a bundled app they resolve to shapes the library did not understand:

  • Android release: a bare drawable resource name (e.g. src_assets_logo) - this is also exactly what expo-asset hands out as uri/localUri
  • Android with expo-updates: file:///android_res/<dir>/<file> paths, which are not real filesystem paths
  • iOS release: percent-encoded file:// URLs near the app bundle

The Android side only stripped a file:// prefix and called BitmapFactory.decodeFile(); iOS pushed every URL through NSURLSession. This PR adds proper local-source resolution on both platforms, mirroring what React Native core does for <Image>:

  • Android - new LocalImageLoader dispatches on Uri.parse().scheme, following RN's ImageSource/ResourceDrawableIdHelper and expo-asset's ResourceAsset.kt: bare resource names (lowercased, - replaced with _, numeric ids accepted, drawable then raw), file:// with percent-decoding, file:///android_res/ with density-qualifier stripping, file:///android_asset/, asset://, res://, content://, and data: base64 URIs. All decodes reuse the existing screen-width downsampling.
  • iOS/macOS - new ENRMLocalImageLoader mirrors RCTImageFromLocalAssetURL: bundle-relative sources go through imageNamed: first (asset catalogs and 2x/3x scale selection for free), falling back to direct file loading with percent-decoding via fileSystemRepresentation. ENRMImageDownloader branches to it before the NSURLSession path, so local files skip HTTP caching/timeouts but keep both memory cache tiers. ENRMImageAttachment is unchanged.

Testing

Dev mode (both platforms)

  1. Open Storybook, EnrichedMarkdownText/Block/Image:
    • Default - remote image still loads (network path regression check)
    • LocalAsset - renders via Metro URL; description shows the resolved URI
    • DataUri - checkerboard renders; on Android this runs the new LocalImageLoader even in dev
  2. Re-enter the DataUri/LocalAsset stories to confirm memory-cache hits render instantly (no flash of empty span).

Release builds (the actual #377 scenario)
3. Android: yarn android:test:release, open the LocalAsset story - the URI is now a bare drawable name and must render through the resource lookup.
4. iOS: build the example app in Release - LocalAsset resolves to a bundle file:// URL and must render through ENRMLocalImageLoader.
5. Rotate/resize on both to confirm the processed-variant cache and reflow still work for locally loaded images.

Regression checks
6. Remote images with imageRequestHeaders still fetch and cache per-header (local sources ignore headers by design).
8. Inline images (image next to text) still render for both local and remote sources.

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class support for rendering bundled/local image sources in markdown across iOS/macOS and Android, aligning behavior with React Native’s <Image> resolution so release builds work the same as dev (Fixes #377).

Changes:

  • iOS/macOS: introduce a local image loader and branch ENRMImageDownloader to load local sources without NSURLSession.
  • Android: add LocalImageLoader to handle multiple non-network URI shapes (resources, android_res/android_asset, content, data URIs) and route non-HTTP(S) spans through it.
  • Docs + example: document supported URI forms and add Storybook stories for local asset + data URI.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/react-native-enriched-markdown/ios/utils/ENRMLocalImageLoader.m New iOS/macOS local image resolution implementation (bundle-relative + file loading).
packages/react-native-enriched-markdown/ios/utils/ENRMLocalImageLoader.h Public API/docs for local image detection + loading.
packages/react-native-enriched-markdown/ios/utils/ENRMImageDownloader.m Routes local sources through the local loader before NSURLSession.
packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/LocalImageLoader.kt New Android local-source decoder supporting RN/expo URI variants.
packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageDownloader.kt Adds a bytes-based downsample decode helper used by local loading paths.
packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt Uses scheme parsing to route non-HTTP(S) images through LocalImageLoader.
packages/android-enriched-markdown/ui/src/main/java/com/swmansion/enriched/markdown/utils/text/LocalImageLoader.kt Mirrors Android local-source decoder for the UI package.
packages/android-enriched-markdown/ui/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageDownloader.kt Adds bytes-based downsample decode helper in the UI package.
packages/android-enriched-markdown/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt Routes non-HTTP(S) images through LocalImageLoader in the UI package.
docs/IMAGE_CACHING.md Documents supported image source formats and behavior differences for local vs network.
apps/react-native-example/.rnstorybook/stories/components/EnrichedMarkdownText/block/Image.stories.tsx Adds Storybook coverage for local bundled assets and data URIs.
Comments suppressed due to low confidence (1)

packages/react-native-enriched-markdown/ios/utils/ENRMLocalImageLoader.m:55

  • decoded is an NSString; absolutePath isn’t an NSString API and should fail to compile. If the intent is to branch on absolute vs bundle-relative paths, use isAbsolutePath.
    if (decoded.absolutePath) {

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

Comment thread packages/react-native-enriched-markdown/ios/utils/ENRMImageDownloader.m Outdated
@eszlamczyk
eszlamczyk requested a review from hryhoriiK97 July 24, 2026 16:15
@eszlamczyk
eszlamczyk marked this pull request as ready for review July 24, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support loading local images

2 participants