Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
df87952
feat: add `loadFromAssetAsync`
bglgwyng Jul 5, 2025
67bee8a
feat: declare `AssetImageLoadOptions`
bglgwyng Jul 5, 2025
d4fed55
feat: add support for custom image sizing and orientation in media li…
bglgwyng Jul 8, 2025
8b376d6
refactor: standardize error handling in `HybridImageFactory`
bglgwyng Jul 8, 2025
91c1c78
refactor: rename `NitroMediaLibraryImageTab` to `NitroAssetImageTab`
bglgwyng Jul 8, 2025
e06fdaf
style: lint
bglgwyng Jul 8, 2025
f9fd4ce
doc: update the `loadFromAssetAsync` description
bglgwyng Jul 8, 2025
5f20be4
style: format
bglgwyng Jul 8, 2025
efd82c3
style: format
bglgwyng Jul 8, 2025
fa2dd0b
fix: fix asset not found error message
bglgwyng Jul 8, 2025
a1c92f1
refactor: move orientation mapping to `CGImagePropertyOrientation` ex…
bglgwyng Jul 9, 2025
3edca91
refactor: move `aspectFit` mapping to `PHImageContentMode` extension
bglgwyng Jul 9, 2025
e51f563
refactor: separate definitions
bglgwyng Jul 9, 2025
900f07a
refactor: add async `PHImageManager` extension
bglgwyng Jul 9, 2025
5ea8d1f
feat: add unsupported `loadFromAssetAsync` method stub for Android pl…
bglgwyng Jul 9, 2025
429bed1
doc: remove unnecessary comment
bglgwyng Jul 9, 2025
92510b7
Merge branch 'main' into support-images-from-media-library
bglgwyng Jul 16, 2025
5263248
chore: update lockfile
bglgwyng Jul 16, 2025
fb89847
fix: add missing files
bglgwyng Jul 16, 2025
0ebe155
chore: replace eslint with biome for linting
bglgwyng Jul 16, 2025
19db201
style: lint
bglgwyng Jul 16, 2025
14e50a8
feat: add privacy access reason 3B52.1 to PrivacyInfo.xcprivacy
bglgwyng Jul 17, 2025
76cefdf
ci: set Xcode 16.4 version for iOS build workflow
bglgwyng Jul 19, 2025
67d87f5
Merge branch 'main' into support-images-from-media-library
bglgwyng Jul 23, 2025
2864e3b
chore: style
bglgwyng Jul 24, 2025
ffa92b8
fix: rerun nitrogen
bglgwyng Jul 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2

- name: Select Xcode version
run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer

- name: Install npm dependencies (bun)
run: bun install

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class HybridImageFactory: HybridImageFactorySpec() {
override fun loadFromResourcesAsync(name: String): Promise<HybridImageSpec> {
return Promise.async { loadFromResources(name) }
}

override fun loadFromAssetAsync(assetName: String, options: AssetImageLoadOptions?): Promise<HybridImageSpec> {
throw Error("ImageFactory.loadFromAssetAsync(assetName:options:) is not supported on Android!")
}

override fun loadFromSymbol(symbolName: String): HybridImageSpec {
throw Error("ImageFactory.loadFromSymbol(symbolName:) is not supported on Android!")
Expand Down
7 changes: 5 additions & 2 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"name": "NitroImageExample",
"version": "0.0.1",
"dependencies": {
"@react-native-camera-roll/camera-roll": "^7.10.1",
"@react-navigation/bottom-tabs": "^7.3.14",
"@react-navigation/native": "^7.1.10",
"react": "19.0.0",
Expand Down Expand Up @@ -382,6 +383,8 @@

"@pnpm/npm-conf": ["@pnpm/[email protected]", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw=="],

"@react-native-camera-roll/camera-roll": ["@react-native-camera-roll/[email protected]", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-6zuK+E+z3a4Nij5OrkMh9BL7J1/Eg0PB8iX7/chNwhghpTZ93cr3Zrj/02ueglN0BV/tIKmb+BDERfzVIGRT7w=="],

"@react-native-community/cli": ["@react-native-community/[email protected]", "", { "dependencies": { "@react-native-community/cli-clean": "18.0.0", "@react-native-community/cli-config": "18.0.0", "@react-native-community/cli-doctor": "18.0.0", "@react-native-community/cli-server-api": "18.0.0", "@react-native-community/cli-tools": "18.0.0", "@react-native-community/cli-types": "18.0.0", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-DyKptlG78XPFo7tDod+we5a3R+U9qjyhaVFbOPvH4pFNu5Dehewtol/srl44K6Cszq0aEMlAJZ3juk0W4WnOJA=="],

"@react-native-community/cli-clean": ["@react-native-community/[email protected]", "", { "dependencies": { "@react-native-community/cli-tools": "18.0.0", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-glob": "^3.3.2" } }, "sha512-+k64EnJaMI5U8iNDF9AftHBJW+pO/isAhncEXuKRc6IjRtIh6yoaUIIf5+C98fgjfux7CNRZAMQIkPbZodv2Gw=="],
Expand Down Expand Up @@ -742,7 +745,7 @@

"ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],

"electron-to-chromium": ["[email protected].183", "", {}, "sha512-vCrDBYjQCAEefWGjlK3EpoSKfKbT10pR4XXPdn65q7snuNOZnthoVpBfZPykmDapOKfoD+MMIPG8ZjKyyc9oHA=="],
"electron-to-chromium": ["[email protected].185", "", {}, "sha512-dYOZfUk57hSMPePoIQ1fZWl1Fkj+OshhEVuPacNKWzC1efe56OsHY3l/jCfiAgIICOU3VgOIdoq7ahg7r7n6MQ=="],

"emoji-regex": ["[email protected]", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],

Expand Down Expand Up @@ -1240,7 +1243,7 @@

"react-native-safe-area-context": ["[email protected]", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw=="],

"react-native-screens": ["react-native-screens@4.11.1", "", { "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.1.7", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-F0zOzRVa3ptZfLpD0J8ROdo+y1fEPw+VBFq1MTY/iyDu08al7qFUO5hLMd+EYMda5VXGaTFCa8q7bOppUszhJw=="],
"react-native-screens": ["react-native-screens@4.12.0", "", { "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.1.7", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-T2KL6RcDSYDRZswh9glRe600Hvaeq240U21eaqv0uxCNmJz05UeFc4YGQgbFPI8XsakPKx3HjNonItxElFy+QA=="],

"react-refresh": ["[email protected]", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],

Expand Down
2 changes: 2 additions & 0 deletions example/ios/NitroImageExample/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSPhotoLibraryUsageDescription</key>
<string></string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
Expand Down
1 change: 1 addition & 0 deletions example/ios/NitroImageExample/PrivacyInfo.xcprivacy
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
<string>3B52.1</string>
</array>
</dict>
<dict>
Expand Down
56 changes: 42 additions & 14 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ PODS:
- ReactCommon/turbomodule/core
- SDWebImage
- Yoga
- NitroModules (0.26.3):
- NitroModules (0.26.4):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1396,7 +1396,7 @@ PODS:
- React-jsiexecutor
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- react-native-safe-area-context (5.4.1):
- react-native-cameraroll (7.10.1):
- DoubleConversion
- glog
- hermes-engine
Expand All @@ -1411,8 +1411,6 @@ PODS:
- React-hermes
- React-ImageManager
- React-jsi
- react-native-safe-area-context/common (= 5.4.1)
- react-native-safe-area-context/fabric (= 5.4.1)
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
Expand All @@ -1422,7 +1420,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/common (5.4.1):
- react-native-safe-area-context (5.5.2):
- DoubleConversion
- glog
- hermes-engine
Expand All @@ -1437,6 +1435,8 @@ PODS:
- React-hermes
- React-ImageManager
- React-jsi
- react-native-safe-area-context/common (= 5.5.2)
- react-native-safe-area-context/fabric (= 5.5.2)
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
Expand All @@ -1446,7 +1446,31 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/fabric (5.4.1):
- react-native-safe-area-context/common (5.5.2):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-safe-area-context/fabric (5.5.2):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1797,7 +1821,7 @@ PODS:
- React-Core
- SDWebImage (~> 5.11.1)
- SDWebImageWebPCoder (~> 0.8.4)
- RNScreens (4.11.1):
- RNScreens (4.12.0):
- DoubleConversion
- glog
- hermes-engine
Expand All @@ -1821,9 +1845,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNScreens/common (= 4.11.1)
- RNScreens/common (= 4.12.0)
- Yoga
- RNScreens/common (4.11.1):
- RNScreens/common (4.12.0):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1866,7 +1890,7 @@ DEPENDENCIES:
- glog (from `../../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- NitroImage (from `../node_modules/react-native-nitro-image`)
- NitroModules (from `../../node_modules/react-native-nitro-modules`)
- NitroModules (from `../node_modules/react-native-nitro-modules`)
- RCT-Folly (from `../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCT-Folly/Fabric (from `../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTDeprecation (from `../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
Expand Down Expand Up @@ -1900,6 +1924,7 @@ DEPENDENCIES:
- React-logger (from `../../node_modules/react-native/ReactCommon/logger`)
- React-Mapbuffer (from `../../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- "react-native-cameraroll (from `../../node_modules/@react-native-camera-roll/camera-roll`)"
- react-native-safe-area-context (from `../../node_modules/react-native-safe-area-context`)
- React-NativeModulesApple (from `../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../../node_modules/react-native/ReactCommon/oscompat`)
Expand Down Expand Up @@ -1963,7 +1988,7 @@ EXTERNAL SOURCES:
NitroImage:
:path: "../node_modules/react-native-nitro-image"
NitroModules:
:path: "../../node_modules/react-native-nitro-modules"
:path: "../node_modules/react-native-nitro-modules"
RCT-Folly:
:podspec: "../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
RCTDeprecation:
Expand Down Expand Up @@ -2026,6 +2051,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native/ReactCommon"
React-microtasksnativemodule:
:path: "../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-cameraroll:
:path: "../../node_modules/@react-native-camera-roll/camera-roll"
react-native-safe-area-context:
:path: "../../node_modules/react-native-safe-area-context"
React-NativeModulesApple:
Expand Down Expand Up @@ -2107,7 +2134,7 @@ SPEC CHECKSUMS:
hermes-engine: 94ed01537bdeccaab1adbf94b040d115d6fa1a7f
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
NitroImage: 7f0a8fda3268c7169c9f4f23437d5841cbf4c96a
NitroModules: f36b94e48ff1705fc6b84bc1953f40e2da4196c2
NitroModules: 763fe03c46a734a615e648ff2c77158cd26d8f89
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
RCTDeprecation: c3e3f5b4ea83e7ff3bc86ce09e2a54b7affd687d
RCTRequired: ee438439880dffc9425930d1dd1a3c883ee6879c
Expand Down Expand Up @@ -2139,7 +2166,8 @@ SPEC CHECKSUMS:
React-logger: 514fac028fee60c84591f951c7c04ba1c5023334
React-Mapbuffer: fae8da2c01aeb7f26ad739731b6dba61fd02fd97
React-microtasksnativemodule: 20454ffccff553f0ee73fd20873aa8555a5867fb
react-native-safe-area-context: 5594ec631ede9c311c5c0efa244228eff845ce88
react-native-cameraroll: 41084e42ab4ec08940452737aca3fd5e0edc63fe
react-native-safe-area-context: 7e926a200d4bc9c56562275743705c6b56176455
React-NativeModulesApple: 65b2735133d6ce8a3cb5f23215ef85e427b0139c
React-oscompat: f26aa2a4adc84c34212ab12c07988fe19e9cf16a
React-perflogger: e15a0d43d1928e1c82f4f0b7fc05f7e9bccfede8
Expand Down Expand Up @@ -2172,7 +2200,7 @@ SPEC CHECKSUMS:
ReactCodegen: 16c2bfcebf870208d7e29ff0c065f4c0fa03034d
ReactCommon: e243aa261effc83c10208f0794bade55ca9ae5b6
RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87
RNScreens: 482e9707f9826230810c92e765751af53826d509
RNScreens: d8f03344886bd566bba24b9e02dbd28979631d3e
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"pods": "bundle install && cd ios && bundle exec pod install"
},
"dependencies": {
"@react-native-camera-roll/camera-roll": "^7.10.1",
"@react-navigation/bottom-tabs": "^7.3.14",
"@react-navigation/native": "^7.1.10",
"react": "19.0.0",
Expand Down
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createStaticNavigation } from "@react-navigation/native";
import { EmptyTab } from "./EmptyTab";
import { FastImageTab } from "./FastImageTab";
import { NitroAssetImageTab } from "./NitroAssetImageTab";
import { NitroImageTab } from "./NitroImageTab";

const Tabs = createBottomTabNavigator({
Expand All @@ -17,6 +18,7 @@ const Tabs = createBottomTabNavigator({
Empty: EmptyTab,
FastImage: FastImageTab,
NitroImage: NitroImageTab,
NitroMediaLibraryImage: NitroAssetImageTab,
},
});
const Navigation = createStaticNavigation(Tabs);
Expand Down
65 changes: 65 additions & 0 deletions example/src/NitroAssetImageTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
import React, { useEffect, useState } from "react";
import { FlatList, StyleSheet, Text, View } from "react-native";
import {
type Image,
loadImageFromAssetAsync,
NitroImage,
} from "react-native-nitro-image";

function useAssetImage(url: string): Image | undefined {
const [image, setImage] = useState<Image | undefined>(undefined);

useEffect(() => {
const load = async () => {
try {
const i = await loadImageFromAssetAsync(
url.replace(/^ph:\/\//, ""),
);
setImage(i);
} catch (error) {
console.error(`Failed to load image from "${url}"!`, error);
setImage(undefined);
}
};
load();
}, [url]);

return image;
}
function AsyncImageImpl({ url }: { url: string }): React.ReactNode {
const image = useAssetImage(url);
return <NitroImage style={styles.image} image={image} />;
}
const AsyncImage = React.memo(AsyncImageImpl);

export function NitroAssetImageTab() {
const [imageURLs, setImageURLs] = useState<string[]>([]);

useEffect(() => {
CameraRoll.getPhotos({ first: 100, assetType: "Photos" }).then(
(res) => {
setImageURLs(res.edges.map((edge) => edge.node.image.uri));
},
);
}, []);

return (
<View>
<Text>NitroMediaLibraryImage Tab</Text>
<FlatList
numColumns={4}
windowSize={3}
data={imageURLs}
renderItem={({ item: url }) => <AsyncImage url={url} />}
/>
</View>
);
}

const styles = StyleSheet.create({
image: {
width: "25%",
aspectRatio: 1,
},
});
39 changes: 38 additions & 1 deletion ios/HybridImageFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation
import NitroModules
import SDWebImage
import Photos

class HybridImageFactory: HybridImageFactorySpec {
private let queue = DispatchQueue(label: "image-loader",
Expand All @@ -21,14 +22,50 @@ class HybridImageFactory: HybridImageFactorySpec {
guard let url = URL(string: urlString) else {
throw RuntimeError.error(withMessage: "URL string \"\(urlString)\" is not a valid URL!")
}

return Promise.async {
let webImageOptions = options?.toSDWebImageOptions() ?? []
let uiImage = try await SDWebImageManager.shared.loadImage(with: url, options: webImageOptions)
return HybridImage(uiImage: uiImage)
}
}

/**
* Load Image from Photo Library Asset ID
*/
func loadFromAssetAsync(assetId: String, options: AssetImageLoadOptions?) throws -> Promise<any HybridImageSpec> {
return Promise.async {
// Move PHAsset fetching inside the async block
let asset = try await PHAsset.fetchAsset(withLocalIdentifier: assetId)

let requestOptions = PHImageRequestOptions()
requestOptions.version = .current
requestOptions.deliveryMode = .highQualityFormat
requestOptions.isNetworkAccessAllowed = true

if let size = options?.size {
let contentMode = PHImageContentMode(aspectFit: options?.aspectFit)
let uiImage = try await PHImageManager.default().requestImage(
for: asset,
targetSize: CGSize(width: size.width, height: size.height),
contentMode: contentMode,
options: requestOptions
)
return HybridImage(uiImage: uiImage)
} else {
let (imageData, orientation) = try await PHImageManager.default().requestImageDataAndOrientation(
for: asset,
options: requestOptions
)
guard let cgImage = UIImage(data: imageData)?.cgImage else {
throw RuntimeError.error(withMessage: "Failed to create CGImage from data")
}
let uiImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation(cgImageOrientation: orientation))
return HybridImage(uiImage: uiImage)
}
}
}

/**
* Load Image from file path
*/
Expand Down
Loading