first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

2
node_modules/expo-image/.eslintrc.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
// @generated by expo-module-scripts
module.exports = require('expo-module-scripts/eslintrc.base.js');

825
node_modules/expo-image/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,825 @@
# Changelog
## Unpublished
### 🛠 Breaking changes
### 🎉 New features
### 🐛 Bug fixes
### 💡 Others
## 55.0.6 — 2026-03-05
### 🐛 Bug fixes
- Added `tintColor` option to `ImageLoadOptions`. This resolves [#42007](https://github.com/expo/expo/issues/42007). ([#42821](https://github.com/expo/expo/pull/42821)) by [@HubertBer](https://github.com/HubertBer).
## 55.0.5 — 2026-02-25
### 🐛 Bug fixes
- [iOS] Fixed compilation errors in Xcode 26.4 Beta 1 ([#43346](https://github.com/expo/expo/pull/43346) by [@tsapeta](https://github.com/tsapeta))
## 55.0.4 — 2026-02-20
### 🐛 Bug fixes
- [Android] Uses shared cookie jar for image requests. ([#43257](https://github.com/expo/expo/pull/43257) by [@alanjhughes](https://github.com/alanjhughes))
## 55.0.3 — 2026-01-27
_This version does not introduce any user-facing changes._
## 55.0.2 — 2026-01-26
### 🐛 Bug fixes
- [iOS] Fixed `useImage` crashing on SVGs when the max dimensions are not set. ([#42496](https://github.com/expo/expo/pull/42496) by [@tsapeta](https://github.com/tsapeta))
## 55.0.1 — 2026-01-22
_This version does not introduce any user-facing changes._
## 55.0.0 — 2026-01-21
### 🎉 New features
- [iOS] Add `color` and `fontSize` style props for SF Symbols to set tint color and size. ([#42320](https://github.com/expo/expo/pull/42320) by [@EvanBacon](https://github.com/EvanBacon))
- [iOS] Add support for SF Symbols `source="sf:star"`. ([#41907](https://github.com/expo/expo/pull/41907) by [@EvanBacon](https://github.com/EvanBacon))
- [Android] Upgrades Glide to `5.0.5`. ([#39713](https://github.com/expo/expo/pull/39713) by [@lukmccall](https://github.com/lukmccall))
- [iOS] Added support for HDR images ([#40242](https://github.com/expo/expo/pull/40242) by [@tsapeta](https://github.com/tsapeta))
- [iOS] Adopted Swift 6 ([#40369](https://github.com/expo/expo/pull/40369) by [@tsapeta](https://github.com/tsapeta))
- [iOS] Provide plugin to disable `libdav1d`. ([#40691](https://github.com/expo/expo/pull/40691) by [@alanjhughes](https://github.com/alanjhughes))
- [iOS] feat: add `configureCache` option ([#40647](https://github.com/expo/expo/pull/40647) by [@kosmydel](https://github.com/kosmydel))
- [Web] Add `loading` prop for lazy loading images. ([#41442](https://github.com/expo/expo/pull/41442) by [@mozzius](https://github.com/mozzius))
- [iOS] Added support for PSD images. ([#42077](https://github.com/expo/expo/pull/42077) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- [android] Fix loading delayed placeholder ([#40956](https://github.com/expo/expo/pull/40956) by [@kosmydel](https://github.com/kosmydel))
- Fix tvOS SymbolEffectOptions availability check ([#42393](https://github.com/expo/expo/pull/42393) by [@gabrieldonadel](https://github.com/gabrieldonadel))
### 💡 Others
- Remove tests related files from the published package content. ([#39551](https://github.com/expo/expo/pull/39551) by [@Simek](https://github.com/Simek))
## 3.0.11 - 2025-12-05
_This version does not introduce any user-facing changes._
## 3.0.10 - 2025-10-20
_This version does not introduce any user-facing changes._
## 3.0.9 - 2025-10-07
### 🐛 Bug fixes
- [Android] Fixed `You can't start or clear loads in RequestListener or Target callbacks`. ([#40212](https://github.com/expo/expo/pull/40212) by [@lukmccall](https://github.com/lukmccall))
## 3.0.8 — 2025-09-11
_This version does not introduce any user-facing changes._
## 3.0.7 — 2025-09-03
### 🐛 Bug fixes
- [iOS] Fix images not displaying in Material Top Tabs navigator. ([#39323](https://github.com/expo/expo/pull/39323) by [@lukmccall](https://github.com/lukmccall))
## 3.0.6 — 2025-09-02
_This version does not introduce any user-facing changes._
## 3.0.5 — 2025-08-31
_This version does not introduce any user-facing changes._
## 3.0.4 — 2025-08-26
### 🐛 Bug fixes
- [Android] Fixed `The method 'getResourceDrawableUri' was expected to be of type static` exception. ([#39143](https://github.com/expo/expo/pull/39143) by [@lukmccall](https://github.com/lukmccall))
## 3.0.3 — 2025-08-25
_This version does not introduce any user-facing changes._
## 3.0.2 — 2025-08-16
_This version does not introduce any user-facing changes._
## 3.0.1 — 2025-08-15
_This version does not introduce any user-facing changes._
## 3.0.0 — 2025-08-13
### 🎉 New features
- Add `generateThumbhashAsync` ([#38090](https://github.com/expo/expo/pull/38090) by [@Wenszel](https://github.com/Wenszel))
- Add support for `ImageRef` source in `generateBlurhashAsync` ([#37901](https://github.com/expo/expo/pull/37901) by [@Wenszel](https://github.com/Wenszel))
- [Android] Add generateBlurhashAsync ([#37817](https://github.com/expo/expo/pull/37817) by [@Wenszel](https://github.com/Wenszel))
### 🐛 Bug fixes
- [Android] Fix animation resuming by casting image to GifDrawable. ([#37363](https://github.com/expo/expo/pull/37363) by [@Wenszel](https://github.com/Wenszel))
- [Web] Fix `alt` as an alias for `accessibilityLabel` ([#37682](https://github.com/expo/expo/pull/37682) by [@huextrat](https://github.com/huextrat))
- [iOS] Fix caching resized images from Photo Library. ([#38105](https://github.com/expo/expo/pull/38105) by [@jakex7](https://github.com/jakex7))
- [iOS] Fix `generatePlaceholder` method syntax error by removing unwanted trailing comma. ([#38318](https://github.com/expo/expo/pull/38318) by [@bortolilucas](https://github.com/bortolilucas))
## 2.4.0 - 2025-07-17
### 🎉 New features
- [iOS] Add a new prop - `enforceEarlyResizing` to reduce the memory usage of the image view. ([#37909](https://github.com/expo/expo/pull/37909) by [@lukmccall](https://github.com/lukmccall))
### 🐛 Bug fixes
- [iOS] Speed up displaying local assets. ([#37795](https://github.com/expo/expo/pull/37795) by [@aleqsio](https://github.com/aleqsio))
- [iOS] Fix some operation were incorrectly cancelled. ([#37987](https://github.com/expo/expo/pull/37987) by [@lukmccall](https://github.com/lukmccall))
## 2.3.2 - 2025-07-01
### 🐛 Bug fixes
- [iOS] Use specified cache type when no transformation is applied ([#37777](https://github.com/expo/expo/pull/37777) by [@jakex7](https://github.com/jakex7))
## 2.3.1 - 2025-07-01
### 🐛 Bug fixes
- [iOS] Fixed contentPosition is not correct after switching theme. ([#37374](https://github.com/expo/expo/pull/37374) by [@kudo](https://github.com/kudo))
### 📚 3rd party library updates
- [Android] Bumped GIF Glide plugin to 3.0.5 for Android 16KB page size support. ([#37454](https://github.com/expo/expo/pull/37454) by [@kudo](https://github.com/kudo))
## 2.3.0 - 2025-06-11
### 🛠 Breaking changes
- [iOS] `useAppleWebpCodec` has been moved from the source object to the component's prop to make it usable with the local assets. ([#37300](https://github.com/expo/expo/pull/37300) by [@tsapeta](https://github.com/tsapeta))
### 🐛 Bug fixes
- [iOS] Fix blurry images when using `tintColor` by scaling `imageThumbnailPixelSize` with screen density. ([#37235](https://github.com/expo/expo/pull/37235) by [@hirbod](https://github.com/hirbod))
## 2.2.1 - 2025-06-10
_This version does not introduce any user-facing changes._
## 2.2.0 - 2025-06-04
### 🎉 New features
- Add imperative api to lock/unlock/reload resource. ([#36912](https://github.com/expo/expo/pull/36912) by [@jakex7](https://github.com/jakex7))
### 🐛 Bug fixes
- Fix React Server Components support. ([#36801](https://github.com/expo/expo/pull/36801) by [@EvanBacon](https://github.com/EvanBacon))
- [iOS] Fix PhotoLibrary assets being scaled twice. ([#36776](https://github.com/expo/expo/pull/36776) by [@alanjhughes](https://github.com/alanjhughes))
- [iOS] Don't add transformers when unnecessary. ([#36884](https://github.com/expo/expo/pull/36884) by [@jakex7](https://github.com/jakex7))
- [Web] Fix `tintColor` in React 19. ([#37133](https://github.com/expo/expo/pull/37133) by [@bradleyayers](https://github.com/bradleyayers))
## 2.1.7 — 2025-05-06
_This version does not introduce any user-facing changes._
## 2.1.6 — 2025-04-30
_This version does not introduce any user-facing changes._
## 2.1.5 — 2025-04-25
### 🐛 Bug fixes
- Fixed `CUICatalog: Invalid asset name supplied: ''` error on iOS when the path is empty. ([#36294](https://github.com/expo/expo/pull/36294) by [@Innei](https://github.com/Innei))
## 2.1.4 — 2025-04-14
### 🐛 Bug fixes
- Fixed SVG image tinting on iOS. ([#35927](https://github.com/expo/expo/pull/35927) by [@kudo](https://github.com/kudo))
- [Android] Fixed OutOfMemoryError crash when displaying some gif images ([#36097](https://github.com/expo/expo/pull/36097) by [@rahimrahman](https://github.com/rahimrahman))
## 2.1.3 — 2025-04-11
_This version does not introduce any user-facing changes._
## 2.1.2 — 2025-04-09
_This version does not introduce any user-facing changes._
## 2.1.1 — 2025-04-08
### 🐛 Bug fixes
- Fixed SVG image tinting on iOS. ([#35927](https://github.com/expo/expo/pull/35927) by [@kudo](https://github.com/kudo))
## 2.1.0 — 2025-04-04
### 🛠 Breaking changes
- upgrade RN to 0.78 ([#35050](https://github.com/expo/expo/pull/35050) by [@vonovak](https://github.com/vonovak))
### 🎉 New features
- type-compatibility with react-native 0.77 ([#34027](https://github.com/expo/expo/pull/34027) by [@vonovak](https://github.com/vonovak))
- Added `ImageSource.useAppleWebpCodec` on iOS to allow using libwebp codec for better animation performance on some images. ([#35802](https://github.com/expo/expo/pull/35802) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- Rename `fetchpriority` prop to `fetchPriority` to silence web error. ([#35411](https://github.com/expo/expo/pull/35411) by [@EvanBacon](https://github.com/EvanBacon))
- Fixed `tintColor="currentColor"` conflicts on web. ([#34604](https://github.com/expo/expo/pull/34604) by [@bradleyayers](https://github.com/bradleyayers))
### 💡 Others
- Update `ImageProps` type so `children` are omitted. ([#33210](https://github.com/expo/expo/pull/33210) by [@ashaller2017](https://github.com/ashaller2017))
- [Android] Started using expo modules gradle plugin. ([#34176](https://github.com/expo/expo/pull/34176) by [@lukmccall](https://github.com/lukmccall))
- [iOS] Fix warnings which will become errors in Swift 6. ([#35428](https://github.com/expo/expo/pull/35428) by [@behenate](https://github.com/behenate))
### 📚 3rd party library updates
- Bumped `SDWebImage` to 5.21.0. ([#35795](https://github.com/expo/expo/pull/35795) by [@kudo](https://github.com/kudo))
## 2.0.7 - 2025-03-26
### 🐛 Bug fixes
- [iOS] Fixed image be cropped with `contentPosition` on New Architecture mode. ([#35630](https://github.com/expo/expo/pull/35630) by [@kudo](https://github.com/kudo))
## 2.0.6 - 2025-02-19
### 🐛 Bug fixes
- [Android] Fixes a regression in `loadAsync` from [#34767](https://github.com/expo/expo/pull/34767). ([#34965](https://github.com/expo/expo/pull/34965) by [@alanjhughes](https://github.com/alanjhughes)) ([#34767](https://github.com/expo/expo/pull/34767), [#34965](https://github.com/expo/expo/pull/34965) by [@alanjhughes](https://github.com/alanjhughes))
## 2.0.5 - 2025-02-10
### 🐛 Bug fixes
- [Android] Add headers to `loadAsync` requests. ([#34767](https://github.com/expo/expo/pull/34767) by [@alanjhughes](https://github.com/alanjhughes))
## 2.0.4 - 2025-01-10
_This version does not introduce any user-facing changes._
## 2.0.3 - 2024-11-29
### 🐛 Bug fixes
- [Android] Fixed `useImage` causing a native crash when uri is unresolvable. ([#33268](https://github.com/expo/expo/pull/33268) by [@lukmccall](https://github.com/lukmccall))
- [Android] Fixes adding a blurhash placeholder to `Image` or `ImageBackground` causing blurry version of the image. ([#33272](https://github.com/expo/expo/pull/33272) by [@lukmccall](https://github.com/lukmccall))
## 2.0.2 — 2024-11-22
### 💡 Others
- Add a warning when `children` are passed to `Image`. ([#33139](https://github.com/expo/expo/pull/33139) by [@alanjhughes](https://github.com/alanjhughes))
## 2.0.1 — 2024-11-19
### 🐛 Bug fixes
- [Android] Fixes Gif animations never stopping even when loop count is set to 1. ([#32944](https://github.com/expo/expo/pull/32944) by [@lukmccall](https://github.com/lukmccall))
- [Android] Fixed `borderColor` is not applied. ([#33026](https://github.com/expo/expo/pull/33026) by [@lukmccall](https://github.com/lukmccall))
- [Android] Fixed `border` related props weren't applied correctly. ([#33078](https://github.com/expo/expo/pull/33078) by [@lukmccall](https://github.com/lukmccall))
### 💡 Others
- [Android] migrate ImageView to CSSBackgroundDrawable ([#33024](https://github.com/expo/expo/pull/33024) by [@vonovak](https://github.com/vonovak))
## 2.0.0 — 2024-11-11
_This version does not introduce any user-facing changes._
## 2.0.0-preview.1 — 2024-10-31
### 🐛 Bug fixes
- Fixed displaying a placeholder when `useImage` hook is used. ([#32430](https://github.com/expo/expo/pull/32430) by [@tsapeta](https://github.com/tsapeta))
## 2.0.0-preview.0 — 2024-10-22
### 🛠 Breaking changes
- Bumped iOS and tvOS deployment target to 15.1. ([#30840](https://github.com/expo/expo/pull/30840), [#30871](https://github.com/expo/expo/pull/30871) by [@tsapeta](https://github.com/tsapeta))
### 🎉 New features
- Added `Image.loadAsync` API. ([#25079](https://github.com/expo/expo/pull/25079) by [@tsapeta](https://github.com/tsapeta), [#26824](https://github.com/expo/expo/pull/26824) by [@aleqsio](https://github.com/aleqsio), [#31575](https://github.com/expo/expo/pull/31575) by [@tsapeta](https://github.com/tsapeta))
- Add basic React Server Component support. ([#29869](https://github.com/expo/expo/pull/29869) by [@EvanBacon](https://github.com/EvanBacon))
- [iOS] Added support for rendering shared image refs. ([#30661](https://github.com/expo/expo/pull/30661) by [@tsapeta](https://github.com/tsapeta))
- [Android] Added support for rendering shared image refs. ([#31098](https://github.com/expo/expo/pull/31098) by [@lukmccall](https://github.com/lukmccall))
- [Android] Added support for rendering shared refs of `Bitmap's`. ([#31440](https://github.com/expo/expo/pull/31440) by [@lukmccall](https://github.com/lukmccall))
- Added `useImage` hook. ([#31171](https://github.com/expo/expo/pull/31171) by [@tsapeta](https://github.com/tsapeta))
- Added downscaling options to `useImage` hook. ([#32113](https://github.com/expo/expo/pull/32113) by [@lukmccall](https://github.com/lukmccall))
### 🐛 Bug fixes
- Fix props not being passed to parent container. ([#29416](https://github.com/expo/expo/pull/29416) by [@aleqsio](https://github.com/aleqsio))
- Add missing `react` and `react-native` peer dependencies for isolated modules. ([#30469](https://github.com/expo/expo/pull/30469) by [@byCedric](https://github.com/byCedric))
- Add missing `react-native-web` optional peer dependency for isolated modules. ([#30689](https://github.com/expo/expo/pull/30689) by [@byCedric](https://github.com/byCedric))
- [Web] Fix type incompatibility between style prop and `@types/react-native-web` ([#31150](https://github.com/expo/expo/pull/31150) by [@adamhari](https://github.com/adamhari))
- [iOS] Fixed `isAnimated` property always returning `true`. ([#31834](https://github.com/expo/expo/pull/31834) by [@tsapeta](https://github.com/tsapeta))
- [iOS] Fixed `stopAnimating` broken on downscaled animated images. ([#32053](https://github.com/expo/expo/pull/32053) by [@kudo](https://github.com/kudo))
### 💡 Others
- Use the `src` folder as the Metro target. ([#30665](https://github.com/expo/expo/pull/30665) by [@tsapeta](https://github.com/tsapeta))
- Provide image's memory footprint for better garbage collection. ([#31168](https://github.com/expo/expo/pull/31168) by [@tsapeta](https://github.com/tsapeta) & [#31784](https://github.com/expo/expo/pull/31784) by [@lukmccall](https://github.com/lukmccall))
### ⚠️ Notices
- Added support for React Native 0.75.x. ([#30034](https://github.com/expo/expo/pull/30034) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Added support for React Native 0.76.x. ([#31552](https://github.com/expo/expo/pull/31552) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## 1.13.0 - 2024-09-23
### 🎉 New features
- Added `onDisplay` event. ([#31581](https://github.com/expo/expo/pull/31581) by [@tsapeta](https://github.com/tsapeta))
## 1.12.15 - 2024-08-24
_This version does not introduce any user-facing changes._
## 1.12.13 - 2024-07-16
### 🐛 Bug fixes
- [Web] Fix blurhash not working and causing fetches to `blurhash:/` uri. ([#27587](https://github.com/expo/expo/pull/27587) by [@aleqsio](https://github.com/aleqsio))
- [iOS] Fixed `blurRadius` not working. Also made the effect render more consistently across platforms. ([#29678](https://github.com/expo/expo/pull/29678) by [@vonovak](https://github.com/vonovak))
- Fixed `tintColor` not working on Safari browsers. ([#29169](https://github.com/expo/expo/pull/29169) by [@bradleyayers](https://github.com/bradleyayers))
- Fixed reanimated support on web. ([#29197](https://github.com/expo/expo/pull/29197) by [@nishan](https://github.com/intergalacticspacehighway)) ([#29197](https://github.com/expo/expo/pull/29197) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
## 1.12.12 - 2024-06-13
### 💡 Others
- Removed @react-native/assets-registry dependency. ([#29541](https://github.com/expo/expo/pull/29541) by [@kudo](https://github.com/kudo))
## 1.12.11 - 2024-06-06
_This version does not introduce any user-facing changes._
## 1.12.10 - 2024-06-05
### 💡 Others
- Pin @react-native subpackage versions to 0.74.83. ([#29441](https://github.com/expo/expo/pull/29441) by [@kudo](https://github.com/kudo))
## 1.12.9 — 2024-05-09
### 💡 Others
- Added setup for native unit tests. ([#28678](https://github.com/expo/expo/pull/28678) by [@tsapeta](https://github.com/tsapeta))
## 1.12.8 — 2024-05-06
_This version does not introduce any user-facing changes._
## 1.12.7 — 2024-05-04
### 🎉 New features
- Added support for displaying animated AVIF images on Android. ([#28609](https://github.com/expo/expo/pull/28609) by [@fobos531](https://github.com/fobos531))
### 🐛 Bug fixes
- Fix `avif` images not rendering. ([#28608](https://github.com/expo/expo/pull/28608) by [@alanjhughes](https://github.com/alanjhughes))
## 1.12.6 — 2024-05-02
_This version does not introduce any user-facing changes._
## 1.12.5 — 2024-05-01
_This version does not introduce any user-facing changes._
## 1.12.4 — 2024-04-24
### 🐛 Bug fixes
- Fixed an issue where certain images would not animate at certain sizes on `iOS`. ([#28335](https://github.com/expo/expo/pull/28335) by [@fobos531](https://github.com/fobos531))
- Fixed SVG assets without viewbox attribute not being rendered on Android. ([#28369](https://github.com/expo/expo/pull/28369) by [@lukmccall](https://github.com/lukmccall))
## 1.12.3 — 2024-04-23
_This version does not introduce any user-facing changes._
## 1.12.2 — 2024-04-22
### 🐛 Bug fixes
- Fixed an issue where certain images would not animate at certain sizes on `iOS`. ([#28335](https://github.com/expo/expo/pull/28335) by [@fobos531](https://github.com/fobos531), [#28371](https://github.com/expo/expo/pull/28371) by [@tsapeta](https://github.com/tsapeta))
## 1.12.1 — 2024-04-19
_This version does not introduce any user-facing changes._
## 1.12.0 — 2024-04-18
### 🎉 New features
- On `iOS`, support loading assets in the native project. This already worked on android. ([#27251](https://github.com/expo/expo/pull/27251) by [@alanjhughes](https://github.com/alanjhughes))
- Support prefetching images with HTTP headers. ([#28133](https://github.com/expo/expo/pull/28133) by [@toy0605](https://github.com/toy0605))
### 🐛 Bug fixes
- [iOS] Fixed an issue where data URIs caused crashes on iOS16+. ([#28320](https://github.com/expo/expo/pull/28320) by [@aleqsio](https://github.com/aleqsio))
- Refactor web implementations of `useSourceSelection` to avoid unnecessary rerenders. ([#27569](https://github.com/expo/expo/pull/27569) by [@aleqsio](https://github.com/aleqsio))
- Fixed an issue where `isBlurhashString` always returned `true` .([#27251](https://github.com/expo/expo/pull/27251) by [@alanjhughes](https://github.com/alanjhughes))
- Fix #available check to account for tvOS. ([#27272](https://github.com/expo/expo/pull/27272) by [@alanjhughes](https://github.com/alanjhughes))
- Fixed jest may cause `RangeError: Invalid string length` error when generating a snapshot. ([#27354](https://github.com/expo/expo/pull/27354) by [@lukmccall](https://github.com/lukmccall))
- Fixed placeholders weren't correctly scaled down on the Android. ([#28255](https://github.com/expo/expo/pull/28255) by [@lukmccall](https://github.com/lukmccall))
### 💡 Others
- [iOS] Bump SDWebImage to `5.19.1` to include the 3rd-party privacy manifest. ([#27874](https://github.com/expo/expo/pull/27874) by [@aparedes](https://github.com/aparedes), []() by [@tsapeta](https://github.com/tsapeta))
- Use `typeof window` checks for removing server code. ([#27514](https://github.com/expo/expo/pull/27514) by [@EvanBacon](https://github.com/EvanBacon))
- Removed deprecated backward compatible Gradle settings. ([#28083](https://github.com/expo/expo/pull/28083) by [@kudo](https://github.com/kudo))
- [iOS] Bump SDWebImageWebPCoder to `0.14.6`. ([#28273](https://github.com/expo/expo/pull/28273) by [@alanjhughes](https://github.com/alanjhughes))
- Automatically clean blurhash cache when the app's memory is running low. ([#28276](https://github.com/expo/expo/pull/28276) by [@lukmccall](https://github.com/lukmccall))
## 1.11.0 — 2024-02-05
### 🎉 New features
- [Android] Adds new prop `decodeFormat` to specify the format that should be used during the decoding process. ([#26442](https://github.com/expo/expo/pull/26442) by [@lukmccall](https://github.com/lukmccall))
- [iOS] Added `generateBlurhashAsync` function. ([#26430](https://github.com/expo/expo/pull/26430) by [@aleqsio](https://github.com/aleqsio))
- [Android] Adds automatic downsampling when the asset exceeds the hardware bitmap size limit. ([#26792](https://github.com/expo/expo/pull/26792) by [@lukmccall](https://github.com/lukmccall))
### 🐛 Bug fixes
- Fixed ResizeObserver attaching on every image transition. ([#25819](https://github.com/expo/expo/pull/25819) by [@aleqsio](https://github.com/aleqsio))
- [Android] Fixed the tine color was applied to the mask element. ([#26323](https://github.com/expo/expo/pull/26323) by [@lukmccall](https://github.com/lukmccall))
- [Web] Fixed `nativeViewRef` invalid prop warning. ([#25922](https://github.com/expo/expo/pull/25922) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
## 1.10.4 - 2024-01-18
_This version does not introduce any user-facing changes._
## 1.10.3 - 2024-01-12
_This version does not introduce any user-facing changes._
## 1.10.2 - 2024-01-10
### 🐛 Bug fixes
- [Android] Fixed the issue with the application of tint color when an element does not have a style assigned to it. ([#26251](https://github.com/expo/expo/pull/26251) by [@lukmccall](https://github.com/lukmccall))
- [Android] Fixed the tint color wasn't applied to the root element. ([#26339](https://github.com/expo/expo/pull/26339) by [@lukmccall](https://github.com/lukmccall))
- [Android] Fixed that the tint color on SVGs can't be changed dynamically. ([#26350](https://github.com/expo/expo/pull/26350) by [@lukmccall](https://github.com/lukmccall))
### 💡 Others
- Replace deprecated `com.facebook.react:react-native:+` Android dependency with `com.facebook.react:react-android`. ([#26237](https://github.com/expo/expo/pull/26237) by [@kudo](https://github.com/kudo))
## 1.10.1 - 2023-12-19
_This version does not introduce any user-facing changes._
## 1.10.0 — 2023-12-12
### 🎉 New features
- Added support for React Native 0.73.0. ([#24971](https://github.com/expo/expo/pull/24971), [#25453](https://github.com/expo/expo/pull/25453) by [@gabrieldonadel](https://github.com/gabrieldonadel))
## 1.9.0 — 2023-11-14
### 🛠 Breaking changes
- Bumped iOS deployment target to 13.4. ([#25063](https://github.com/expo/expo/pull/25063) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- On `Android` bump `compileSdkVersion` and `targetSdkVersion` to `34`. ([#24708](https://github.com/expo/expo/pull/24708) by [@alanjhughes](https://github.com/alanjhughes))
### 🎉 New features
- [Android] The stability of the memory cache key generation has been improved. ([#25372](https://github.com/expo/expo/pull/25372) by [@lukmccall](https://github.com/lukmccall))
### 💡 Others
- Collapse re-export of `react-native/Libraries/Image/AssetRegistry` to `@react-native/assets-registry/registry`. ([#25265](https://github.com/expo/expo/pull/25265) by [@EvanBacon](https://github.com/EvanBacon))
- [Android] Changed how the `tintColor` is applied to the SVG. ([#25377](https://github.com/expo/expo/pull/25377) by [@lukmccall](https://github.com/lukmccall))
## 1.8.0 — 2023-11-13
### 🎉 New features
- Return a promise in the `prefetch` method. ([#25196](https://github.com/expo/expo/pull/25196) by [@gkasdorf](https://github.com/gkasdorf))
- [Android] Added `autoplay` prop and `startAnimating()` and `stopAnimating()` functions to reflect changes made to iOS in [#25008](https://github.com/expo/expo/pull/25008). ([#25124](https://github.com/expo/expo/pull/25124) by [@gkasdorf](https://github.com/gkasdorf))
### 🐛 Bug fixes
- [Android] Fix `contentFit` not working for `SVG` images. ([#25187](https://github.com/expo/expo/pull/25187) by [@behenate](https://github.com/behenate))
- [iOS] Start loading the image before the view mounts to fix issues with the PagerView. ([#25343](https://github.com/expo/expo/pull/25343) by [@tsapeta](https://github.com/tsapeta))
- [Android] Fix `SVG` not scaling correctly in the release mode. ([#25326](https://github.com/expo/expo/pull/25326) by [@lukmccall](https://github.com/lukmccall))
- [Android] Fix incorrect `intrinsicSize` returned for SVGs. ([#25048](https://github.com/expo/expo/pull/25048) by [@behenate](https://github.com/behenate))
### 💡 Others
- [Android] Add tracing. ([#25251](https://github.com/expo/expo/pull/25251) by [@lukmccall](https://github.com/lukmccall))
## 1.7.0 — 2023-11-01
### 🎉 New features
- [iOS] Added support for `allowDownscaling` prop. ([#25012](https://github.com/expo/expo/pull/25012) by [@behenate](https://github.com/behenate))
- Added `getCachePathAsync()` to retrieve the path of the cached image file if it exists ([#24980](https://github.com/expo/expo/pull/24980) by [@gkasdorf](https://github.com/gkasdorf))
- [iOS] Added `autoplay` prop to control whether an animated image will automatically animate or not ([#25008](https://github.com/expo/expo/pull/25008) by [@gkasdorf](https://github.com/gkasdorf))
- [iOS] Added `startAnimating()` and `stopAnimating()` functions to start or stop an image's animation ([#25008](https://github.com/expo/expo/pull/25008) by [@gkasdorf](https://github.com/gkasdorf))
### 🐛 Bug fixes
- [Android] fix crash when loading local image files with no file extension ([#24201](https://github.com/expo/expo/pull/25032) by [@kadikraman](https://github.com/kadikraman))
- [iOS] Fix compilation on tvOS. ([#25010](https://github.com/expo/expo/pull/25010) by [@douglowder](https://github.com/douglowder))
- [iOS] Fixed issue where some animated images would cause the app to hang ([#25008](https://github.com/expo/expo/pull/25008) by [@gkasdorf](https://github.com/gkasdorf))
## 1.6.1 — 2023-11-01
### 🐛 Bug fixes
- [Android] fix crash when loading local image files with no file extension ([#24201](https://github.com/expo/expo/pull/25032) by [@kadikraman](https://github.com/kadikraman))
## 1.3.5 — 2023-11-01
### 🐛 Bug fixes
- [Android] fix crash when loading local image files with no file extension ([#24201](https://github.com/expo/expo/pull/25032) by [@kadikraman](https://github.com/kadikraman))
## 1.6.0 — 2023-10-17
### 🛠 Breaking changes
- Dropped support for Android SDK 21 and 22. ([#24201](https://github.com/expo/expo/pull/24201) by [@behenate](https://github.com/behenate))
### 🎉 New features
- Added support for the `headers` key in the `source` object on web. ([#24447](https://github.com/expo/expo/pull/24447) by [@aleqsio](https://github.com/aleqsio))
- Add support for setting `tintColor` on SVGs on Android (part 1) ([#24733](https://github.com/expo/expo/pull/24733) by [@alanjhughes](https://github.com/alanjhughes) and [@kadikraman](https://github.com/kadikraman))
- Add support for setting `tintColor` on SVGs on Android (part 2) ([#24888](https://github.com/expo/expo/pull/24888) by [@kadikraman](https://github.com/kadikraman))
### 🐛 Bug fixes
- Remove `GlideWebpDecoder` until they update their `libwebp` dependency. ([#24656](https://github.com/expo/expo/pull/24656) by [@alanjhughes](https://github.com/alanjhughes))
- [web] Fix content fit not being applied correctly when using hash placeholders. ([#24542](https://github.com/expo/expo/pull/24542) by [@aleqsio](https://github.com/aleqsio))
- [macCatalyst] Fix build with `ImageAnalyzer` on macCatalyst below 17.0. ([#24880](https://github.com/expo/expo/pull/24880) by [@kesha-antonov](https://github.com/kesha-antonov))
### 💡 Others
- Ship untranspiled JSX to support custom handling of `jsx` and `createElement`. ([#24889](https://github.com/expo/expo/pull/24889) by [@EvanBacon](https://github.com/EvanBacon))
- Make `placeholderContentFit` visible in the docs. ([#24801](https://github.com/expo/expo/pull/24801) by [@behenate](https://github.com/behenate))
## 1.0.2 — 2023-09-29
### 🐛 Bug fixes
- Remove `GlideWebpDecoder` until they update their `libwebp` dependency. ([#24656](https://github.com/expo/expo/pull/24656) by [@alanjhughes](https://github.com/alanjhughes))
## 1.3.4 — 2023-09-28
### 🐛 Bug fixes
- Remove `GlideWebpDecoder` until they update their `libwebp` dependency. ([#24656](https://github.com/expo/expo/pull/24656) by [@alanjhughes](https://github.com/alanjhughes))
## 1.5.2 — 2023-09-18
_This version does not introduce any user-facing changes._
## 1.3.3 — 2023-09-15
### 🐛 Bug fixes
- Fixed placeholders aren't always replaced by full-size images on Android. ([#23705](https://github.com/expo/expo/pull/23705) by [@lukmccall](https://github.com/lukmccall))
- Fix the image components rendering incorrect assets when the Proguard is enabled on Android. ([#23704](https://github.com/expo/expo/pull/23704) by [@lukmccall](https://github.com/lukmccall))
- Fixed gif and awebp memory leak on Android. ([#24259](https://github.com/expo/expo/pull/24259) by [@jingpeng](https://github.com/jingpeng))
- Suppress "Operation cancelled by user during sending the request" error when the load request is canceled (interrupted) by a new one. ([#24279](https://github.com/expo/expo/pull/24279) by [@tsapeta](https://github.com/tsapeta))
## 1.5.1 — 2023-09-11
### 🐛 Bug fixes
- Suppress "Operation cancelled by user during sending the request" error when the load request is canceled (interrupted) by a new one. ([#24279](https://github.com/expo/expo/pull/24279) by [@tsapeta](https://github.com/tsapeta))
- Fixed gif and awebp memory leak on Android. ([#24259](https://github.com/expo/expo/pull/24259) by [@jingpeng](https://github.com/jingpeng))
## 1.5.0 — 2023-09-04
### 🎉 New features
- Added support for React Native 0.73. ([#24018](https://github.com/expo/expo/pull/24018) by [@kudo](https://github.com/kudo))
### 💡 Others
- On iOS, bump SDWebImage versions. ([#23858](https://github.com/expo/expo/pull/23858) by [@alanjhughes](https://github.com/alanjhughes))
## 1.4.1 — 2023-08-02
_This version does not introduce any user-facing changes._
## 1.4.0 — 2023-07-28
### 🎉 New features
- [Web] Add support for `tintColor` prop on web. ([#23434](https://github.com/expo/expo/pull/23434) by [@aleqsio](https://github.com/aleqsio))
- [Web] Add support for static image responsiveness using `srcset` attributes. ([#22088](https://github.com/expo/expo/pull/22088) by [@aleqsio](https://github.com/aleqsio))
### 🐛 Bug fixes
- Fixed placeholders aren't always replaced by full-size images on Android. ([#23705](https://github.com/expo/expo/pull/23705) by [@lukmccall](https://github.com/lukmccall))
- Fixed the image components rendering incorrect assets when the Proguard is enabled on Android. ([#23704](https://github.com/expo/expo/pull/23704) by [@lukmccall](https://github.com/lukmccall))
### 💡 Others
- [Web] Refactored and split some functions for better readability. ([#23465](https://github.com/expo/expo/pull/23465) by [@aleqsio](https://github.com/aleqsio))
## 1.3.2 - 2023-07-12
### 🐛 Bug fixes
- [iOS] Fixed `tintColor` prop not working for SVGs. ([#23418](https://github.com/expo/expo/pull/23418) by [@tsapeta](https://github.com/tsapeta))
## 1.3.1 - 2023-06-29
### 🐛 Bug fixes
- Fixed an issue where recyclingKey would reset the image source on mount. ([#23187](https://github.com/expo/expo/pull/23187) by [@hirbod](https://github.com/hirbod))
## 1.3.0 — 2023-06-13
### 🎉 New features
- Add prefetch implementation on web. ([#22630](https://github.com/expo/expo/pull/22630) by [@aleqsio](https://github.com/aleqsio))
- Add `ImageBackground` component. ([#22347](https://github.com/expo/expo/pull/22347) by [@alanjhughes](https://github.com/alanjhughes))
- Added support for React Native 0.72. ([#22588](https://github.com/expo/expo/pull/22588) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- Fixed styles order breaking layouting on web. ([#22630](https://github.com/expo/expo/pull/22630) by [@aleqsio](https://github.com/aleqsio))
- Uses prop spreading on web to pass all unused props to the native image component ([#22340](https://github.com/expo/expo/pull/22340) by [@makkarMeenu](https://github.com/makkarMeenu))
- Fixed Android build warnings for Gradle version 8. ([#22537](https://github.com/expo/expo/pull/22537), [#22609](https://github.com/expo/expo/pull/22609) by [@kudo](https://github.com/kudo))
### 💡 Others
- Updated `SDWebImage` to `5.15.8`, `SDWebImageWebPCoder` to `0.11.0` and `SDWebImageSVGCoder` to `1.7.0`. ([#22576](https://github.com/expo/expo/pull/22576) by [@tsapeta](https://github.com/tsapeta))
## 1.2.3 — 2023-05-16
### 🐛 Bug fixes
- Upgrade SDWebImageAVIFCoder to fix compiling issue with libavif < 0.11.0. ([#22491](https://github.com/expo/expo/pull/22491) by [@matinzd](https://github.com/matinzd))
## 1.2.2 — 2023-04-27
### 🐛 Bug fixes
- Fix for the "limited" media library permission. ([#22261](https://github.com/expo/expo/pull/22261) by [@tsapeta](https://github.com/tsapeta))
## 1.2.1 — 2023-04-17
### 🐛 Bug fixes
- [Android] Fix `url` property returned by the `onLoad` event. ([#22161](https://github.com/expo/expo/pull/22161) by [@lukmccall](https://github.com/lukmccall))
- [Android] Fix images not loading after the app was foregrounded. ([#22159](https://github.com/expo/expo/pull/22159) by [@lukmccall](https://github.com/lukmccall))
- [Android] Fixed image was loaded event if the view dimensions were 0. ([#22157](https://github.com/expo/expo/pull/22157) by [@lukmccall](https://github.com/lukmccall))
- Fix generating the image from ThumbHash that starts with a slash character. ([#22160](https://github.com/expo/expo/pull/22160) by [@tsapeta](https://github.com/tsapeta))
## 1.2.0 — 2023-04-14
### 🎉 New features
- [Web] Add support for `require()` assets. ([#21798](https://github.com/expo/expo/pull/21798) by [@aleqsio](https://github.com/aleqsio))
- Add `alt` prop as an alias to `accessibilityLabel`. ([#21884](https://github.com/expo/expo/pull/21884) by [@EvanBacon](https://github.com/EvanBacon))
- [Web] Add `accessibilityLabel` support on web. ([#21884](https://github.com/expo/expo/pull/21884) by [@EvanBacon](https://github.com/EvanBacon))
- Added `ThumbHash` support for Android, iOS and Web. ([#21952](https://github.com/expo/expo/pull/21952) by [@behenate](https://github.com/behenate))
### 🐛 Bug fixes
- [Web] Improve transition behavior when switching back and forth two sources. ([#22099](https://github.com/expo/expo/pull/22099) by [@aleqsio](https://github.com/aleqsio))
- [Web] Prevent breaking in static rendering environments. ([#21883](https://github.com/expo/expo/pull/21883) by [@EvanBacon](https://github.com/EvanBacon))
- [Android] Fixed image disappearing before navigation animation is complete. ([#22066](https://github.com/expo/expo/pull/22066) by [@sallen450](https://github.com/sallen450))
- [Web] Fixed monorepo asset resolution in production for Metro web. ([#22094](https://github.com/expo/expo/pull/22094) by [@EvanBacon](https://github.com/EvanBacon))
## 1.1.0 — 2023-03-25
### 🎉 New features
- [Android] Add automatic asset downscaling to improve performance. ([#21628](https://github.com/expo/expo/pull/21628) by [@lukmccall](https://github.com/lukmccall))
### 🐛 Bug fixes
- Fixed the `tintColor` not being passed to native view. ([#21576](https://github.com/expo/expo/pull/21576) by [@andrew-levy](https://github.com/andrew-levy))
- Fixed `canvas: trying to use a recycled bitmap` on Android. ([#21658](https://github.com/expo/expo/pull/21658) by [@lukmccall](https://github.com/lukmccall))
- Fixed crashes caused by empty placeholder or source on Android. ([#21695](https://github.com/expo/expo/pull/21695) by [@lukmccall](https://github.com/lukmccall))
- Fixes `shouldDownscale` don't respect the scale factor on iOS. ([#21839](https://github.com/expo/expo/pull/21839) by [@ouabing](https://github.com/ouabing))
- Fixes cache policy not being correctly applied when set to `none` on iOS. ([#21840](https://github.com/expo/expo/pull/21840) by [@ouabing](https://github.com/ouabing))
## 1.0.0 — 2023-02-21
_This version does not introduce any user-facing changes._
## 1.0.0-rc.2 — 2023-02-20
### 🎉 New features
- Added `recyclingKey` prop that allows reseting the image view content when the view is recycled. ([#21297](https://github.com/expo/expo/pull/21297) & [#21309](https://github.com/expo/expo/pull/21309) by [@tsapeta](https://github.com/tsapeta) & [@lukmccall](https://github.com/lukmccall))
## 1.0.0-rc.1 — 2023-02-14
### 🐛 Bug fixes
- Fixed `You can't start or clear loads in RequestListener or Target callbacks` on Android. ([#21192](https://github.com/expo/expo/pull/21192) by [@lukmccall](https://github.com/lukmccall))
- Fixed SVGs are not rendered in the release mode on Android. ([#21214](https://github.com/expo/expo/pull/21214) by [@lukmccall](https://github.com/lukmccall))
- Stop sending `onProgress` event when the asset size is unknown which led to diving by zero and a crash. ([#21215](https://github.com/expo/expo/pull/21215) by [@tsapeta](https://github.com/tsapeta))
## 1.0.0-rc.0 — 2023-02-09
### 🎉 New features
- Added `placeholderContentFit` prop implementation on the web. ([#21106](https://github.com/expo/expo/pull/21106) by [@aleqsio](https://github.com/aleqsio))
## 1.0.0-beta.6 — 2023-02-06
### 🎉 New features
- Added new prop `placeholderContentFit` to specify custom content fit on the placeholder. ([#21096](https://github.com/expo/expo/pull/21096) by [@magrinj](https://github.com/magrinj))
### 🐛 Bug fixes
- [iOS] Fixed possible freezes by processing images concurrently off the main thread. ([#21086](https://github.com/expo/expo/pull/21086) by [@tsapeta](https://github.com/tsapeta))
## 1.0.0-beta.5 — 2023-02-03
_This version does not introduce any user-facing changes._
## 1.0.0-beta.4 — 2023-01-31
### 🐛 Bug fixes
- Fixed a crash on Android where `isScreenReaderFocusable` crashes devices below api 28. ([#21012](https://github.com/expo/expo/pull/21012) by [@alanhughes](https://github.com/alanjhughes))
## 1.0.0-beta.3 — 2023-01-30
### 🐛 Bug fixes
- Fixed a crash on iOS below 16.0 introduced by the Live Text interaction feature. ([#20987](https://github.com/expo/expo/pull/20987) by [@tsapeta](https://github.com/tsapeta))
## 1.0.0-beta.2 — 2023-01-25
### 🎉 New features
- Added support for Live text interaction. ([#20915](https://github.com/expo/expo/pull/20915) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
### 🐛 Bug fixes
- `ImageProps` now extends `ViewProps`. ([#20942](https://github.com/expo/expo/pull/20942) by [@appden](https://github.com/appden))
## 1.0.0-beta.1 — 2023-01-20
### 🐛 Bug fixes
- Use `SDImageAWebPCoder` on iOS 14+ to speed up loading WebP images. ([#20897](https://github.com/expo/expo/pull/20897) by [@tsapeta](https://github.com/tsapeta))
### 💡 Others
- On Android bump `compileSdkVersion` and `targetSdkVersion` to `33`. ([#20721](https://github.com/expo/expo/pull/20721) by [@lukmccall](https://github.com/lukmccall))
- Upgraded `SDWebImage` to `5.15.0` and `SDWebImageAVIFCoder` to `0.9.4`. ([#20898](https://github.com/expo/expo/pull/20898) by [@tsapeta](https://github.com/tsapeta))
## 1.0.0-beta.0 — 2023-01-19
### 🎉 New features
- Added support for crossfade transition on Android. ([#20784](https://github.com/expo/expo/pull/20784) by [@lukmccall](https://github.com/lukmccall))
- Added web support for the `blurRadius` prop. ([#20845](https://github.com/expo/expo/pull/20845) by [@aleqsio](https://github.com/aleqsio))
- Support for `accessible` and `accessibilityLabel` props on Android. ([#20801](https://github.com/expo/expo/pull/20801) by [@lukmccall](https://github.com/lukmccall))
- Support for `accessible` and `accessibilityLabel` props on iOS. ([#20892](https://github.com/expo/expo/pull/20892) by [@alanhughes](https://github.com/alanjhughes))
## 1.0.0-alpha.6 — 2023-01-10
### 🎉 New features
- Introduced the `source.cacheKey` parameter to customize the key used for caching the source image. ([#20772](https://github.com/expo/expo/pull/20772) by [@tsapeta](https://github.com/tsapeta), [#20776](https://github.com/expo/expo/pull/20776) by [@lukmccall](https://github.com/lukmccall))
## 1.0.0-alpha.5 — 2023-01-04
### 🎉 New features
- Added support for assets from the iOS Photo Library (`ph://` urls). ([#20700](https://github.com/expo/expo/pull/20700) by [@tsapeta](https://github.com/tsapeta))
### 🐛 Bug fixes
- Fixed `ImageProps` type not allowing an array of styles. ([#20701](https://github.com/expo/expo/pull/20701) by [@tsapeta](https://github.com/tsapeta))
## 1.0.0-alpha.4 — 2022-12-30
### 🐛 Bug fixes
- Fixed compatibility with `react-native-shared-element` on iOS. ([#20592](https://github.com/expo/expo/pull/20592) by [@IjzerenHein](https://github.com/ijzerenhein))
## 1.0.0-alpha.3 — 2022-12-21
### 🎉 New features
- Initial release 🥳

64
node_modules/expo-image/README.md generated vendored Normal file
View File

@@ -0,0 +1,64 @@
<p>
<a href="https://docs.expo.dev/versions/unversioned/sdk/image/">
<img
src="../../.github/resources/expo-image.svg"
alt="expo-image"
height="64" />
</a>
</p>
A cross-platform, performant image component for React Native and Expo.
## Main features
- Designed for speed
- Support for many image formats (including animated ones)
- Disk and memory caching
- Supports [BlurHash](https://blurha.sh) and [ThumbHash](https://evanw.github.io/thumbhash/) - compact representations of a placeholder for an image
- Transitioning between images when the source changes (no more flickering!)
- Implements the CSS [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) and [`object-position`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) properties (see [`contentFit`](#contentfit) and [`contentPosition`](#contentposition) props)
- Uses performant [`SDWebImage`](https://github.com/SDWebImage/SDWebImage) and [`Glide`](https://github.com/bumptech/glide) under the hood
## Supported image formats
| Format | Android | iOS | Web |
| :--------: | :-----: | :-: | :--------------------------------------------: |
| WebP | ✅ | ✅ | ✅ |
| PNG / APNG | ✅ | ✅ | ✅ |
| AVIF | ✅ | ✅ | ✅ |
| HEIC | ✅ | ✅ | ❌ [not adopted yet](https://caniuse.com/heif) |
| JPEG | ✅ | ✅ | ✅ |
| GIF | ✅ | ✅ | ✅ |
| SVG | ✅ | ✅ | ✅ |
| ICO | ✅ | ✅ | ✅ |
| ICNS | ❌ | ✅ | ❌ |
# API documentation
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/image/)
# Installation in managed Expo projects
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, follow the installation instructions in the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/image/).
# Installation in bare React Native projects
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
### Add the package to your npm dependencies
```
npx expo install expo-image
```
### Configure for iOS
Run `npx pod-install` after installing the npm package.
### Configure for Android
No additional setup necessary.
# Contributing
Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).

56
node_modules/expo-image/android/build.gradle generated vendored Normal file
View File

@@ -0,0 +1,56 @@
buildscript {
dependencies {
classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:${rootProject["kspVersion"]}"
}
}
plugins {
id 'com.android.library'
id 'kotlin-kapt'
id 'expo-module-gradle-plugin'
}
apply plugin: 'com.google.devtools.ksp'
group = 'expo.modules.image'
version = '55.0.6'
android {
namespace "expo.modules.image"
defaultConfig {
versionCode 1
versionName "55.0.6"
consumerProguardFiles("proguard-rules.pro")
buildConfigField("boolean", "ALLOW_GLIDE_LOGS", project.properties.get("EXPO_ALLOW_GLIDE_LOGS", "false"))
}
sourceSets {
main {
java {
if (expoModule.safeExtGet("excludeAppGlideModule", false)) {
exclude("**/ExpoImageAppGlideModule.kt")
}
}
}
}
}
dependencies {
def GLIDE_VERSION = "5.0.5"
implementation 'com.facebook.react:react-android'
api "com.github.bumptech.glide:glide:${GLIDE_VERSION}"
ksp "com.github.bumptech.glide:ksp:${GLIDE_VERSION}"
api 'com.caverock:androidsvg-aar:1.4'
implementation "com.github.penfeizhou.android.animation:glide-plugin:3.0.5"
implementation "com.github.bumptech.glide:avif-integration:${GLIDE_VERSION}"
api "com.github.bumptech.glide:okhttp3-integration:${GLIDE_VERSION}"
api "com.squareup.okhttp3:okhttp:${expoModule.safeExtGet("okHttpVersion", '4.9.2')}"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
implementation "jp.wasabeef:glide-transformations:4.3.0"
}

28
node_modules/expo-image/android/proguard-rules.pro generated vendored Normal file
View File

@@ -0,0 +1,28 @@
# https://bumptech.github.io/glide/doc/download-setup.html#proguard
-keep public class * extends com.bumptech.glide.module.LibraryGlideModule
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule {
<init>(...);
}
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
*** rewind();
}
-keep public class com.bumptech.glide.request.ThumbnailRequestCoordinator {
*;
}
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
# https://bumptech.github.io/glide/doc/configuration.html#applications
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
-keep public class com.bumptech.glide.integration.webp.WebpImage { *; }
-keep public class com.bumptech.glide.integration.webp.WebpFrame { *; }
-keep public class com.bumptech.glide.integration.webp.WebpBitmapFactory { *; }

View File

@@ -0,0 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Begin Glide configuration -->
<!-- Internet access (https://bumptech.github.io/glide/doc/download-setup.html#internet) -->
<uses-permission android:name="android.permission.INTERNET" />
<!--
Allows Glide to monitor connectivity status and restart failed requests if users go from a
a disconnected to a connected network state.
-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Local storage (https://bumptech.github.io/glide/doc/download-setup.html#local-storage) -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- End Glide configuration -->
</manifest>

View File

@@ -0,0 +1,125 @@
package com.caverock.androidsvg
import com.caverock.androidsvg.SVG.SPECIFIED_COLOR
import com.caverock.androidsvg.SVG.SPECIFIED_FILL
import com.caverock.androidsvg.SVG.SvgElementBase
internal fun replaceColor(paint: SVG.SvgPaint?, newColor: Int) {
if (paint is SVG.Colour && paint !== SVG.Colour.TRANSPARENT) {
paint.colour = newColor
}
}
internal fun replaceStyles(style: SVG.Style?, newColor: Int) {
if (style == null) {
return
}
replaceColor(style.color, newColor)
replaceColor(style.fill, newColor)
replaceColor(style.stroke, newColor)
replaceColor(style.stopColor, newColor)
replaceColor(style.solidColor, newColor)
replaceColor(style.viewportFill, newColor)
}
internal fun hasStyle(element: SvgElementBase): Boolean {
if (element.style == null && element.baseStyle == null) {
return false
}
val style = element.style
val hasColorInStyle = style != null &&
(
style.color != null || style.fill != null || style.stroke != null ||
style.stroke != null || style.stopColor != null || style.solidColor != null
)
if (hasColorInStyle) {
return true
}
val baseStyle = element.baseStyle ?: return false
return baseStyle.color != null || baseStyle.fill != null || baseStyle.stroke != null ||
baseStyle.viewportFill != null || baseStyle.stopColor != null || baseStyle.solidColor != null
}
internal fun defineStyles(element: SvgElementBase, newColor: Int, hasStyle: Boolean) {
if (hasStyle) {
return
}
val style = if (element.style != null) {
element.style
} else {
SVG.Style().also {
element.style = it
}
}
val color = SVG.Colour(newColor)
when (element) {
is SVG.Path,
is SVG.Circle,
is SVG.Ellipse,
is SVG.Rect,
is SVG.SolidColor,
is SVG.Line,
is SVG.Polygon,
is SVG.PolyLine -> {
style.apply {
fill = color
specifiedFlags = SPECIFIED_FILL
}
}
is SVG.TextPath -> {
style.apply {
this.color = color
specifiedFlags = SPECIFIED_COLOR
}
}
}
}
internal fun applyTintColor(element: SVG.SvgObject, newColor: Int, parentDefinesStyle: Boolean) {
// We want to keep the colors in the mask as they control the visibility of the element to which the mask is applied.
if (element is SVG.Mask) {
return
}
val definesStyle = if (element is SvgElementBase) {
val hasStyle = parentDefinesStyle || hasStyle(element)
replaceStyles(element.baseStyle, newColor)
replaceStyles(element.style, newColor)
defineStyles(element, newColor, hasStyle)
hasStyle
} else {
parentDefinesStyle
}
if (element is SVG.SvgContainer) {
for (child in element.children) {
applyTintColor(child, newColor, definesStyle)
}
}
}
fun applyTintColor(svg: SVG, newColor: Int) {
val root = svg.rootElement
svg.cssRules?.forEach { rule ->
replaceStyles(rule.style, newColor)
}
replaceStyles(root.baseStyle, newColor)
replaceStyles(root.style, newColor)
val hasStyle = hasStyle(root)
for (child in root.children) {
applyTintColor(child, newColor, hasStyle)
}
}

View File

@@ -0,0 +1,226 @@
package expo.modules.image
import android.os.Build
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.bumptech.glide.request.target.Target
import expo.modules.image.enums.ContentFit
import expo.modules.image.records.DecodeFormat
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
/**
* Glide uses `hashCode` and `equals` of the `DownsampleStrategy` to calculate the cache key.
* However, we generate this object dynamically, which means that each instance will be different.
* Unfortunately, this behaviour is not correct since Glide will not load
* the image from memory no matter what.
* To fix this issue, we set the `hashCode` to a fixed number and
* override `equals` to only check if objects have the common type.
*/
abstract class CustomDownsampleStrategy : DownsampleStrategy() {
override fun equals(other: Any?): Boolean {
return other is CustomDownsampleStrategy
}
override fun hashCode(): Int {
return 302008237
}
}
object NoopDownsampleStrategy : DownsampleStrategy() {
override fun getScaleFactor(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
): Float = 1f
override fun getSampleSizeRounding(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
): SampleSizeRounding = SampleSizeRounding.QUALITY
}
class PlaceholderDownsampleStrategy(
private val target: ImageViewWrapperTarget
) : CustomDownsampleStrategy() {
private var wasTriggered = false
override fun getScaleFactor(sourceWidth: Int, sourceHeight: Int, requestedWidth: Int, requestedHeight: Int): Float {
if (!wasTriggered) {
target.placeholderWidth = sourceWidth
target.placeholderHeight = sourceHeight
wasTriggered = true
}
return 1f
}
override fun getSampleSizeRounding(sourceWidth: Int, sourceHeight: Int, requestedWidth: Int, requestedHeight: Int): SampleSizeRounding {
return SampleSizeRounding.QUALITY
}
}
class ContentFitDownsampleStrategy(
private val target: ImageViewWrapperTarget,
private val contentFit: ContentFit
) : CustomDownsampleStrategy() {
private var wasTriggered = false
override fun getScaleFactor(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
): Float {
// The method is invoked twice per asset, but we only need to preserve the original dimensions for the first call.
// As Glide uses Android downsampling, it can only adjust dimensions by a factor of two,
// and hence two distinct scaling factors are computed to achieve greater accuracy.
if (!wasTriggered) {
target.sourceWidth = sourceWidth
target.sourceHeight = sourceHeight
wasTriggered = true
}
// The size of the container is unknown, we don't know what to do, so we just run the default scale.
if (requestedWidth == Target.SIZE_ORIGINAL || requestedHeight == Target.SIZE_ORIGINAL) {
return 1f
}
val aspectRation = calculateScaleFactor(
sourceWidth.toFloat(),
sourceHeight.toFloat(),
requestedWidth.toFloat(),
requestedHeight.toFloat()
)
// We don't want to upscale the image
return min(1f, aspectRation)
}
private fun calculateScaleFactor(
sourceWidth: Float,
sourceHeight: Float,
requestedWidth: Float,
requestedHeight: Float
): Float = when (contentFit) {
ContentFit.Contain -> min(
requestedWidth / sourceWidth,
requestedHeight / sourceHeight
)
ContentFit.Cover -> java.lang.Float.max(
requestedWidth / sourceWidth,
requestedHeight / sourceHeight
)
ContentFit.ScaleDown -> if (requestedWidth < sourceWidth || requestedHeight < sourceHeight) {
// The container is smaller than the image — scale it down and behave like `contain`
min(
requestedWidth / sourceWidth,
requestedHeight / sourceHeight
)
} else {
// The container is bigger than the image — don't scale it and behave like `none`
1f
}
else -> 1f
}
override fun getSampleSizeRounding(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
) = SampleSizeRounding.QUALITY
}
/**
* Android has hardware bitmap size limit that can be drown on the canvas.
* To prevents crashes, we need to downsample the image to fit into the maximum bitmap size.
*/
class SafeDownsampleStrategy(
private val decodeFormat: DecodeFormat
) : CustomDownsampleStrategy() {
override fun getScaleFactor(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
): Float {
if (maxBitmapSize <= 0) {
return 1f
}
val sourceSize = sourceWidth * sourceHeight * decodeFormat.toBytes()
if (sourceSize <= maxBitmapSize) {
return 1f
}
// We need to downsample the image to fit into the maximum bitmap size.
// Calculate the aspect ratio of the source image. It's always <= 1.
val srcAspectRatio = min(sourceWidth, sourceHeight).toDouble() / max(sourceWidth, sourceHeight).toDouble()
// Calculate the area of the destination image.
val dstArea = maxBitmapSize / decodeFormat.toBytes()
// Calculate the longer side of the destination image using following formulas:
// dstLongerSide * dstSmallerSide = dstArea
// srcAspectRation * dstLongerSide = dstSmallerSide
// after substitution:
// srcAspectRation * dstLongerSide * dstLongerSide = dstArea
// dstLongerSide = sqrt(dstArea / srcAspectRatio)
val x = floor(sqrt(dstArea.toDouble() / srcAspectRatio)).toInt()
// Calculate the scale factor using longer side of both images.
val scaleFactor = x.toDouble() / max(sourceWidth, sourceHeight).toDouble()
return scaleFactor.toFloat()
}
override fun getSampleSizeRounding(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
): SampleSizeRounding {
return SampleSizeRounding.MEMORY
}
private val maxBitmapSize by lazy {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return@lazy -1
}
return@lazy try {
val defaultSize = 100 * 1024 * 1024 // 100 MB - from `RecordingCanvas` src
// We're trying to get the value of `ro.hwui.max_texture_allocation_size` property
// which is used by `RecordingCanvas` to determine the maximum bitmap size.
@Suppress("PrivateApi")
val getIntMethod = Class
.forName("android.os.SystemProperties")
.getMethod("getInt", String::class.java, Int::class.java)
getIntMethod.isAccessible = true
(getIntMethod.invoke(null, "ro.hwui.max_texture_allocation_size", defaultSize) as Int)
.coerceAtLeast(defaultSize)
} catch (e: Throwable) {
// If something goes wrong we just return -1 and don't downsample the image.
-1
}
}
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other !is SafeDownsampleStrategy) {
return false
}
return decodeFormat == other.decodeFormat
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + decodeFormat.hashCode()
return result
}
}

View File

@@ -0,0 +1,8 @@
package expo.modules.image
import com.bumptech.glide.load.Option
object CustomOptions {
// To pass the tint color to the SVG decoder, we need to wrap it in a custom Glide option.
val tintColor = Option.memory<Int>("ExpoTintColor")
}

View File

@@ -0,0 +1,6 @@
package expo.modules.image
import expo.modules.kotlin.exception.CodedException
class ImageLoadFailed(exception: Exception) :
CodedException(message = "Failed to load the image: ${exception.message}")

View File

@@ -0,0 +1,26 @@
package expo.modules.image
import android.content.Context
import android.util.Log
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule
/**
* We need to include an [AppGlideModule] for [GlideModule] annotations
* to work.
*/
@GlideModule
class ExpoImageAppGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
super.applyOptions(context, builder)
builder.setLogLevel(
if (BuildConfig.ALLOW_GLIDE_LOGS) {
Log.VERBOSE
} else {
Log.ERROR
}
)
}
}

View File

@@ -0,0 +1,22 @@
package expo.modules.image
import android.content.ComponentCallbacks2
import android.content.res.Configuration
import expo.modules.image.blurhash.BlurhashDecoder
/**
* Clears the Blurhash cache when the memory is low.
*/
object ExpoImageComponentCallbacks : ComponentCallbacks2 {
override fun onConfigurationChanged(newConfig: Configuration) = Unit
override fun onLowMemory() {
BlurhashDecoder.clearCache()
}
override fun onTrimMemory(level: Int) {
if (level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
onLowMemory()
}
}
}

View File

@@ -0,0 +1,338 @@
package expo.modules.image
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.Base64
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.drawable.toBitmapOrNull
import androidx.core.view.doOnDetach
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.Headers
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.github.penfeizhou.animation.apng.APNGDrawable
import com.github.penfeizhou.animation.gif.GifDrawable
import com.github.penfeizhou.animation.webp.WebPDrawable
import expo.modules.image.blurhash.BlurhashEncoder
import expo.modules.image.enums.ContentFit
import expo.modules.image.enums.Priority
import expo.modules.image.records.CachePolicy
import expo.modules.image.records.ContentPosition
import expo.modules.image.records.DecodeFormat
import expo.modules.image.records.DecodedSource
import expo.modules.image.records.ImageLoadOptions
import expo.modules.image.records.ImageTransition
import expo.modules.image.records.SourceMap
import expo.modules.image.thumbhash.ThumbhashEncoder
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.functions.Coroutine
import expo.modules.kotlin.functions.Queues
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.sharedobjects.SharedRef
import expo.modules.kotlin.types.Either
import expo.modules.kotlin.types.EitherOfThree
import expo.modules.kotlin.types.toKClass
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.URL
class ExpoImageModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoImage")
OnCreate {
appContext.reactContext?.registerComponentCallbacks(ExpoImageComponentCallbacks)
}
OnDestroy {
appContext.reactContext?.unregisterComponentCallbacks(ExpoImageComponentCallbacks)
}
AsyncFunction("prefetch") { urls: List<String>, cachePolicy: CachePolicy, headersMap: Map<String, String>?, promise: Promise ->
val context = appContext.reactContext ?: return@AsyncFunction false
var imagesLoaded = 0
var failed = false
val headers = headersMap?.let {
LazyHeaders.Builder().apply {
it.forEach { (key, value) ->
addHeader(key, value)
}
}.build()
} ?: Headers.DEFAULT
urls.forEach {
Glide
.with(context)
.load(GlideUrl(it, headers)) // Use `load` instead of `download` to store the asset in the memory cache
// We added `quality` and `downsample` to create the same cache key as in final image load.
.encodeQuality(100)
.downsample(NoopDownsampleStrategy)
.customize(`when` = cachePolicy == CachePolicy.MEMORY) {
diskCacheStrategy(DiskCacheStrategy.NONE)
}
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean {
if (!failed) {
failed = true
promise.resolve(false)
}
return true
}
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
imagesLoaded++
if (imagesLoaded == urls.size) {
promise.resolve(true)
}
return true
}
})
.submit()
}
}
AsyncFunction("loadAsync") Coroutine { source: SourceMap, options: ImageLoadOptions? ->
ImageLoadTask(appContext, source, options ?: ImageLoadOptions()).load()
}
suspend fun generatePlaceholder(
source: Either<URL, Image>,
encoder: (Bitmap) -> String
): String {
val image = source.let {
if (it.`is`(Image::class)) {
it.get(Image::class)
} else {
ImageLoadTask(appContext, SourceMap(uri = it.get(URL::class).toString()), ImageLoadOptions()).load()
}
}
return withContext(Dispatchers.Default) {
encoder(image.ref.toBitmap())
}
}
AsyncFunction("generateBlurhashAsync") Coroutine { source: Either<URL, Image>, numberOfComponents: Pair<Int, Int> ->
generatePlaceholder(source) { bitmap ->
BlurhashEncoder.encode(bitmap, numberOfComponents)
}
}
AsyncFunction("generateThumbhashAsync") Coroutine { source: Either<URL, Image> ->
generatePlaceholder(source) { bitmap ->
Base64.encodeToString(
ThumbhashEncoder.encode(bitmap),
Base64.NO_WRAP
)
}
}
Class(Image::class) {
Property("width") { image: Image ->
image.ref.intrinsicWidth
}
Property("height") { image: Image ->
image.ref.intrinsicHeight
}
Property("scale") { image: Image ->
// Not relying on `2x` in the filename, but want to make the following true:
// If you multiply the logical size of the image by this value, you get the dimensions of the image in pixels.
val screenDensity = appContext.reactContext?.resources?.displayMetrics?.density ?: 1f
(image.ref.toBitmapOrNull()?.density ?: 1) / (screenDensity * 160.0f)
}
Property("isAnimated") { image: Image ->
if (image.ref is GifDrawable) {
return@Property true
}
if (image.ref is APNGDrawable) {
return@Property true
}
if (image.ref is WebPDrawable) {
return@Property true
}
false
}
Property<Any?>("mediaType") { ->
null // not easily supported on Android https://github.com/bumptech/glide/issues/1378#issuecomment-236879983
}
}
AsyncFunction("clearMemoryCache") {
val activity = appContext.currentActivity ?: return@AsyncFunction false
Glide.get(activity).clearMemory()
return@AsyncFunction true
}.runOnQueue(Queues.MAIN)
AsyncFunction<Boolean>("clearDiskCache") {
val activity = appContext.currentActivity ?: return@AsyncFunction false
activity.let {
Glide.get(activity).clearDiskCache()
}
return@AsyncFunction true
}
AsyncFunction("getCachePathAsync") { cacheKey: String ->
val context = appContext.reactContext ?: return@AsyncFunction null
val glideUrl = GlideUrl(cacheKey)
val target = Glide.with(context).asFile().load(glideUrl).onlyRetrieveFromCache(true).submit()
return@AsyncFunction try {
val file = target.get()
file.absolutePath
} catch (_: Exception) {
null
}
}
View(ExpoImageViewWrapper::class) {
Events(
"onLoadStart",
"onProgress",
"onError",
"onLoad",
"onDisplay"
)
Prop("source") { view: ExpoImageViewWrapper, sources: EitherOfThree<List<SourceMap>, SharedRef<Drawable>, SharedRef<Bitmap>>? ->
if (sources == null) {
view.sources = emptyList()
return@Prop
}
if (sources.`is`(toKClass<List<SourceMap>>())) {
view.sources = sources.get(toKClass<List<SourceMap>>())
return@Prop
}
if (sources.`is`(toKClass<SharedRef<Drawable>>())) {
val drawable = sources.get(toKClass<SharedRef<Drawable>>()).ref
view.sources = listOf(DecodedSource(drawable))
return@Prop
}
val bitmap = sources.get(toKClass<SharedRef<Bitmap>>()).ref
val context = appContext.reactContext ?: throw Exceptions.ReactContextLost()
val drawable = BitmapDrawable(context.resources, bitmap)
view.sources = listOf(DecodedSource(drawable))
}
Prop("contentFit") { view: ExpoImageViewWrapper, contentFit: ContentFit? ->
view.contentFit = contentFit ?: ContentFit.Cover
}
Prop("placeholderContentFit") { view: ExpoImageViewWrapper, placeholderContentFit: ContentFit? ->
view.placeholderContentFit = placeholderContentFit ?: ContentFit.ScaleDown
}
Prop("contentPosition") { view: ExpoImageViewWrapper, contentPosition: ContentPosition? ->
view.contentPosition = contentPosition ?: ContentPosition.center
}
Prop("blurRadius") { view: ExpoImageViewWrapper, blurRadius: Int? ->
view.blurRadius = blurRadius?.takeIf { it > 0 }
}
Prop("transition") { view: ExpoImageViewWrapper, transition: ImageTransition? ->
view.transition = transition
}
Prop("tintColor") { view: ExpoImageViewWrapper, color: Int? ->
view.tintColor = color
}
Prop("placeholder") { view: ExpoImageViewWrapper, placeholder: List<SourceMap>? ->
view.placeholders = placeholder ?: emptyList()
}
Prop("accessible") { view: ExpoImageViewWrapper, accessible: Boolean? ->
view.accessible = accessible == true
}
Prop("accessibilityLabel") { view: ExpoImageViewWrapper, accessibilityLabel: String? ->
view.accessibilityLabel = accessibilityLabel
}
Prop("focusable") { view: ExpoImageViewWrapper, isFocusable: Boolean? ->
view.isFocusableProp = isFocusable == true
}
Prop("priority") { view: ExpoImageViewWrapper, priority: Priority? ->
view.priority = priority ?: Priority.NORMAL
}
Prop("cachePolicy") { view: ExpoImageViewWrapper, cachePolicy: CachePolicy? ->
view.cachePolicy = cachePolicy ?: CachePolicy.DISK
}
Prop("recyclingKey") { view: ExpoImageViewWrapper, recyclingKey: String? ->
view.recyclingKey = recyclingKey
}
Prop("allowDownscaling") { view: ExpoImageViewWrapper, allowDownscaling: Boolean? ->
view.allowDownscaling = allowDownscaling != false
}
Prop("autoplay") { view: ExpoImageViewWrapper, autoplay: Boolean? ->
view.autoplay = autoplay != false
}
Prop("decodeFormat") { view: ExpoImageViewWrapper, format: DecodeFormat? ->
view.decodeFormat = format ?: DecodeFormat.ARGB_8888
}
AsyncFunction("startAnimating") { view: ExpoImageViewWrapper ->
view.setIsAnimating(true)
}
AsyncFunction("stopAnimating") { view: ExpoImageViewWrapper ->
view.setIsAnimating(false)
}
AsyncFunction("lockResourceAsync") { view: ExpoImageViewWrapper ->
view.lockResource = true
}
AsyncFunction("unlockResourceAsync") { view: ExpoImageViewWrapper ->
view.lockResource = false
}
AsyncFunction("reloadAsync") { view: ExpoImageViewWrapper ->
view.shouldRerender = true
view.rerenderIfNeeded(force = true)
}
OnViewDidUpdateProps { view: ExpoImageViewWrapper ->
view.rerenderIfNeeded()
}
OnViewDestroys { view: ExpoImageViewWrapper ->
view.doOnDetach {
view.onViewDestroys()
}
}
}
}
}

View File

@@ -0,0 +1,129 @@
package expo.modules.image
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.PorterDuff
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.Log
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.graphics.transform
import androidx.core.view.isVisible
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import expo.modules.image.enums.ContentFit
import expo.modules.image.records.ContentPosition
@OptIn(UnstableReactNativeAPI::class)
@SuppressLint("ViewConstructor")
class ExpoImageView(
context: Context
) : AppCompatImageView(context) {
var currentTarget: ImageViewWrapperTarget? = null
var isPlaceholder: Boolean = false
fun recycleView(): ImageViewWrapperTarget? {
setImageDrawable(null)
val target = currentTarget?.apply {
isUsed = false
}
currentTarget = null
isVisible = false
isPlaceholder = false
return target
}
private var transformationMatrixChanged = false
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
applyTransformationMatrix()
}
fun applyTransformationMatrix() {
if (drawable == null) {
return
}
if (isPlaceholder) {
applyTransformationMatrix(
drawable,
placeholderContentFit,
sourceHeight = currentTarget?.placeholderHeight,
sourceWidth = currentTarget?.placeholderWidth
)
} else {
applyTransformationMatrix(drawable, contentFit, contentPosition)
}
}
private fun applyTransformationMatrix(
drawable: Drawable,
contentFit: ContentFit,
contentPosition: ContentPosition = ContentPosition.center,
sourceWidth: Int? = currentTarget?.sourceWidth,
sourceHeight: Int? = currentTarget?.sourceHeight
) {
val imageRect = RectF(0f, 0f, drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat())
val viewRect = RectF(0f, 0f, width.toFloat(), height.toFloat())
val matrix = contentFit.toMatrix(
imageRect,
viewRect,
sourceWidth ?: -1,
sourceHeight ?: -1
)
val scaledImageRect = imageRect.transform(matrix)
imageMatrix = matrix.apply {
contentPosition.apply(this, scaledImageRect, viewRect)
}
}
init {
clipToOutline = true
scaleType = ScaleType.MATRIX
}
// region Component Props
internal var contentFit: ContentFit = ContentFit.Cover
set(value) {
field = value
transformationMatrixChanged = true
}
internal var placeholderContentFit: ContentFit = ContentFit.ScaleDown
set(value) {
field = value
transformationMatrixChanged = true
}
internal var contentPosition: ContentPosition = ContentPosition.center
set(value) {
field = value
transformationMatrixChanged = true
}
internal fun setTintColor(color: Int?) {
color?.let { setColorFilter(it, PorterDuff.Mode.SRC_IN) } ?: clearColorFilter()
}
override fun draw(canvas: Canvas) {
// If we encounter a recycled bitmap here, it suggests an issue where we may have failed to
// finish clearing the image bitmap before the UI attempts to display it.
// One solution could be to suppress the error and assume that the second image view is currently responsible for displaying the correct view.
if ((drawable as? BitmapDrawable)?.bitmap?.isRecycled == true) {
Log.e("ExpoImage", "Trying to use a recycled bitmap")
recycleView()?.let { target ->
(parent as? ExpoImageViewWrapper)?.requestManager?.let { requestManager ->
target.clear(requestManager)
}
}
}
super.draw(canvas)
}
}

View File

@@ -0,0 +1,654 @@
package expo.modules.image
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Handler
import android.view.View
import android.widget.FrameLayout
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.bumptech.glide.request.RequestOptions
import com.github.penfeizhou.animation.gif.GifDrawable
import expo.modules.image.enums.ContentFit
import expo.modules.image.enums.Priority
import expo.modules.image.events.GlideRequestListener
import expo.modules.image.events.OkHttpProgressListener
import expo.modules.image.okhttp.GlideUrlWrapper
import expo.modules.image.records.CachePolicy
import expo.modules.image.records.ContentPosition
import expo.modules.image.records.DecodeFormat
import expo.modules.image.records.ImageErrorEvent
import expo.modules.image.records.ImageLoadEvent
import expo.modules.image.records.ImageProgressEvent
import expo.modules.image.records.ImageTransition
import expo.modules.image.records.Source
import expo.modules.image.svg.SVGPictureDrawable
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.tracing.beginAsyncTraceBlock
import expo.modules.kotlin.tracing.trace
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
import jp.wasabeef.glide.transformations.BlurTransformation
import java.lang.ref.WeakReference
import kotlin.math.abs
import kotlin.math.min
@SuppressLint("ViewConstructor")
class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
private val activity: Activity
get() = appContext.throwingActivity
internal val requestManager = getOrCreateRequestManager(appContext, activity)
private val progressListener = OkHttpProgressListener(WeakReference(this))
private val firstView = ExpoImageView(activity)
private val secondView = ExpoImageView(activity)
private val mainHandler = Handler(context.mainLooper)
/**
* @returns the view which is currently active or will be used when both views are empty
*/
private val activeView: ExpoImageView
get() {
if (secondView.drawable != null) {
return secondView
}
return firstView
}
private var firstTarget = ImageViewWrapperTarget(WeakReference(this))
private var secondTarget = ImageViewWrapperTarget(WeakReference(this))
internal val onLoadStart by EventDispatcher<Unit>()
internal val onProgress by EventDispatcher<ImageProgressEvent>()
internal val onError by EventDispatcher<ImageErrorEvent>()
internal val onLoad by EventDispatcher<ImageLoadEvent>()
internal val onDisplay by EventDispatcher<Unit>()
internal var sources: List<Source> = emptyList()
private val bestSource: Source?
get() = getBestSource(sources)
internal var placeholders: List<Source> = emptyList()
private val bestPlaceholder: Source?
get() = getBestSource(placeholders)
internal var blurRadius: Int? = null
set(value) {
if (field != value) {
shouldRerender = true
}
field = value
}
internal var transition: ImageTransition? = null
internal var contentFit: ContentFit = ContentFit.Cover
set(value) {
field = value
activeView.contentFit = value
transformationMatrixChanged = true
}
internal var placeholderContentFit: ContentFit = ContentFit.ScaleDown
set(value) {
field = value
activeView.placeholderContentFit = value
transformationMatrixChanged = true
}
internal var contentPosition: ContentPosition = ContentPosition.center
set(value) {
field = value
activeView.contentPosition = value
transformationMatrixChanged = true
}
internal var tintColor: Int? = null
set(value) {
field = value
// To apply the tint color to the SVG, we need to recreate the drawable.
if (activeView.drawable is SVGPictureDrawable) {
shouldRerender = true
} else {
activeView.setTintColor(value)
}
}
internal var isFocusableProp: Boolean = false
set(value) {
field = value
activeView.isFocusable = value
}
internal var accessible: Boolean = false
set(value) {
field = value
setIsScreenReaderFocusable(activeView, value)
}
internal var accessibilityLabel: String? = null
set(value) {
field = value
activeView.contentDescription = accessibilityLabel
}
var recyclingKey: String? = null
set(value) {
clearViewBeforeChangingSource = field != null && value != null && value != field
field = value
}
internal var allowDownscaling: Boolean = true
set(value) {
field = value
shouldRerender = true
}
internal var decodeFormat: DecodeFormat = DecodeFormat.ARGB_8888
set(value) {
field = value
shouldRerender = true
}
internal var autoplay: Boolean = true
internal var lockResource: Boolean = false
internal var priority: Priority = Priority.NORMAL
internal var cachePolicy: CachePolicy = CachePolicy.DISK
fun setIsAnimating(setAnimating: Boolean) {
// Animatable animations always start from the beginning when resumed.
// So we check first if the resource is a GifDrawable, because it can continue
// from where it was paused.
when (val resource = activeView.drawable) {
is GifDrawable -> setIsAnimating(resource, setAnimating)
is Animatable -> setIsAnimating(resource, setAnimating)
}
}
private fun setIsAnimating(resource: GifDrawable, setAnimating: Boolean) {
if (setAnimating) {
if (resource.isPaused) {
resource.resume()
} else {
resource.start()
}
} else {
resource.pause()
}
}
private fun setIsAnimating(resource: Animatable, setAnimating: Boolean) {
if (setAnimating) {
resource.start()
} else {
resource.stop()
}
}
/**
* Whether the image should be loaded again
*/
internal var shouldRerender = false
/**
* Currently loaded source
*/
private var loadedSource: GlideModelProvider? = null
/**
* Currently loaded placeholder
*/
private var loadedPlaceholder: GlideModelProvider? = null
/**
* Whether the transformation matrix should be reapplied
*/
private var transformationMatrixChanged = false
/**
* Whether the view content should be cleared to blank when the source was changed.
*/
private var clearViewBeforeChangingSource = false
/**
* Copies saved props to the provided view.
* It ensures that the view state is up to date.
*/
private fun copyProps(view: ExpoImageView) {
view.contentFit = contentFit
view.contentPosition = contentPosition
view.setTintColor(tintColor)
view.isFocusable = isFocusableProp
view.contentDescription = accessibilityLabel
setIsScreenReaderFocusable(view, accessible)
}
/**
* Allows `isScreenReaderFocusable` to be set on apis below level 28
*/
private fun setIsScreenReaderFocusable(view: View, value: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
view.isScreenReaderFocusable = value
} else {
ViewCompat.setAccessibilityDelegate(
this,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
info.isScreenReaderFocusable = value
super.onInitializeAccessibilityNodeInfo(host, info)
}
}
)
}
}
/**
* When a new resource is available, this method tries to handle it.
* It decides where provided bitmap should be displayed and clears the previous target/image.
*/
fun onResourceReady(
target: ImageViewWrapperTarget,
resource: Drawable,
isPlaceholder: Boolean = false
) =
// The "onResourceReady" function will be triggered when the new resource is available by the Glide.
// According to the Glide documentation (https://bumptech.github.io/glide/doc/debugging.html#you-cant-start-or-clear-loads-in-requestlistener-or-target-callbacks),
// it's not advisable to clear the Glide target within the stack frame.
// To avoid this, a new runnable is posted to the front of the main queue, which can then clean or create targets.
// This ensures that the "onResourceReady" frame of the Glide code will be discarded, and the internal state can be altered once again.
// Normally, using "postAtFrontOfQueue" can lead to issues such as message queue starvation, ordering problems, and other unexpected consequences.
// However, in this case, it is safe to use as long as nothing else is added to the queue.
// The intention is simply to wait for the Glide code to finish before the content of the underlying views is changed during the same rendering tick.
mainHandler.postAtFrontOfQueue {
trace(Trace.tag, "onResourceReady") {
val transitionDuration = (transition?.duration ?: 0).toLong()
// If provided resource is a placeholder, but the target doesn't have a source, we treat it as a normal image.
if (!isPlaceholder || !target.hasSource) {
val (newView, previousView) = if (firstView.drawable == null) {
firstView to secondView
} else {
secondView to firstView
}
val clearPreviousView = {
previousView
.recycleView()
?.apply {
// When the placeholder is loaded, one target is displayed in both views.
// So we just have to move the reference to a new view instead of clearing the target.
if (this != target) {
clear(requestManager)
}
}
}
configureView(newView, target, resource, isPlaceholder)
// Dispatch "onDisplay" event only for the main source (no placeholder).
if (target.hasSource) {
onDisplay.invoke(Unit)
}
if (transitionDuration <= 0) {
clearPreviousView()
newView.alpha = 1f
newView.bringToFront()
} else {
newView.bringToFront()
previousView.alpha = 1f
newView.alpha = 0f
previousView.animate().apply {
duration = transitionDuration
alpha(0f)
withEndAction {
clearPreviousView()
}
}
newView.animate().apply {
duration = transitionDuration
alpha(1f)
}
}
} else {
// We don't want to show the placeholder if something is currently displayed.
// There is one exception - when we're displaying a different placeholder.
if ((firstView.drawable != null && !firstView.isPlaceholder) || secondView.drawable != null) {
return@trace
}
firstView
.recycleView()
?.apply {
// The current target is already bound to the view. We don't want to cancel it in that case.
if (this != target) {
clear(requestManager)
}
}
configureView(firstView, target, resource, isPlaceholder)
if (transitionDuration > 0) {
firstView.bringToFront()
firstView.alpha = 0f
secondView.isVisible = false
firstView.animate().apply {
duration = transitionDuration
alpha(1f)
}
}
}
// If our image is animated, we want to see if autoplay is disabled. If it is, we should
// stop the animation as soon as the resource is ready. Placeholders should not follow this
// value since the intention is almost certainly to display the animation (i.e. a spinner)
if (resource is Animatable && !isPlaceholder && !autoplay) {
resource.stop()
}
}
}
private fun configureView(
view: ExpoImageView,
target: ImageViewWrapperTarget,
resource: Drawable,
isPlaceholder: Boolean
) {
view.let {
it.setImageDrawable(resource)
it.isPlaceholder = isPlaceholder
it.placeholderContentFit = target.placeholderContentFit ?: ContentFit.ScaleDown
copyProps(it)
it.isVisible = true
it.currentTarget = target
// The view isn't layout when it's invisible.
// Therefore, we have to set the correct size manually.
it.layout(0, 0, width, height)
it.applyTransformationMatrix()
}
target.isUsed = true
if (resource is Animatable) {
resource.start()
}
}
private fun getBestSource(sources: List<Source>): Source? {
if (sources.isEmpty()) {
return null
}
if (sources.size == 1) {
return sources.first()
}
val targetPixelCount = width * height
if (targetPixelCount == 0) {
return null
}
var bestSource: Source? = null
var bestFit = Double.MAX_VALUE
sources.forEach {
val fit = abs(1 - (it.pixelCount / targetPixelCount))
if (fit < bestFit) {
bestFit = fit
bestSource = it
}
}
return bestSource
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
rerenderIfNeeded(
shouldRerenderBecauseOfResize = allowDownscaling &&
contentFit != ContentFit.Fill &&
contentFit != ContentFit.None
)
}
private fun createPropOptions(): RequestOptions {
return RequestOptions()
.priority(this@ExpoImageViewWrapper.priority.toGlidePriority())
.customize(`when` = cachePolicy != CachePolicy.MEMORY_AND_DISK && cachePolicy != CachePolicy.MEMORY) {
skipMemoryCache(true)
}
.customize(`when` = cachePolicy == CachePolicy.NONE || cachePolicy == CachePolicy.MEMORY) {
diskCacheStrategy(DiskCacheStrategy.NONE)
}
.customize(blurRadius) {
transform(BlurTransformation(min(it, 25), 4))
}
}
fun onViewDestroys() {
firstView.setImageDrawable(null)
secondView.setImageDrawable(null)
requestManager.clear(firstTarget)
requestManager.clear(secondTarget)
}
private fun cleanIfNeeded(
newBestSource: Source?,
newBestSourceModel: GlideModelProvider?,
newBestPlaceholderModel: GlideModelProvider?
): Boolean {
// We only clean the image when the source is set to null and we don't have a placeholder or the view is empty.
if (width == 0 || height == 0 || (newBestSource == null || newBestSourceModel == null) && newBestPlaceholderModel == null) {
firstView.recycleView()
secondView.recycleView()
requestManager.clear(firstTarget)
requestManager.clear(secondTarget)
shouldRerender = false
loadedSource = null
loadedPlaceholder = null
transformationMatrixChanged = false
clearViewBeforeChangingSource = false
return true
}
return false
}
private fun createDownsampleStrategy(target: ImageViewWrapperTarget): DownsampleStrategy {
return if (!allowDownscaling) {
DownsampleStrategy.NONE
} else if (
contentFit != ContentFit.Fill &&
contentFit != ContentFit.None
) {
ContentFitDownsampleStrategy(target, contentFit)
} else {
// it won't downscale the image if the image is smaller than hardware bitmap size limit
SafeDownsampleStrategy(decodeFormat)
}
}
private fun clearViewBeforeChangingSource() {
if (clearViewBeforeChangingSource) {
val activeView = if (firstView.drawable != null) {
firstView
} else {
secondView
}
activeView
.recycleView()
?.apply {
clear(requestManager)
}
}
}
internal fun rerenderIfNeeded(shouldRerenderBecauseOfResize: Boolean = false, force: Boolean = false) =
trace(Trace.tag, "rerenderIfNeeded(shouldRerenderBecauseOfResize=$shouldRerenderBecauseOfResize,force=$force)") {
if (lockResource && !force) {
return@trace
}
val bestSource = bestSource
val bestPlaceholder = bestPlaceholder
val sourceToLoad = bestSource?.createGlideModelProvider(context)
val placeholder = bestPlaceholder?.createGlideModelProvider(context)
if (cleanIfNeeded(bestSource, sourceToLoad, placeholder)) {
// the view was cleaned
return@trace
}
val shouldRerender = sourceToLoad != loadedSource || placeholder != loadedPlaceholder || shouldRerender || (sourceToLoad == null && placeholder != null)
if (!shouldRerender && !shouldRerenderBecauseOfResize) {
// In the case where the source didn't change, but the transformation matrix has to be
// recalculated, we can apply the new transformation right away.
// When the source and the matrix is different, we don't want to do anything.
// We don't want to changed the transformation of the currently displayed image.
// The new matrix will be applied when new resource is loaded.
if (transformationMatrixChanged) {
activeView.applyTransformationMatrix()
}
transformationMatrixChanged = false
clearViewBeforeChangingSource = false
return@trace
}
clearViewBeforeChangingSource()
this.shouldRerender = false
loadedSource = sourceToLoad
loadedPlaceholder = placeholder
val options = bestSource?.createGlideOptions(context)
val propOptions = createPropOptions()
val model = sourceToLoad?.getGlideModel()
if (model is GlideUrlWrapper) {
model.progressListener = progressListener
}
onLoadStart.invoke(Unit)
val newTarget = if (secondTarget.isUsed) {
firstTarget
} else {
secondTarget
}
newTarget.hasSource = sourceToLoad != null
val downsampleStrategy = createDownsampleStrategy(newTarget)
val request = requestManager
.asDrawable()
.load(model)
.customize(bestPlaceholder, placeholder) { placeholderSource, placeholderModel ->
val newPlaceholderContentFit = if (!placeholderSource.usesPlaceholderContentFit()) {
contentFit
} else {
placeholderContentFit
}
newTarget.placeholderContentFit = newPlaceholderContentFit
thumbnail(
requestManager.load(placeholderModel.getGlideModel())
.downsample(PlaceholderDownsampleStrategy(newTarget))
.apply(placeholderSource.createGlideOptions(context))
)
}
.downsample(downsampleStrategy)
.addListener(GlideRequestListener(WeakReference(this)))
.encodeQuality(100)
.format(decodeFormat.toGlideFormat())
.apply(propOptions)
.apply(options)
.customize(tintColor) {
apply(RequestOptions().set(CustomOptions.tintColor, it))
}
val cookie = Trace.getNextCookieValue()
beginAsyncTraceBlock(Trace.tag, Trace.loadNewImageBlock, cookie)
newTarget.setCookie(cookie)
request.into(newTarget)
transformationMatrixChanged = false
clearViewBeforeChangingSource = false
}
init {
val matchParent = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
layoutParams = matchParent
firstView.isVisible = true
secondView.isVisible = true
// We need to add a `FrameLayout` to allow views to overflow.
// With the `LinearLayout` is impossible to render two views on each other.
val layout = FrameLayout(context).apply {
layoutParams = matchParent
addView(
firstView,
matchParent
)
addView(
secondView,
matchParent
)
}
addView(layout, matchParent)
}
companion object {
private var requestManager: RequestManager? = null
private var appContextRef: WeakReference<AppContext?> = WeakReference(null)
private var activityRef: WeakReference<Activity?> = WeakReference(null)
fun getOrCreateRequestManager(
appContext: AppContext,
activity: Activity
): RequestManager = synchronized(Companion) {
val cachedRequestManager = requestManager
?: return createNewRequestManager(activity).also {
requestManager = it
appContextRef = WeakReference(appContext)
activityRef = WeakReference(activity)
}
// Request manager was created using different activity or app context
if (appContextRef.get() != appContext || activityRef.get() != activity) {
return createNewRequestManager(activity).also {
requestManager = it
appContextRef = WeakReference(appContext)
activityRef = WeakReference(activity)
}
}
return cachedRequestManager
}
private fun createNewRequestManager(activity: Activity): RequestManager = Glide.with(activity)
}
}

View File

@@ -0,0 +1,67 @@
package expo.modules.image
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.request.RequestOptions
/**
* Conditionally applies the block to the RequestBuilder if the condition is true.
*/
fun <T> RequestBuilder<T>.customize(`when`: Boolean, block: RequestBuilder<T>.() -> RequestBuilder<T>): RequestBuilder<T> {
if (!`when`) {
return this
}
return block()
}
/**
* Conditionally applies the block to the RequestBuilder if the value is not null.
*/
inline fun <T, P> RequestBuilder<T>.customize(value: P?, block: RequestBuilder<T>.(P) -> RequestBuilder<T>): RequestBuilder<T> {
if (value == null) {
return this
}
return block(value)
}
/**
* Conditionally applies the block to the RequestBuilder if both values aren't null.
*/
inline fun <T, P1, P2> RequestBuilder<T>.customize(first: P1?, second: P2?, block: RequestBuilder<T>.(P1, P2) -> RequestBuilder<T>): RequestBuilder<T> {
if (first == null || second == null) {
return this
}
return block(first, second)
}
/**
* Conditionally applies the block to the RequestOptions if the condition is true.
*/
inline fun RequestOptions.customize(`when`: Boolean, block: RequestOptions.() -> RequestOptions): RequestOptions {
if (!`when`) {
return this
}
return block()
}
/**
* Conditionally applies the block to the RequestOptions if the value is not null.
*/
inline fun <T> RequestOptions.customize(value: T?, block: RequestOptions.(T) -> RequestOptions): RequestOptions {
if (value == null) {
return this
}
return block(value)
}
fun <T> RequestBuilder<T>.apply(options: RequestOptions?): RequestBuilder<T> {
if (options == null) {
return this
}
return apply(options)
}

View File

@@ -0,0 +1,51 @@
package expo.modules.image
import android.graphics.drawable.Drawable
import android.net.Uri
import com.bumptech.glide.load.model.GlideUrl
import expo.modules.image.blurhash.BlurhashModel
import expo.modules.image.decodedsource.DecodedModel
import expo.modules.image.okhttp.GlideUrlWrapper
import expo.modules.image.thumbhash.ThumbhashModel
fun interface GlideModelProvider {
fun getGlideModel(): Any
}
data class DecodedModelProvider(
private val drawable: Drawable
) : GlideModelProvider {
override fun getGlideModel() = DecodedModel(drawable)
}
data class UrlModelProvider(
private val glideUrl: GlideUrl
) : GlideModelProvider {
override fun getGlideModel() = GlideUrlWrapper(glideUrl)
}
data class RawModelProvider(
private val data: String
) : GlideModelProvider {
override fun getGlideModel() = data
}
data class UriModelProvider(
private val uri: Uri
) : GlideModelProvider {
override fun getGlideModel() = uri
}
data class BlurhashModelProvider(
private val uri: Uri,
private val width: Int,
private val height: Int
) : GlideModelProvider {
override fun getGlideModel() = BlurhashModel(uri, width, height)
}
data class ThumbhashModelProvider(
private val uri: Uri
) : GlideModelProvider {
override fun getGlideModel() = ThumbhashModel(uri)
}

View File

@@ -0,0 +1,20 @@
package expo.modules.image
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import expo.modules.kotlin.sharedobjects.SharedRef
class Image(ref: Drawable) : SharedRef<Drawable>(ref) {
override val nativeRefType: String = "image"
override fun getAdditionalMemoryPressure(): Int {
val ref = ref
if (ref is BitmapDrawable) {
return ref.bitmap.allocationByteCount
}
// We can't get the size in bytes of the drawable.
// Let's just return the size in pixels for now.
return ref.intrinsicWidth * ref.intrinsicHeight
}
}

View File

@@ -0,0 +1,51 @@
package expo.modules.image
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import androidx.annotation.RequiresApi
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.Glide
import expo.modules.image.records.ImageLoadOptions
import expo.modules.image.records.SourceMap
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.exception.Exceptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
open class ImageLoadTask(
private val appContext: AppContext,
private val source: SourceMap,
private val options: ImageLoadOptions
) {
@RequiresApi(Build.VERSION_CODES.O)
suspend fun load(): Image {
val context =
this@ImageLoadTask.appContext.reactContext ?: throw Exceptions.ReactContextLost()
val sourceToLoad = source.createGlideModelProvider(context)
val model = sourceToLoad?.getGlideModel()
try {
val drawable = withContext(Dispatchers.IO) {
Glide
.with(context)
.asDrawable()
.load(model)
.centerInside()
.customize(options.tintColor) {
apply(RequestOptions().set(CustomOptions.tintColor, it.toArgb()))
}
.submit(options.maxWidth, options.maxHeight)
.get()
}
if (drawable is BitmapDrawable && options.tintColor != null) {
drawable.setTint(options.tintColor.toArgb())
}
return Image(drawable)
} catch (e: Exception) {
throw ImageLoadFailed(e)
}
}
}

View File

@@ -0,0 +1,42 @@
package expo.modules.image
import android.graphics.RectF
fun calcXTranslation(
value: Float,
imageRect: RectF,
viewRect: RectF,
isPercentage: Boolean = false,
isReverse: Boolean = false
): Float = calcTranslation(value, imageRect.width(), viewRect.width(), isPercentage, isReverse)
fun calcYTranslation(
value: Float,
imageRect: RectF,
viewRect: RectF,
isPercentage: Boolean = false,
isReverse: Boolean = false
): Float = calcTranslation(value, imageRect.height(), viewRect.height(), isPercentage, isReverse)
fun calcTranslation(
value: Float,
imageRefValue: Float,
viewRefValue: Float,
isPercentage: Boolean = false,
isReverse: Boolean = false
): Float {
if (isPercentage) {
val finalPercentage = if (isReverse) {
100f - value
} else {
value
}
return (finalPercentage / 100f) * (viewRefValue - imageRefValue)
}
if (isReverse) {
return viewRefValue - imageRefValue - value
}
return value
}

View File

@@ -0,0 +1,350 @@
package expo.modules.image
import android.content.Context
import android.graphics.Point
import android.graphics.drawable.Drawable
import android.util.Log
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowManager
import androidx.annotation.VisibleForTesting
import com.bumptech.glide.RequestManager
import com.bumptech.glide.request.Request
import com.bumptech.glide.request.ThumbnailRequestCoordinator
import com.bumptech.glide.request.target.SizeReadyCallback
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
import com.bumptech.glide.util.Preconditions
import com.bumptech.glide.util.Synthetic
import expo.modules.core.utilities.ifNull
import expo.modules.image.enums.ContentFit
import expo.modules.kotlin.tracing.endAsyncTraceBlock
import java.lang.ref.WeakReference
import kotlin.math.max
/**
* A custom target to provide a smooth transition between multiple drawables.
* It delegates images to the [ExpoImageViewWrapper], where we handle the loaded [Drawable].
* When the target is cleared, we don't do anything. The [ExpoImageViewWrapper] is responsible for
* clearing bitmaps before freeing targets. That may be error-prone, but that is the only way
* of implementing the transition between bitmaps.
*/
class ImageViewWrapperTarget(
private val imageViewHolder: WeakReference<ExpoImageViewWrapper>
) : Target<Drawable> {
/**
* Whether the target has a main, non-placeholder source
*/
var hasSource = false
/**
* Whether the target is used - the asset loaded by it has been drawn in the image view
*/
var isUsed = false
/**
* The main source height where -1 means unknown
*/
var sourceHeight = -1
/**
* The main source width where -1 means unknown
*/
var sourceWidth = -1
/**
* The placeholder height where -1 means unknown
*/
var placeholderHeight = -1
/**
* The placeholder width where -1 means unknown
*/
var placeholderWidth = -1
private var cookie = -1
fun setCookie(newValue: Int) {
endLoadingNewImageTraceBlock()
synchronized(this) {
cookie = newValue
}
}
/**
* The content fit of the placeholder
*/
var placeholderContentFit: ContentFit? = null
private var request: Request? = null
private var sizeDeterminer = SizeDeterminer(imageViewHolder)
private fun endLoadingNewImageTraceBlock() = synchronized(this) {
if (cookie < 0) {
return@synchronized
}
endAsyncTraceBlock(Trace.tag, Trace.loadNewImageBlock, cookie)
cookie = -1
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
// The image view should always be valid. When the view is deallocated, all targets should be
// canceled. Therefore that code shouldn't be called in that case. Instead of crashing, we
// decided to ignore that.
val imageView = imageViewHolder.get().ifNull {
endLoadingNewImageTraceBlock()
Log.w("ExpoImage", "The `ExpoImageViewWrapper` was deallocated, but the target wasn't canceled in time.")
return
}
// The thumbnail and full request are handled in the same way by Glide.
// Here we're checking if the provided resource is the final bitmap or a thumbnail.
val isPlaceholder = if (request is ThumbnailRequestCoordinator) {
(request as? ThumbnailRequestCoordinator)
?.getPrivateFullRequest()
?.isComplete == false
} else {
false
}
if (!isPlaceholder) {
endLoadingNewImageTraceBlock()
}
imageView.onResourceReady(this, resource, isPlaceholder)
}
override fun onStart() = Unit
override fun onStop() = Unit
override fun onDestroy() = Unit
override fun onLoadStarted(placeholder: Drawable?) = Unit
// When loading fails, it's handled by the global listener, therefore that method can be NOOP.
override fun onLoadFailed(errorDrawable: Drawable?) {
endLoadingNewImageTraceBlock()
}
override fun onLoadCleared(placeholder: Drawable?) = Unit
override fun getSize(cb: SizeReadyCallback) {
// If we can't resolve the image, we just return unknown size.
// It shouldn't happen in a production application, because it means that our view was deallocated.
if (imageViewHolder.get() == null) {
cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
return
}
sizeDeterminer.getSize(cb)
}
override fun removeCallback(cb: SizeReadyCallback) {
sizeDeterminer.removeCallback(cb)
}
override fun setRequest(request: Request?) {
this.request = request
}
override fun getRequest() = request
fun clear(requestManager: RequestManager) {
sizeDeterminer.clearCallbacksAndListener()
requestManager.clear(this)
}
}
// Copied from the Glide codebase.
// We modified that to receive a weak ref to our view instead of strong one.
internal class SizeDeterminer(private val imageViewHolder: WeakReference<ExpoImageViewWrapper>) {
private val cbs: MutableList<SizeReadyCallback> = ArrayList()
@Synthetic
var waitForLayout = false
private var layoutListener: SizeDeterminerLayoutListener? = null
private fun notifyCbs(width: Int, height: Int) {
// One or more callbacks may trigger the removal of one or more additional callbacks, so we
// need a copy of the list to avoid a concurrent modification exception. One place this
// happens is when a full request completes from the in memory cache while its thumbnail is
// still being loaded asynchronously. See #2237.
for (cb in ArrayList(cbs)) {
cb.onSizeReady(width, height)
}
}
@Synthetic
fun checkCurrentDimens() {
if (cbs.isEmpty()) {
return
}
val currentWidth = targetWidth
val currentHeight = targetHeight
if (!isViewStateAndSizeValid(currentWidth, currentHeight)) {
return
}
notifyCbs(currentWidth, currentHeight)
clearCallbacksAndListener()
}
fun getSize(cb: SizeReadyCallback) {
val view = imageViewHolder.get() ?: return
val currentWidth = targetWidth
val currentHeight = targetHeight
if (isViewStateAndSizeValid(currentWidth, currentHeight)) {
cb.onSizeReady(currentWidth, currentHeight)
return
}
// We want to notify callbacks in the order they were added and we only expect one or two
// callbacks to be added a time, so a List is a reasonable choice.
if (!cbs.contains(cb)) {
cbs.add(cb)
}
if (layoutListener == null) {
val observer = view.viewTreeObserver
layoutListener = SizeDeterminerLayoutListener(this)
observer.addOnPreDrawListener(layoutListener)
}
}
/**
* The callback may be called anyway if it is removed by another [SizeReadyCallback] or
* otherwise removed while we're notifying the list of callbacks.
*
*
* See #2237.
*/
fun removeCallback(cb: SizeReadyCallback) {
cbs.remove(cb)
}
fun clearCallbacksAndListener() {
// Keep a reference to the layout attachStateListener and remove it here
// rather than having the observer remove itself because the observer
// we add the attachStateListener to will be almost immediately merged into
// another observer and will therefore never be alive. If we instead
// keep a reference to the attachStateListener and remove it here, we get the
// current view tree observer and should succeed.
val observer = imageViewHolder.get()?.viewTreeObserver
if (observer?.isAlive == true) {
observer.removeOnPreDrawListener(layoutListener)
}
layoutListener = null
cbs.clear()
}
private fun isViewStateAndSizeValid(width: Int, height: Int): Boolean {
return isDimensionValid(width) && isDimensionValid(height)
}
private val targetHeight: Int
get() {
val view = imageViewHolder.get() ?: return Target.SIZE_ORIGINAL
val verticalPadding = view.paddingTop + view.paddingBottom
val layoutParams = view.layoutParams
val layoutParamSize = layoutParams?.height ?: PENDING_SIZE
return getTargetDimen(view.height, layoutParamSize, verticalPadding)
}
private val targetWidth: Int
get() {
val view = imageViewHolder.get() ?: return Target.SIZE_ORIGINAL
val horizontalPadding = view.paddingLeft + view.paddingRight
val layoutParams = view.layoutParams
val layoutParamSize = layoutParams?.width ?: PENDING_SIZE
return getTargetDimen(view.width, layoutParamSize, horizontalPadding)
}
private fun getTargetDimen(viewSize: Int, paramSize: Int, paddingSize: Int): Int {
val view = imageViewHolder.get() ?: return Target.SIZE_ORIGINAL
// We consider the View state as valid if the View has non-null layout params and a non-zero
// layout params width and height. This is imperfect. We're making an assumption that View
// parents will obey their child's layout parameters, which isn't always the case.
val adjustedParamSize = paramSize - paddingSize
if (adjustedParamSize > 0) {
return adjustedParamSize
}
// Since we always prefer layout parameters with fixed sizes, even if waitForLayout is true,
// we might as well ignore it and just return the layout parameters above if we have them.
// Otherwise we should wait for a layout pass before checking the View's dimensions.
if (waitForLayout && view.isLayoutRequested) {
return PENDING_SIZE
}
// We also consider the View state valid if the View has a non-zero width and height. This
// means that the View has gone through at least one layout pass. It does not mean the Views
// width and height are from the current layout pass. For example, if a View is re-used in
// RecyclerView or ListView, this width/height may be from an old position. In some cases
// the dimensions of the View at the old position may be different than the dimensions of the
// View in the new position because the LayoutManager/ViewParent can arbitrarily decide to
// change them. Nevertheless, in most cases this should be a reasonable choice.
val adjustedViewSize = viewSize - paddingSize
if (adjustedViewSize > 0) {
return adjustedViewSize
}
// Finally we consider the view valid if the layout parameter size is set to wrap_content.
// It's difficult for Glide to figure out what to do here. Although Target.SIZE_ORIGINAL is a
// coherent choice, it's extremely dangerous because original images may be much too large to
// fit in memory or so large that only a couple can fit in memory, causing OOMs. If users want
// the original image, they can always use .override(Target.SIZE_ORIGINAL). Since wrap_content
// may never resolve to a real size unless we load something, we aim for a square whose length
// is the largest screen size. That way we're loading something and that something has some
// hope of being downsampled to a size that the device can support. We also log a warning that
// tries to explain what Glide is doing and why some alternatives are preferable.
// Since WRAP_CONTENT is sometimes used as a default layout parameter, we always wait for
// layout to complete before using this fallback parameter (ConstraintLayout among others).
if (!view.isLayoutRequested && paramSize == ViewGroup.LayoutParams.WRAP_CONTENT) {
return getMaxDisplayLength(view.context)
}
// If the layout parameters are < padding, the view size is < padding, or the layout
// parameters are set to match_parent or wrap_content and no layout has occurred, we should
// wait for layout and repeat.
return PENDING_SIZE
}
private fun isDimensionValid(size: Int): Boolean {
return size > 0 || size == Target.SIZE_ORIGINAL
}
private class SizeDeterminerLayoutListener(sizeDeterminer: SizeDeterminer) : ViewTreeObserver.OnPreDrawListener {
private val sizeDeterminerRef: WeakReference<SizeDeterminer>
init {
sizeDeterminerRef = WeakReference(sizeDeterminer)
}
override fun onPreDraw(): Boolean {
val sizeDeterminer = sizeDeterminerRef.get()
sizeDeterminer?.checkCurrentDimens()
return true
}
}
companion object {
// Some negative sizes (Target.SIZE_ORIGINAL) are valid, 0 is never valid.
private const val PENDING_SIZE = 0
@VisibleForTesting
var maxDisplayLength: Int? = null
// Use the maximum to avoid depending on the device's current orientation.
@Suppress("DEPRECATION") // We have copied this code from Glide and are waiting for them to remove the deprecated APIs.
private fun getMaxDisplayLength(context: Context): Int {
if (maxDisplayLength == null) {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = Preconditions.checkNotNull(windowManager).defaultDisplay
val displayDimensions = Point()
display.getSize(displayDimensions)
maxDisplayLength = max(displayDimensions.x, displayDimensions.y)
}
return maxDisplayLength!!
}
}
}

View File

@@ -0,0 +1,47 @@
package expo.modules.image
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
import java.util.*
object ResourceIdHelper {
private val idMap = mutableMapOf<String, Int>()
@SuppressLint("DiscouragedApi")
private fun getResourceRawId(context: Context, name: String): Int {
if (name.isEmpty()) {
return -1
}
val normalizedName = name.lowercase(Locale.ROOT).replace("-", "_")
synchronized(this) {
val id = idMap[normalizedName]
if (id != null) {
return id
}
return context
.resources
.getIdentifier(normalizedName, "raw", context.packageName)
.also {
idMap[normalizedName] = it
}
}
}
fun getResourceUri(context: Context, name: String): Uri? {
val drawableUri = ResourceDrawableIdHelper.getResourceDrawableUri(context, name)
if (drawableUri != Uri.EMPTY) {
return drawableUri
}
val resId = getResourceRawId(context, name)
return if (resId > 0) {
Uri.Builder().scheme("res").path(resId.toString()).build()
} else {
null
}
}
}

View File

@@ -0,0 +1,21 @@
package expo.modules.image
import android.util.Log
import com.bumptech.glide.request.Request
import com.bumptech.glide.request.ThumbnailRequestCoordinator
fun ThumbnailRequestCoordinator.getPrivateFullRequest(): Request? {
return getPrivateField("full")
}
private fun <T> ThumbnailRequestCoordinator.getPrivateField(name: String): T? {
return try {
val field = this.javaClass.getDeclaredField(name)
field.isAccessible = true
@Suppress("UNCHECKED_CAST")
field.get(this) as T
} catch (e: Throwable) {
Log.e("ExpoImage", "Couldn't receive the `$name` field", e)
null
}
}

View File

@@ -0,0 +1,11 @@
package expo.modules.image
object Trace {
val tag = "ExpoImage"
val loadNewImageBlock = "load new image"
private var lastCookieValue = 0
fun getNextCookieValue() = synchronized(this) {
lastCookieValue++
}
}

View File

@@ -0,0 +1,20 @@
package expo.modules.image
import com.facebook.yoga.YogaConstants
fun Float.ifYogaUndefinedUse(value: Float) =
if (YogaConstants.isUndefined(this)) {
value
} else {
this
}
inline fun Float.ifYogaDefinedUse(transformFun: (current: Float) -> Float) =
if (YogaConstants.isUndefined(this)) {
this
} else {
transformFun(this)
}
fun makeYogaUndefinedIfNegative(value: Float) =
if (!YogaConstants.isUndefined(value) && value < 0) YogaConstants.UNDEFINED else value

View File

@@ -0,0 +1,32 @@
package expo.modules.image.blurhash
import android.graphics.Bitmap
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import expo.modules.kotlin.exception.CodedException
class BlurhashDecodingFailure(blurHash: String?) : CodedException(
message = "Cannot decode provided blurhash '$blurHash'"
)
class BlurHashFetcher(
private val blurHash: String?,
private val width: Int,
private val height: Int,
private val punch: Float
) : DataFetcher<Bitmap> {
override fun cleanup() = Unit
override fun cancel() = Unit
override fun getDataClass(): Class<Bitmap> = Bitmap::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Bitmap>) {
val bitmap = BlurhashDecoder.decode(blurHash, width, height, punch)
if (bitmap == null) {
callback.onLoadFailed(BlurhashDecodingFailure(blurHash))
return
}
callback.onDataReady(bitmap)
}
}

View File

@@ -0,0 +1,180 @@
package expo.modules.image.blurhash
import android.graphics.Bitmap
import android.graphics.Color
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.withSign
/**
* Copied from https://github.com/woltapp/blurhash.
*/
object BlurhashDecoder {
// cache Math.cos() calculations to improve performance.
// The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps
// the cache is enabled by default, it is recommended to disable it only when just a few images are displayed
private val cacheCosinesX = HashMap<Int, DoubleArray>()
private val cacheCosinesY = HashMap<Int, DoubleArray>()
/**
* Clear calculations stored in memory cache.
* The cache is not big, but will increase when many image sizes are used,
* if the app needs memory it is recommended to clear it.
*/
fun clearCache() {
cacheCosinesX.clear()
cacheCosinesY.clear()
}
/**
* Decode a blur hash into a new bitmap.
*
* @param useCache use in memory cache for the calculated math, reused by images with same size.
* if the cache does not exist yet it will be created and populated with new calculations.
* By default it is true.
*/
fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true): Bitmap? {
if (blurHash == null || blurHash.length < 6) {
return null
}
val numCompEnc = decode83(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
return null
}
val maxAcEnc = decode83(blurHash, 1, 2)
val maxAc = (maxAcEnc + 1) / 166f
val colors = Array(numCompX * numCompY) { i ->
if (i == 0) {
val colorEnc = decode83(blurHash, 2, 6)
decodeDc(colorEnc)
} else {
val from = 4 + i * 2
val colorEnc = decode83(blurHash, from, from + 2)
decodeAc(colorEnc, maxAc * punch)
}
}
return composeBitmap(width, height, numCompX, numCompY, colors, useCache)
}
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
var result = 0
for (i in from until to) {
val index = charMap[str[i]] ?: -1
if (index != -1) {
result = result * 83 + index
}
}
return result
}
private fun decodeDc(colorEnc: Int): FloatArray {
val r = colorEnc shr 16
val g = (colorEnc shr 8) and 255
val b = colorEnc and 255
return floatArrayOf(BlurhashHelpers.srgbToLinear(r), BlurhashHelpers.srgbToLinear(g), BlurhashHelpers.srgbToLinear(b))
}
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
val r = value / (19 * 19)
val g = (value / 19) % 19
val b = value % 19
return floatArrayOf(
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
signedPow2((b - 9) / 9.0f) * maxAc
)
}
private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
private fun composeBitmap(
width: Int,
height: Int,
numCompX: Int,
numCompY: Int,
colors: Array<FloatArray>,
useCache: Boolean
): Bitmap {
// use an array for better performance when writing pixel colors
val imageArray = IntArray(width * height)
val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX)
val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX)
val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY)
val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY)
for (y in 0 until height) {
for (x in 0 until width) {
var r = 0f
var g = 0f
var b = 0f
for (j in 0 until numCompY) {
for (i in 0 until numCompX) {
val cosX = cosinesX.getCos(calculateCosX, i, numCompX, x, width)
val cosY = cosinesY.getCos(calculateCosY, j, numCompY, y, height)
val basis = (cosX * cosY).toFloat()
val color = colors[j * numCompX + i]
r += color[0] * basis
g += color[1] * basis
b += color[2] * basis
}
}
imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))
}
}
return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
}
private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when {
calculate -> {
DoubleArray(height * numCompY).also {
cacheCosinesY[height * numCompY] = it
}
}
else -> {
cacheCosinesY[height * numCompY]!!
}
}
private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when {
calculate -> {
DoubleArray(width * numCompX).also {
cacheCosinesX[width * numCompX] = it
}
}
else -> cacheCosinesX[width * numCompX]!!
}
private fun DoubleArray.getCos(
calculate: Boolean,
x: Int,
numComp: Int,
y: Int,
size: Int
): Double {
if (calculate) {
this[x + numComp * y] = cos(Math.PI * y * x / size)
}
return this[x + numComp * y]
}
private fun linearToSrgb(value: Float): Int {
val v = value.coerceIn(0f, 1f)
return if (v <= 0.0031308f) {
(v * 12.92f * 255f + 0.5f).toInt()
} else {
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
}
}
private val charMap = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
)
.mapIndexed { i, c -> c to i }
.toMap()
}

View File

@@ -0,0 +1,112 @@
package expo.modules.image.blurhash
import android.graphics.Bitmap
import android.graphics.Color
import kotlin.math.*
/**
* Rewritten in kotlin from https://github.com/woltapp/blurhash/blob/master/Swift/BlurHashEncode.swift
*/
object BlurhashEncoder {
fun encode(image: Bitmap, numberOfComponents: Pair<Int, Int>): String {
val pixels = IntArray(image.width * image.height)
image.getPixels(pixels, 0, image.width, 0, 0, image.width, image.height)
val factors = calculateBlurFactors(pixels, image.width, image.height, numberOfComponents)
val dc = factors.first()
val ac = factors.drop(1)
val hashBuilder = StringBuilder()
encodeFlag(numberOfComponents, hashBuilder)
val maximumValue = encodeMaximumValue(ac, hashBuilder)
hashBuilder.append(encode83(encodeDC(dc), 4))
for (factor in ac) {
hashBuilder.append(encode83(encodeAC(factor, maximumValue), 2))
}
return hashBuilder.toString()
}
private fun encodeFlag(numberOfComponents: Pair<Int, Int>, hashBuilder: StringBuilder) {
val sizeFlag = (numberOfComponents.first - 1) + (numberOfComponents.second - 1) * 9
hashBuilder.append(encode83(sizeFlag, 1))
}
private fun encodeMaximumValue(ac: List<Triple<Float, Float, Float>>, hash: StringBuilder): Float {
val maximumValue: Float
if (ac.isNotEmpty()) {
val actualMaximumValue = ac.maxOf { t -> max(max(abs(t.first), abs(t.second)), abs(t.third)) }
val quantisedMaximumValue = max(0f, min(82f, floor(actualMaximumValue * 166f - 0.5f))).toInt()
maximumValue = (quantisedMaximumValue + 1).toFloat() / 166f
hash.append(encode83(quantisedMaximumValue, 1))
} else {
maximumValue = 1f
hash.append(encode83(0, 1))
}
return maximumValue
}
private fun calculateBlurFactors(pixels: IntArray, width: Int, height: Int, numberOfComponents: Pair<Int, Int>): List<Triple<Float, Float, Float>> {
val factors = mutableListOf<Triple<Float, Float, Float>>()
for (y in 0 until numberOfComponents.second) {
for (x in 0 until numberOfComponents.first) {
val normalisation = if (x == 0 && y == 0) 1f else 2f
val factor = multiplyBasisFunction(pixels, width, height, x, y, normalisation)
factors.add(factor)
}
}
return factors
}
private fun encode83(value: Int, length: Int): String {
var result = ""
for (i in 1..length) {
val digit = (value / 83f.pow((length - i).toFloat())) % 83f
result += ENCODE_CHARACTERS[digit.toInt()]
}
return result
}
private const val ENCODE_CHARACTERS =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
private fun encodeDC(value: Triple<Float, Float, Float>): Int {
val roundedR = BlurhashHelpers.linearTosRGB(value.first)
val roundedG = BlurhashHelpers.linearTosRGB(value.second)
val roundedB = BlurhashHelpers.linearTosRGB(value.third)
return (roundedR shl 16) + (roundedG shl 8) + roundedB
}
private fun encodeAC(value: Triple<Float, Float, Float>, maximumValue: Float): Int {
val quantR = max(0f, min(18f, floor(BlurhashHelpers.signPow(value.first / maximumValue, 0.5f) * 9f + 9.5f)))
val quantG = max(0f, min(18f, floor(BlurhashHelpers.signPow(value.second / maximumValue, 0.5f) * 9f + 9.5f)))
val quantB = max(0f, min(18f, floor(BlurhashHelpers.signPow(value.third / maximumValue, 0.5f) * 9f + 9.5f)))
return (quantR * 19f * 19f + quantG * 19f + quantB).toInt()
}
private fun multiplyBasisFunction(pixels: IntArray, width: Int, height: Int, x: Int, y: Int, normalisation: Float): Triple<Float, Float, Float> {
var r = 0f
var g = 0f
var b = 0f
for (j in 0 until height) {
for (i in 0 until width) {
val basis = normalisation * cos(PI.toFloat() * x * i / width) * cos(PI.toFloat() * y * j / height)
val pixel = pixels[i + j * width]
val pr = BlurhashHelpers.srgbToLinear(Color.red(pixel))
val pg = BlurhashHelpers.srgbToLinear(Color.green(pixel))
val pb = BlurhashHelpers.srgbToLinear(Color.blue(pixel))
r += basis * pr
g += basis * pg
b += basis * pb
}
}
val scale = 1f / (width * height)
return Triple(r * scale, g * scale, b * scale)
}
}

View File

@@ -0,0 +1,38 @@
package expo.modules.image.blurhash
import android.graphics.Bitmap
import kotlin.math.*
object BlurhashHelpers {
fun srgbToLinear(colorEnc: Int): Float {
val v = colorEnc / 255f
return if (v <= 0.04045f) {
(v / 12.92f)
} else {
((v + 0.055f) / 1.055f).pow(2.4f)
}
}
fun linearTosRGB(value: Float): Int {
val v = max(0f, min(1f, value))
return if (v <= 0.0031308) {
(v * 12.92 * 255 + 0.5).toInt()
} else {
(1.055 * (v.pow(1f / 2.4f) - 0.055) * 255 + 0.5).toInt()
}
}
fun signPow(value: Float, exp: Float): Float {
return abs(value).pow(exp) * sign(value)
}
fun getBitsPerPixel(bitmap: Bitmap): Int {
return when (bitmap.config) {
Bitmap.Config.ARGB_8888 -> 32
Bitmap.Config.RGB_565 -> 16
Bitmap.Config.ALPHA_8 -> 8
Bitmap.Config.ARGB_4444 -> 16
else -> 0
}
}
}

View File

@@ -0,0 +1,9 @@
package expo.modules.image.blurhash
import android.net.Uri
data class BlurhashModel(
val uri: Uri,
val width: Int,
val height: Int
)

View File

@@ -0,0 +1,29 @@
package expo.modules.image.blurhash
import android.graphics.Bitmap
import android.net.Uri
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.signature.ObjectKey
class BlurhashModelLoader : ModelLoader<BlurhashModel, Bitmap> {
override fun handles(model: BlurhashModel): Boolean = true
override fun buildLoadData(
model: BlurhashModel,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<Bitmap> {
val blurhash = getPath(model.uri, 0, null) { it }
return ModelLoader.LoadData(
ObjectKey(model),
BlurHashFetcher(blurhash, model.width, model.height, 1f)
)
}
private fun <T> getPath(uri: Uri, index: Int, default: T, converter: (String) -> T): T {
val value = uri.pathSegments.getOrNull(index) ?: return default
return converter(value)
}
}

View File

@@ -0,0 +1,13 @@
package expo.modules.image.blurhash
import android.graphics.Bitmap
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
class BlurhashModelLoaderFactory : ModelLoaderFactory<BlurhashModel, Bitmap> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<BlurhashModel, Bitmap> =
BlurhashModelLoader()
override fun teardown() = Unit
}

View File

@@ -0,0 +1,16 @@
package expo.modules.image.blurhash
import android.content.Context
import android.graphics.Bitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.LibraryGlideModule
@GlideModule
class BlurhashModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry)
registry.prepend(BlurhashModel::class.java, Bitmap::class.java, BlurhashModelLoaderFactory())
}
}

View File

@@ -0,0 +1,27 @@
package expo.modules.image.dataurls
import android.util.Base64
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import java.nio.ByteBuffer
class Base64DataFetcher(private val data: String) : DataFetcher<ByteBuffer> {
override fun cleanup() = Unit
override fun cancel() = Unit
override fun getDataClass(): Class<ByteBuffer> = ByteBuffer::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in ByteBuffer>) {
val base64Section = getBase64Section()
val data = Base64.decode(base64Section, Base64.DEFAULT)
val byteBuffer = ByteBuffer.wrap(data)
callback.onDataReady(byteBuffer)
}
private fun getBase64Section(): String {
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs.
val startOfBase64Section = data.indexOf(',')
return data.substring(startOfBase64Section + 1)
}
}

View File

@@ -0,0 +1,24 @@
package expo.modules.image.dataurls
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.signature.ObjectKey
import java.nio.ByteBuffer
/**
* Loads an [java.io.InputStream] from a Base 64 encoded String.
*/
class Base64ModelLoader : ModelLoader<String, ByteBuffer> {
override fun handles(model: String): Boolean {
return model.startsWith("data:")
}
override fun buildLoadData(
model: String,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<ByteBuffer> {
return ModelLoader.LoadData(ObjectKey(model), Base64DataFetcher(model))
}
}

View File

@@ -0,0 +1,12 @@
package expo.modules.image.dataurls
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import java.nio.ByteBuffer
class Base64ModelLoaderFactory : ModelLoaderFactory<String, ByteBuffer> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<String, ByteBuffer> =
Base64ModelLoader()
override fun teardown() = Unit
}

View File

@@ -0,0 +1,16 @@
package expo.modules.image.dataurls
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.LibraryGlideModule
import java.nio.ByteBuffer
@GlideModule
class Base64Module : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry)
registry.prepend(String::class.java, ByteBuffer::class.java, Base64ModelLoaderFactory())
}
}

View File

@@ -0,0 +1,19 @@
package expo.modules.image.decodedsource
import android.graphics.drawable.Drawable
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
class DecodedFetcher(
private val drawable: Drawable
) : DataFetcher<Drawable> {
override fun cleanup() = Unit
override fun cancel() = Unit
override fun getDataClass(): Class<Drawable> = Drawable::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Drawable>) {
callback.onDataReady(drawable)
}
}

View File

@@ -0,0 +1,5 @@
package expo.modules.image.decodedsource
import android.graphics.drawable.Drawable
class DecodedModel(val drawable: Drawable)

View File

@@ -0,0 +1,18 @@
package expo.modules.image.decodedsource
import android.graphics.drawable.Drawable
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.signature.ObjectKey
class DecodedModelLoader : ModelLoader<DecodedModel, Drawable> {
override fun handles(model: DecodedModel): Boolean = true
override fun buildLoadData(
model: DecodedModel,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<Drawable> {
return ModelLoader.LoadData(ObjectKey(model), DecodedFetcher(model.drawable))
}
}

View File

@@ -0,0 +1,13 @@
package expo.modules.image.decodedsource
import android.graphics.drawable.Drawable
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
class DecodedModelLoaderFactory : ModelLoaderFactory<DecodedModel, Drawable> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<DecodedModel, Drawable> =
DecodedModelLoader()
override fun teardown() = Unit
}

View File

@@ -0,0 +1,16 @@
package expo.modules.image.decodedsource
import android.content.Context
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.LibraryGlideModule
@GlideModule
class DecodedModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry)
registry.prepend(DecodedModel::class.java, Drawable::class.java, DecodedModelLoaderFactory())
}
}

View File

@@ -0,0 +1,218 @@
package expo.modules.image.drawing
import android.content.Context
import android.graphics.Canvas
import android.graphics.Outline
import android.graphics.Path
import android.graphics.RectF
import android.os.Build
import android.view.View
import android.view.ViewOutlineProvider
import com.facebook.react.modules.i18nmanager.I18nUtil
import com.facebook.react.uimanager.FloatUtil
import com.facebook.react.uimanager.PixelUtil
import com.facebook.yoga.YogaConstants
import expo.modules.image.ifYogaUndefinedUse
class OutlineProvider(private val mContext: Context) : ViewOutlineProvider() {
enum class BorderRadiusConfig {
ALL,
TOP_LEFT,
TOP_RIGHT,
BOTTOM_RIGHT,
BOTTOM_LEFT,
TOP_START,
TOP_END,
BOTTOM_START,
BOTTOM_END
}
enum class CornerRadius {
TOP_LEFT,
TOP_RIGHT,
BOTTOM_RIGHT,
BOTTOM_LEFT
}
private var mLayoutDirection = View.LAYOUT_DIRECTION_LTR
private val mBounds = RectF()
val borderRadiiConfig = FloatArray(9) { YogaConstants.UNDEFINED }
private val mCornerRadii = FloatArray(4)
private var mCornerRadiiInvalidated = true
private val mConvexPath = Path()
private var mConvexPathInvalidated = true
init {
updateCornerRadiiIfNeeded()
}
private fun updateCornerRadiiIfNeeded() {
if (!mCornerRadiiInvalidated) {
return
}
val isRTL = mLayoutDirection == View.LAYOUT_DIRECTION_RTL
val isRTLSwap = I18nUtil.instance.doLeftAndRightSwapInRTL(mContext)
updateCornerRadius(
CornerRadius.TOP_LEFT,
BorderRadiusConfig.TOP_LEFT,
BorderRadiusConfig.TOP_RIGHT,
BorderRadiusConfig.TOP_START,
BorderRadiusConfig.TOP_END,
isRTL,
isRTLSwap
)
updateCornerRadius(
CornerRadius.TOP_RIGHT,
BorderRadiusConfig.TOP_RIGHT,
BorderRadiusConfig.TOP_LEFT,
BorderRadiusConfig.TOP_END,
BorderRadiusConfig.TOP_START,
isRTL,
isRTLSwap
)
updateCornerRadius(
CornerRadius.BOTTOM_LEFT,
BorderRadiusConfig.BOTTOM_LEFT,
BorderRadiusConfig.BOTTOM_RIGHT,
BorderRadiusConfig.BOTTOM_START,
BorderRadiusConfig.BOTTOM_END,
isRTL,
isRTLSwap
)
updateCornerRadius(
CornerRadius.BOTTOM_RIGHT,
BorderRadiusConfig.BOTTOM_RIGHT,
BorderRadiusConfig.BOTTOM_LEFT,
BorderRadiusConfig.BOTTOM_END,
BorderRadiusConfig.BOTTOM_START,
isRTL,
isRTLSwap
)
mCornerRadiiInvalidated = false
mConvexPathInvalidated = true
}
private fun updateCornerRadius(
outputPosition: CornerRadius,
inputPosition: BorderRadiusConfig,
oppositePosition: BorderRadiusConfig,
startPosition: BorderRadiusConfig,
endPosition: BorderRadiusConfig,
isRTL: Boolean,
isRTLSwap: Boolean
) {
var radius = borderRadiiConfig[inputPosition.ordinal]
if (isRTL) {
if (isRTLSwap) {
radius = borderRadiiConfig[oppositePosition.ordinal]
}
if (YogaConstants.isUndefined(radius)) {
radius = borderRadiiConfig[endPosition.ordinal]
}
} else {
if (YogaConstants.isUndefined(radius)) {
radius = borderRadiiConfig[startPosition.ordinal]
}
}
radius = radius
.ifYogaUndefinedUse(borderRadiiConfig[BorderRadiusConfig.ALL.ordinal])
.ifYogaUndefinedUse(0f)
mCornerRadii[outputPosition.ordinal] = PixelUtil.toPixelFromDIP(radius)
}
private fun updateConvexPathIfNeeded() {
if (!mConvexPathInvalidated) {
return
}
mConvexPath.reset()
mConvexPath.addRoundRect(
mBounds,
floatArrayOf(
mCornerRadii[CornerRadius.TOP_LEFT.ordinal],
mCornerRadii[CornerRadius.TOP_LEFT.ordinal],
mCornerRadii[CornerRadius.TOP_RIGHT.ordinal],
mCornerRadii[CornerRadius.TOP_RIGHT.ordinal],
mCornerRadii[CornerRadius.BOTTOM_RIGHT.ordinal],
mCornerRadii[CornerRadius.BOTTOM_RIGHT.ordinal],
mCornerRadii[CornerRadius.BOTTOM_LEFT.ordinal],
mCornerRadii[CornerRadius.BOTTOM_LEFT.ordinal]
),
Path.Direction.CW
)
mConvexPathInvalidated = false
}
fun hasEqualCorners(): Boolean {
updateCornerRadiiIfNeeded()
val initialCornerRadius = mCornerRadii[0]
return mCornerRadii.all { initialCornerRadius == it }
}
fun setBorderRadius(radius: Float, position: Int): Boolean {
if (!FloatUtil.floatsEqual(borderRadiiConfig[position], radius)) {
borderRadiiConfig[position] = radius
mCornerRadiiInvalidated = true
return true
}
return false
}
private fun updateBoundsAndLayoutDirection(view: View) {
// Update layout direction
val layoutDirection = view.layoutDirection
if (mLayoutDirection != layoutDirection) {
mLayoutDirection = layoutDirection
mCornerRadiiInvalidated = true
}
// Update size
val left = 0
val top = 0
val right = view.width
val bottom = view.height
if (mBounds.left != left.toFloat() ||
mBounds.top != top.toFloat() ||
mBounds.right != right.toFloat() ||
mBounds.bottom != bottom.toFloat()
) {
mBounds[left.toFloat(), top.toFloat(), right.toFloat()] = bottom.toFloat()
mCornerRadiiInvalidated = true
}
}
override fun getOutline(view: View, outline: Outline) {
updateBoundsAndLayoutDirection(view)
// Calculate outline
updateCornerRadiiIfNeeded()
if (hasEqualCorners()) {
val cornerRadius = mCornerRadii[0]
if (cornerRadius > 0) {
outline.setRoundRect(0, 0, mBounds.width().toInt(), mBounds.height().toInt(), cornerRadius)
} else {
outline.setRect(0, 0, mBounds.width().toInt(), mBounds.height().toInt())
}
} else {
// Clipping is not supported when using a convex path, but drawing the elevation
// shadow is. For the particular case, we fallback to canvas clipping in the view
// which is supposed to call `clipCanvasIfNeeded` in its `draw` method.
updateConvexPathIfNeeded()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
outline.setPath(mConvexPath)
} else {
@Suppress("DEPRECATION")
outline.setConvexPath(mConvexPath)
}
}
}
fun clipCanvasIfNeeded(canvas: Canvas, view: View) {
updateBoundsAndLayoutDirection(view)
updateCornerRadiiIfNeeded()
if (!hasEqualCorners()) {
updateConvexPathIfNeeded()
canvas.clipPath(mConvexPath)
}
}
}

View File

@@ -0,0 +1,89 @@
package expo.modules.image.enums
import android.graphics.Matrix
import android.graphics.RectF
import expo.modules.kotlin.types.Enumerable
import kotlin.math.max
/**
* Describes how the image should be resized to fit its container.
* - Note: It mirrors the CSS [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) property.
*/
enum class ContentFit(val value: String) : Enumerable {
/**
* The image is scaled to maintain its aspect ratio while fitting within the container's box.
* The entire image is made to fill the box, while preserving its aspect ratio,
* so the image will be "letterboxed" if its aspect ratio does not match the aspect ratio of the box.
*/
Contain("contain"),
/**
* The image is sized to maintain its aspect ratio while filling the element's entire content box.
* If the image's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit.
*/
Cover("cover"),
/**
* The image is sized to fill the element's content box. The entire object will completely fill the box.
* If the image's aspect ratio does not match the aspect ratio of its box, then the image will be stretched to fit.
*/
Fill("fill"),
/**
* The image is not resized and is centered by default.
* When specified, the exact position can be controlled with `ContentPosition`.
*/
None("none"),
/**
* The image is sized as if `none` or `contain` were specified,
* whichever would result in a smaller concrete image size.
*/
ScaleDown("scale-down");
internal fun toMatrix(imageRect: RectF, viewRect: RectF, sourceWidth: Int, sourceHeight: Int) = Matrix().apply {
when (this@ContentFit) {
Contain -> setRectToRect(imageRect, viewRect, Matrix.ScaleToFit.START)
Cover -> {
val imageWidth = imageRect.width()
val imageHeight = imageRect.height()
val reactWidth = viewRect.width()
val reactHeight = viewRect.height()
val scale = max(reactWidth / imageWidth, reactHeight / imageHeight)
setScale(scale, scale)
}
Fill -> setRectToRect(imageRect, viewRect, Matrix.ScaleToFit.FILL)
None -> {
// we don't need to do anything
}
ScaleDown -> {
// If we have information about the original size of the source, we can resize the image more drastically.
// In certain situations, we may even permit upscaling when we anticipate the image to be reloaded without any reduction in size,
// which will create a seamless transition between various states.
if (sourceWidth != -1 && sourceHeight != -1) {
val sourceRect = RectF(0f, 0f, sourceWidth.toFloat(), sourceHeight.toFloat())
// Rather than checking if the image rectangle is within the bounds of the view rectangle, we verify the original source rectangle.
// We know that the newly loaded image has larger dimensions than the current one, and therefore,
// it will not be downscaled.
if (sourceRect.width() >= viewRect.width() || sourceRect.height() >= viewRect.height()) {
setRectToRect(imageRect, viewRect, Matrix.ScaleToFit.START)
}
// If the source rectangle is larger than the view rectangle and the image rectangle has not been upscaled to match the source rectangle,
// temporary upscaling is necessary to ensure a seamless transition.
// It should be noted that this upscaling is applied to the downscaled version of the image,
// not the original source image, and will be replaced by the original asset shortly thereafter.
else {
setRectToRect(imageRect, sourceRect, Matrix.ScaleToFit.START)
}
} else {
if (imageRect.width() >= viewRect.width() || imageRect.height() >= viewRect.height()) {
setRectToRect(imageRect, viewRect, Matrix.ScaleToFit.START)
}
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
package expo.modules.image.enums
import com.bumptech.glide.load.DataSource
enum class ImageCacheType(private vararg val dataSources: DataSource) {
NONE(DataSource.LOCAL, DataSource.REMOTE),
DISK(DataSource.DATA_DISK_CACHE, DataSource.RESOURCE_DISK_CACHE),
MEMORY(DataSource.MEMORY_CACHE);
companion object {
fun fromNativeValue(value: DataSource): ImageCacheType =
entries.firstOrNull { it.dataSources.contains(value) } ?: NONE
}
}

View File

@@ -0,0 +1,15 @@
package expo.modules.image.enums
import expo.modules.kotlin.types.Enumerable
enum class Priority(val value: String) : Enumerable {
LOW("low"),
NORMAL("normal"),
HIGH("high");
internal fun toGlidePriority(): com.bumptech.glide.Priority = when (this) {
LOW -> com.bumptech.glide.Priority.LOW
NORMAL -> com.bumptech.glide.Priority.NORMAL
HIGH -> com.bumptech.glide.Priority.IMMEDIATE
}
}

View File

@@ -0,0 +1,77 @@
package expo.modules.image.events
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.util.Log
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import expo.modules.image.ExpoImageViewWrapper
import expo.modules.image.enums.ImageCacheType
import expo.modules.image.records.ImageErrorEvent
import expo.modules.image.records.ImageLoadEvent
import expo.modules.image.records.ImageSource
import expo.modules.image.svg.SVGPictureDrawable
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import java.util.Locale
class GlideRequestListener(
private val expoImageViewWrapper: WeakReference<ExpoImageViewWrapper>
) : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean {
val errorMessage = e
?.message
// Glide always append that line to the end of the message.
// It's not possible to call `logRootCauses` from the JS, so we decided to remove it.
?.removeSuffix("\n call GlideException#logRootCauses(String) for more detail")
?: "Unknown error"
expoImageViewWrapper
.get()
?.onError
?.invoke(ImageErrorEvent(errorMessage))
Log.e("ExpoImage", errorMessage)
e?.logRootCauses("ExpoImage")
return false
}
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
val intrinsicWidth = (resource as? SVGPictureDrawable)?.svgIntrinsicWidth
?: resource.intrinsicWidth
val intrinsicHeight = (resource as? SVGPictureDrawable)?.svgIntrinsicHeight
?: resource.intrinsicHeight
val imageWrapper = expoImageViewWrapper.get() ?: return false
val appContext = imageWrapper.appContext
appContext.mainQueue.launch {
imageWrapper.onLoad.invoke(
ImageLoadEvent(
cacheType = ImageCacheType.fromNativeValue(dataSource).name.lowercase(Locale.getDefault()),
source = ImageSource(
url = model.toString(),
width = intrinsicWidth,
height = intrinsicHeight,
mediaType = null, // TODO(@lukmccall): add mediaType
isAnimated = resource is Animatable
)
)
)
}
return false
}
}

View File

@@ -0,0 +1,26 @@
package expo.modules.image.events
import com.facebook.react.modules.network.ProgressListener
import expo.modules.image.ExpoImageViewWrapper
import expo.modules.image.records.ImageProgressEvent
import java.lang.ref.WeakReference
class OkHttpProgressListener(
private val expoImageViewWrapper: WeakReference<ExpoImageViewWrapper>
) : ProgressListener {
override fun onProgress(bytesWritten: Long, contentLength: Long, done: Boolean) {
// OkHttp calls that function twice at the end - when the last byte was downloaded with done set to false,
// and also shortly after, with done set to true. In both cases, the bytesWritten and the contentLength are equal.
// We want to avoid sending two same events to JS, that's why we return when done is set to true.
if (contentLength <= 0 || done) {
return
}
expoImageViewWrapper.get()?.onProgress?.invoke(
ImageProgressEvent(
loaded = bytesWritten.toInt(),
total = contentLength.toInt()
)
)
}
}

View File

@@ -0,0 +1,107 @@
package expo.modules.image.okhttp
import android.content.Context
import android.webkit.CookieManager
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.Headers
import com.bumptech.glide.module.LibraryGlideModule
import expo.modules.image.events.OkHttpProgressListener
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.io.InputStream
/**
* GlideUrl with custom cache key.
* It wraps the base implementation and overrides logic behind cache key and when two
* objects are equal. Typically, Glide uses the only cache key to compare objects.
* It won't suit our use case. We want to make custom cache key transparent and
* not use it to compare objects.
*/
class GlideUrlWithCustomCacheKey(
uri: String?,
headers: Headers?,
private val cacheKey: String
) : GlideUrl(uri, headers) {
/**
* Cached hash code value
*/
private var hashCode = 0
/**
* @return a super cache key from [GlideUrl]
*/
private fun getBaseCacheKey(): String = super.getCacheKey()
override fun getCacheKey(): String = cacheKey
// Mostly copied from GlideUrl::equal
override fun equals(other: Any?): Boolean {
if (other is GlideUrlWithCustomCacheKey) {
return getBaseCacheKey() == other.getBaseCacheKey() && headers.equals(other.headers)
} else if (other is GlideUrl) {
return getBaseCacheKey() == other.cacheKey && headers.equals(other.headers)
}
return false
}
// Mostly copied from GlideUrl::hashCode
override fun hashCode(): Int {
if (hashCode == 0) {
hashCode = getBaseCacheKey().hashCode()
hashCode = 31 * hashCode + headers.hashCode()
}
return hashCode
}
}
/**
* To connect listener with the request we have to create custom model.
* In that way, we're passing more information to the final data loader.
*/
data class GlideUrlWrapper(val glideUrl: GlideUrl) {
var progressListener: OkHttpProgressListener? = null
override fun toString(): String {
return glideUrl.toString()
}
}
private object SharedCookieJar : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val cookieManager = getCookieManager() ?: return
val urlString = url.toString()
for (cookie in cookies) {
cookieManager.setCookie(urlString, cookie.toString())
}
cookieManager.flush()
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookieManager = getCookieManager() ?: return emptyList()
val cookieString = cookieManager.getCookie(url.toString()) ?: return emptyList()
return cookieString.split(";").mapNotNull { Cookie.parse(url, it.trim()) }
}
private fun getCookieManager(): CookieManager? = runCatching {
CookieManager.getInstance()
}.getOrNull()
}
@GlideModule
class ExpoImageOkHttpClientGlideModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val client = OkHttpClient.Builder()
.cookieJar(SharedCookieJar)
.build()
// We don't use the `GlideUrl` directly but we want to replace the default okhttp loader anyway
// to make sure that the app will use only one client.
registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(client))
registry.prepend(GlideUrlWrapper::class.java, InputStream::class.java, GlideUrlWrapperLoader.Factory(client))
}
}

View File

@@ -0,0 +1,55 @@
package expo.modules.image.okhttp
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.facebook.react.modules.network.ProgressResponseBody
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import java.io.InputStream
class GlideUrlWrapperLoader(
private val commonClient: OkHttpClient
) : ModelLoader<GlideUrlWrapper, InputStream> {
override fun buildLoadData(
model: GlideUrlWrapper,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStream>? {
val loader = OkHttpUrlLoader(
commonClient
.newBuilder()
.addInterceptor(
Interceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse
.newBuilder()
.body(
ProgressResponseBody(requireNotNull(originalResponse.body)) { bytesWritten, contentLength, done ->
model.progressListener?.onProgress(bytesWritten, contentLength, done)
}
)
.build()
}
)
.build()
)
return loader.buildLoadData(model.glideUrl, width, height, options)
}
// The default http loader always returns true.
override fun handles(model: GlideUrlWrapper): Boolean = true
class Factory(
private val commonClient: OkHttpClient
) : ModelLoaderFactory<GlideUrlWrapper, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrlWrapper, InputStream> =
GlideUrlWrapperLoader(commonClient)
override fun teardown() = Unit
}
}

View File

@@ -0,0 +1,10 @@
package expo.modules.image.records
import expo.modules.kotlin.types.Enumerable
enum class CachePolicy(val value: String) : Enumerable {
NONE("none"),
DISK("disk"),
MEMORY("memory"),
MEMORY_AND_DISK("memory-disk")
}

View File

@@ -0,0 +1,89 @@
package expo.modules.image.records
import android.graphics.Matrix
import android.graphics.RectF
import expo.modules.image.calcXTranslation
import expo.modules.image.calcYTranslation
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
/**
* Represents a position value that might be either `Double` or `String`.
* TODO(@lukmccall): Use `Either` instead of `Any`
*/
typealias ContentPositionValue = Any
private typealias CalcAxisOffset = (
value: Float,
imageRect: RectF,
viewRect: RectF,
isPercentage: Boolean,
isReverse: Boolean
) -> Float
class ContentPosition : Record {
@Field
val top: ContentPositionValue? = null
@Field
val bottom: ContentPositionValue? = null
@Field
val right: ContentPositionValue? = null
@Field
val left: ContentPositionValue? = null
private fun ContentPositionValue?.calcOffset(
isReverse: Boolean,
imageRect: RectF,
viewRect: RectF,
calcAxisOffset: CalcAxisOffset
): Float? {
if (this == null) {
return null
}
return if (this is Double) {
val value = this.toFloat()
calcAxisOffset(value, imageRect, viewRect, false, isReverse)
} else {
val value = this as String
if (value == "center") {
calcAxisOffset(50f, imageRect, viewRect, true, isReverse)
} else {
calcAxisOffset(value.removeSuffix("%").toFloat(), imageRect, viewRect, true, isReverse)
}
}
}
private fun offsetX(
imageRect: RectF,
viewRect: RectF
): Float {
return left.calcOffset(false, imageRect, viewRect, ::calcXTranslation)
?: right.calcOffset(true, imageRect, viewRect, ::calcXTranslation)
?: calcXTranslation(50f, imageRect, viewRect, isPercentage = true) // default value
}
private fun offsetY(
imageRect: RectF,
viewRect: RectF
): Float {
return top.calcOffset(false, imageRect, viewRect, ::calcYTranslation)
?: bottom.calcOffset(true, imageRect, viewRect, ::calcYTranslation)
?: calcYTranslation(50f, imageRect, viewRect, isPercentage = true) // default value
}
internal fun apply(to: Matrix, imageRect: RectF, viewRect: RectF) {
val xOffset = offsetX(imageRect, viewRect)
val yOffset = offsetY(imageRect, viewRect)
to.postTranslate(xOffset, yOffset)
}
companion object {
val center = ContentPosition()
}
}

View File

@@ -0,0 +1,22 @@
package expo.modules.image.records
import expo.modules.kotlin.types.Enumerable
enum class DecodeFormat(val value: String) : Enumerable {
ARGB_8888("argb"),
RGB_565("rgb");
fun toGlideFormat(): com.bumptech.glide.load.DecodeFormat {
return when (this) {
ARGB_8888 -> com.bumptech.glide.load.DecodeFormat.PREFER_ARGB_8888
RGB_565 -> com.bumptech.glide.load.DecodeFormat.PREFER_RGB_565
}
}
fun toBytes(): Int {
return when (this) {
ARGB_8888 -> 4
RGB_565 -> 2
}
}
}

View File

@@ -0,0 +1,15 @@
package expo.modules.image.records
import android.graphics.Color
import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
data class ImageLoadOptions(
@Field
val maxWidth: Int = SIZE_ORIGINAL,
@Field
val maxHeight: Int = SIZE_ORIGINAL,
@Field
val tintColor: Color? = null
) : Record

View File

@@ -0,0 +1,8 @@
package expo.modules.image.records
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
data class ImageTransition(
@Field val duration: Int = 0
) : Record

View File

@@ -0,0 +1,192 @@
package expo.modules.image.records
import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.Headers
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ApplicationVersionSignature
import expo.modules.image.BlurhashModelProvider
import expo.modules.image.DecodedModelProvider
import expo.modules.image.GlideModelProvider
import expo.modules.image.RawModelProvider
import expo.modules.image.ThumbhashModelProvider
import expo.modules.image.UriModelProvider
import expo.modules.image.UrlModelProvider
import expo.modules.image.ResourceIdHelper
import expo.modules.image.customize
import expo.modules.image.okhttp.GlideUrlWithCustomCacheKey
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
sealed interface Source {
val width: Int
val height: Int
val scale: Double
val pixelCount: Double
get() = width * height * scale * scale
fun createGlideModelProvider(context: Context): GlideModelProvider?
fun createGlideOptions(context: Context): RequestOptions
/**
* Whether it should use placeholder content fit when used as a placeholder
*/
fun usesPlaceholderContentFit(): Boolean = true
}
class DecodedSource(
val drawable: Drawable
) : Source {
override fun createGlideModelProvider(context: Context): GlideModelProvider {
return DecodedModelProvider(drawable)
}
override val width: Int = drawable.intrinsicWidth
override val height: Int = drawable.intrinsicHeight
override val scale: Double = 1.0
override fun createGlideOptions(context: Context): RequestOptions {
// We don't want to cache already decoded images.
return RequestOptions()
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
}
}
data class SourceMap(
@Field val uri: String? = null,
@Field override val width: Int = 0,
@Field override val height: Int = 0,
@Field override val scale: Double = 1.0,
@Field val headers: Map<String, String>? = null,
@Field val cacheKey: String? = null
) : Source, Record {
private var parsedUri: Uri? = null
private fun isDataUrl() = parsedUri?.scheme?.startsWith("data") ?: false
private fun isContentUrl() = parsedUri?.scheme?.startsWith("content") ?: false
private fun isResourceUri() = parsedUri?.scheme?.startsWith("android.resource") ?: false
private fun isLocalResourceUri() = parsedUri?.scheme?.startsWith("res") ?: false
private fun isLocalFileUri() = parsedUri?.scheme?.startsWith("file") ?: false
private fun isBlurhash() = parsedUri?.scheme?.startsWith("blurhash") ?: false
private fun isThumbhash() = parsedUri?.scheme?.startsWith("thumbhash") ?: false
override fun usesPlaceholderContentFit(): Boolean {
return !isBlurhash() && !isThumbhash()
}
private fun parseUri(context: Context) {
if (parsedUri == null) {
parsedUri = computeUri(context)
}
}
override fun createGlideModelProvider(context: Context): GlideModelProvider? {
if (uri.isNullOrBlank()) {
return null
}
parseUri(context)
if (isContentUrl() || isDataUrl()) {
return RawModelProvider(uri)
}
if (isBlurhash()) {
return BlurhashModelProvider(
parsedUri!!,
width,
height
)
}
if (isThumbhash()) {
return ThumbhashModelProvider(
parsedUri!!
)
}
if (isResourceUri()) {
return UriModelProvider(parsedUri!!)
}
if (isLocalResourceUri()) {
return UriModelProvider(
// Convert `res:/` scheme to `android.resource://`.
// Otherwise, glide can't understand the Uri.
Uri.parse(parsedUri!!.toString().replace("res:/", "android.resource://" + context.packageName + "/"))
)
}
if (isLocalFileUri()) {
return RawModelProvider(parsedUri!!.toString())
}
val glideUrl = if (cacheKey == null) {
GlideUrl(uri, getCustomHeaders())
} else {
GlideUrlWithCustomCacheKey(uri, getCustomHeaders(), cacheKey)
}
return UrlModelProvider(glideUrl)
}
override fun createGlideOptions(context: Context): RequestOptions {
parseUri(context)
return RequestOptions().customize(`when` = width != 0 && height != 0) {
// Override the size for local assets (apart from SVGs). This ensures that
// resizeMode "center" displays the image in the correct size.
override((width * scale).toInt(), (height * scale).toInt())
}.customize(`when` = isResourceUri()) {
// Every local resource (drawable) in Android has its own unique numeric id, which are
// generated at build time. Although these ids are unique, they are not guaranteed unique
// across builds. The underlying glide implementation caches these resources. To make
// sure the cache does not return the wrong image, we should clear the cache when the
// application version changes.
apply(RequestOptions.signatureOf(ApplicationVersionSignature.obtain(context)))
}
}
private fun getCustomHeaders(): Headers {
if (headers == null) {
return LazyHeaders.DEFAULT
}
return LazyHeaders
.Builder()
.apply {
headers.forEach { (key, value) ->
addHeader(key, value)
}
}
.build()
}
private fun computeUri(context: Context): Uri? {
val stringUri = uri ?: return null
return try {
val uri: Uri = Uri.parse(stringUri)
// Verify scheme is set, so that relative uri (used by static resources) are not handled.
if (uri.scheme == null) {
computeLocalUri(stringUri, context)
} else {
uri
}
} catch (e: Exception) {
computeLocalUri(stringUri, context)
}
}
private fun computeLocalUri(stringUri: String, context: Context): Uri? {
return ResourceIdHelper.getResourceUri(context, stringUri)
}
}

View File

@@ -0,0 +1,26 @@
package expo.modules.image.records
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
data class ImageSource(
@Field val url: String,
@Field val width: Int,
@Field val height: Int,
@Field val mediaType: String?,
@Field val isAnimated: Boolean
) : Record
data class ImageLoadEvent(
@Field val cacheType: String,
@Field val source: ImageSource
) : Record
data class ImageProgressEvent(
@Field val loaded: Int,
@Field val total: Int
) : Record
data class ImageErrorEvent(
@Field val error: String
) : Record

View File

@@ -0,0 +1,42 @@
package expo.modules.image.svg
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.resource.SimpleResource
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
import java.io.IOException
import java.io.InputStream
/**
* Decodes an SVG internal representation from an [InputStream].
*
* Copied from https://github.com/bumptech/glide/blob/10acc31a16b4c1b5684f69e8de3117371dfa77a8/samples/svg/src/main/java/com/bumptech/glide/samples/svg/SvgDecoder.java
* and rewritten to Kotlin.
*/
class SVGDecoder : ResourceDecoder<InputStream, SVG> {
// TODO: Can we tell?
override fun handles(source: InputStream, options: Options) = true
@Throws(IOException::class)
override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<SVG>? {
return try {
val svg: SVG = SVG.getFromInputStream(source)
// Use document width and height if view box is not set.
// Later, we will override the document width and height with the dimensions of the native view.
if (svg.documentViewBox == null) {
val documentWidth = svg.documentWidth
val documentHeight = svg.documentHeight
if (documentWidth != -1f && documentHeight != -1f) {
svg.setDocumentViewBox(0f, 0f, documentWidth, documentHeight)
}
}
svg.documentWidth = width.toFloat()
svg.documentHeight = height.toFloat()
SimpleResource(svg)
} catch (ex: SVGParseException) {
throw IOException("Cannot load SVG from stream", ex)
}
}
}

View File

@@ -0,0 +1,49 @@
package expo.modules.image.svg
import android.content.Context
import android.graphics.Picture
import android.graphics.drawable.Drawable
import android.graphics.drawable.PictureDrawable
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.resource.SimpleResource
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.applyTintColor
import expo.modules.image.CustomOptions
/**
* We have to use the intrinsicWidth/Height from the Picture to render the image at a high enough resolution, but at the same time we want to return the actual
* preferred width and height of the SVG to JS. This class allows us to do that.
*/
class SVGPictureDrawable(picture: Picture, val svgIntrinsicWidth: Int, val svgIntrinsicHeight: Int) : PictureDrawable(picture)
/**
* Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]).
*
* Copied from https://github.com/bumptech/glide/blob/10acc31a16b4c1b5684f69e8de3117371dfa77a8/samples/svg/src/main/java/com/bumptech/glide/samples/svg/SvgDrawableTranscoder.java
* and rewritten to Kotlin.
*/
class SVGDrawableTranscoder(val context: Context) : ResourceTranscoder<SVG?, Drawable> {
override fun transcode(toTranscode: Resource<SVG?>, options: Options): Resource<Drawable> {
val svgData = toTranscode.get()
// If the svg doesn't have a viewBox, we can't determine its intrinsic width and height, so we default to 512x512.
// Same dimensions are used in the AndroidSVG library when the viewBox is not set.
val intrinsicWidth = svgData.documentViewBox?.width()?.toInt() ?: 512
val intrinsicHeight = svgData.documentViewBox?.height()?.toInt() ?: 512
val tintColor = options.get(CustomOptions.tintColor)
if (tintColor != null) {
applyTintColor(svgData, tintColor)
}
val picture = SVGPictureDrawable(
svgData.renderToPicture(),
intrinsicWidth,
intrinsicHeight
)
return SimpleResource(
picture
)
}
}

View File

@@ -0,0 +1,26 @@
package expo.modules.image.svg
import android.content.Context
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.LibraryGlideModule
import com.caverock.androidsvg.SVG
import java.io.InputStream
/**
* [LibraryGlideModule] registering support for SVG to Glide.
*
* Copied from https://github.com/bumptech/glide/blob/10acc31a16b4c1b5684f69e8de3117371dfa77a8/samples/svg/src/main/java/com/bumptech/glide/samples/svg/SvgModule.java
* and rewritten to Kotlin.
*/
@GlideModule
class SVGModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry)
registry
.append(InputStream::class.java, SVG::class.java, SVGDecoder())
.register(SVG::class.java, Drawable::class.java, SVGDrawableTranscoder(context))
}
}

View File

@@ -0,0 +1,222 @@
package expo.modules.image.thumbhash
import android.graphics.Bitmap
import android.graphics.Color
// ThumbHash Java implementation (converted to kotlin) thanks to @evanw https://github.com/evanw/thumbhash
object ThumbhashDecoder {
/**
* Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
*
* @param hash The bytes of the ThumbHash.
* @return The width, height, and pixels of the rendered placeholder image.
*/
fun thumbHashToRGBA(hash: ByteArray): Image {
// Read the constants
val header24 = hash[0].toInt() and 255 or (hash[1].toInt() and 255 shl 8) or (hash[2].toInt() and 255 shl 16)
val header16 = hash[3].toInt() and 255 or (hash[4].toInt() and 255 shl 8)
val l_dc = (header24 and 63).toFloat() / 63.0f
val p_dc = (header24 shr 6 and 63).toFloat() / 31.5f - 1.0f
val q_dc = (header24 shr 12 and 63).toFloat() / 31.5f - 1.0f
val l_scale = (header24 shr 18 and 31).toFloat() / 31.0f
val hasAlpha = header24 shr 23 != 0
val p_scale = (header16 shr 3 and 63).toFloat() / 63.0f
val q_scale = (header16 shr 9 and 63).toFloat() / 63.0f
val isLandscape = header16 shr 15 != 0
val lx = Math.max(3, if (isLandscape) if (hasAlpha) 5 else 7 else header16 and 7)
val ly = Math.max(3, if (isLandscape) header16 and 7 else if (hasAlpha) 5 else 7)
val a_dc = if (hasAlpha) (hash[5].toInt() and 15).toFloat() / 15.0f else 1.0f
val a_scale = (hash[5].toInt() shr 4 and 15).toFloat() / 15.0f
// Read the varying factors (boost saturation by 1.25x to compensate for quantization)
val ac_start = if (hasAlpha) 6 else 5
var ac_index = 0
val l_channel = Channel(lx, ly)
val p_channel = Channel(3, 3)
val q_channel = Channel(3, 3)
var a_channel: Channel? = null
ac_index = l_channel.decode(hash, ac_start, ac_index, l_scale)
ac_index = p_channel.decode(hash, ac_start, ac_index, p_scale * 1.25f)
ac_index = q_channel.decode(hash, ac_start, ac_index, q_scale * 1.25f)
if (hasAlpha) {
a_channel = Channel(5, 5)
a_channel.decode(hash, ac_start, ac_index, a_scale)
}
val l_ac = l_channel.ac
val p_ac = p_channel.ac
val q_ac = q_channel.ac
val a_ac = if (hasAlpha) a_channel!!.ac else null
// Decode using the DCT into RGB
val ratio = thumbHashToApproximateAspectRatio(hash)
val w = Math.round(if (ratio > 1.0f) 32.0f else 32.0f * ratio)
val h = Math.round(if (ratio > 1.0f) 32.0f / ratio else 32.0f)
val rgba = ByteArray(w * h * 4)
val cx_stop = Math.max(lx, if (hasAlpha) 5 else 3)
val cy_stop = Math.max(ly, if (hasAlpha) 5 else 3)
val fx = FloatArray(cx_stop)
val fy = FloatArray(cy_stop)
var y = 0
var i = 0
while (y < h) {
var x = 0
while (x < w) {
var l = l_dc
var p = p_dc
var q = q_dc
var a = a_dc
// Precompute the coefficients
for (cx in 0 until cx_stop) fx[cx] = Math.cos(Math.PI / w * (x + 0.5f) * cx).toFloat()
for (cy in 0 until cy_stop) fy[cy] = Math.cos(Math.PI / h * (y + 0.5f) * cy).toFloat()
// Decode L
run {
var cy = 0
var j = 0
while (cy < ly) {
val fy2 = fy[cy] * 2.0f
var cx = if (cy > 0) 0 else 1
while (cx * ly < lx * (ly - cy)) {
l += l_ac[j] * fx[cx] * fy2
cx++
j++
}
cy++
}
}
// Decode P and Q
var cy = 0
var j = 0
while (cy < 3) {
val fy2 = fy[cy] * 2.0f
var cx = if (cy > 0) 0 else 1
while (cx < 3 - cy) {
val f = fx[cx] * fy2
p += p_ac[j] * f
q += q_ac[j] * f
cx++
j++
}
cy++
}
// Decode A
if (hasAlpha) {
var cyAlpha = 0
var k = 0
while (cyAlpha < 5) {
val fy2 = fy[cyAlpha] * 2.0f
var cx = if (cyAlpha > 0) 0 else 1
while (cx < 5 - cyAlpha) {
a += a_ac!![k] * fx[cx] * fy2
cx++
k++
}
cyAlpha++
}
}
// Convert to RGB
val b = l - 2.0f / 3.0f * p
val r = (3.0f * l - b + q) / 2.0f
val g = r - q
rgba[i] = Math.max(0, Math.round(255.0f * Math.min(1f, r))).toByte()
rgba[i + 1] = Math.max(0, Math.round(255.0f * Math.min(1f, g))).toByte()
rgba[i + 2] = Math.max(0, Math.round(255.0f * Math.min(1f, b))).toByte()
rgba[i + 3] = Math.max(0, Math.round(255.0f * Math.min(1f, a))).toByte()
x++
i += 4
}
y++
}
return Image(w, h, rgba)
}
/**
* Converts a ThumbHash into a Bitmap image
*/
fun thumbHashToBitmap(hash: ByteArray): Bitmap {
val thumbhashImage = thumbHashToRGBA(hash)
// TODO: @behenate it should be possible to replace all of the code below with
// with BitmapFactory.decodeByteArray but it always returns null when using thumbhashImage.rgba
val imageArray = IntArray(thumbhashImage.width * thumbhashImage.height)
val thumbhashImageInt = thumbhashImage.rgba.map { it.toUByte().toInt() }
for (i in thumbhashImageInt.indices step 4) {
imageArray[i / 4] = Color.argb(
thumbhashImageInt[i + 3],
thumbhashImageInt[i],
thumbhashImageInt[i + 1],
thumbhashImageInt[i + 2]
)
}
return Bitmap.createBitmap(imageArray, thumbhashImage.width, thumbhashImage.height, Bitmap.Config.ARGB_8888)
}
/**
* Extracts the average color from a ThumbHash. RGB is not be premultiplied by A.
*
* @param hash The bytes of the ThumbHash.
* @return The RGBA values for the average color. Each value ranges from 0 to 1.
*/
fun thumbHashToAverageRGBA(hash: ByteArray): RGBA {
val header = hash[0].toInt() and 255 or (hash[1].toInt() and 255 shl 8) or (hash[2].toInt() and 255 shl 16)
val l = (header and 63).toFloat() / 63.0f
val p = (header shr 6 and 63).toFloat() / 31.5f - 1.0f
val q = (header shr 12 and 63).toFloat() / 31.5f - 1.0f
val hasAlpha = header shr 23 != 0
val a = if (hasAlpha) (hash[5].toInt() and 15).toFloat() / 15.0f else 1.0f
val b = l - 2.0f / 3.0f * p
val r = (3.0f * l - b + q) / 2.0f
val g = r - q
return RGBA(
Math.max(0f, Math.min(1f, r)),
Math.max(0f, Math.min(1f, g)),
Math.max(0f, Math.min(1f, b)),
a
)
}
/**
* Extracts the approximate aspect ratio of the original image.
*
* @param hash The bytes of the ThumbHash.
* @return The approximate aspect ratio (i.e. width / height).
*/
fun thumbHashToApproximateAspectRatio(hash: ByteArray): Float {
val header = hash[3]
val hasAlpha = hash[2].toInt() and 0x80 != 0
val isLandscape = hash[4].toInt() and 0x80 != 0
val lx = if (isLandscape) if (hasAlpha) 5 else 7 else header.toInt() and 7
val ly = if (isLandscape) header.toInt() and 7 else if (hasAlpha) 5 else 7
return lx.toFloat() / ly.toFloat()
}
class Image(var width: Int, var height: Int, var rgba: ByteArray)
class RGBA(var r: Float, var g: Float, var b: Float, var a: Float)
private class Channel(nx: Int, ny: Int) {
var ac: FloatArray
init {
var n = 0
for (cy in 0 until ny) {
var cx = if (cy > 0) 0 else 1
while (cx * ny < nx * (ny - cy)) {
n++
cx++
}
}
ac = FloatArray(n)
}
fun decode(hash: ByteArray, start: Int, index: Int, scale: Float): Int {
var currentIndex = index
for (i in ac.indices) {
val data = hash[start + (currentIndex shr 1)].toInt() shr (currentIndex and 1 shl 2)
ac[i] = ((data and 15).toFloat() / 7.5f - 1.0f) * scale
currentIndex++
}
return currentIndex
}
}
}

View File

@@ -0,0 +1,184 @@
package expo.modules.image.thumbhash
import android.graphics.Bitmap
import android.graphics.Color
// ThumbHash Java implementation (converted to kotlin) thanks to @evanw https://github.com/evanw/thumbhash
object ThumbhashEncoder {
/**
* Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A.
* @param bitmap The bitmap to generate the ThumbHash from.
* @return The ThumbHash as a byte array.
*/
fun encode(bitmap: Bitmap): ByteArray {
// Encoding an image larger than 100x100 is slow with no benefit
val resizedBitmap = resizeKeepingAspectRatio(bitmap, 100)
val w = resizedBitmap.width
val h = resizedBitmap.height
val pixels = IntArray(w * h)
resizedBitmap.getPixels(pixels, 0, w, 0, 0, w, h)
var avg_r = 0f
var avg_g = 0f
var avg_b = 0f
var avg_a = 0f
var i = 0
while (i < w * h) {
val alpha = Color.alpha(pixels[i]) / 255.0f
avg_r += alpha / 255.0f * Color.red(pixels[i])
avg_g += alpha / 255.0f * Color.green(pixels[i])
avg_b += alpha / 255.0f * Color.blue(pixels[i])
avg_a += alpha
i++
}
if (avg_a > 0) {
avg_r /= avg_a
avg_g /= avg_a
avg_b /= avg_a
}
val hasAlpha = avg_a < w * h
val l_limit = if (hasAlpha) 5 else 7 // Use fewer luminance bits if there's alpha
val lx = Math.max(1, Math.round((l_limit * w).toFloat() / Math.max(w, h).toFloat()))
val ly = Math.max(1, Math.round((l_limit * h).toFloat() / Math.max(w, h).toFloat()))
val l = FloatArray(w * h) // luminance
val p = FloatArray(w * h) // yellow - blue
val q = FloatArray(w * h) // red - green
val a = FloatArray(w * h) // alpha
// Convert the image from RGBA to LPQA (composite atop the average color)
i = 0
while (i < w * h) {
val alpha = (Color.alpha(pixels[i]) and 255) / 255.0f
val r = avg_r * (1.0f - alpha) + alpha / 255.0f * Color.red(pixels[i])
val g = avg_g * (1.0f - alpha) + alpha / 255.0f * Color.green(pixels[i])
val b = avg_b * (1.0f - alpha) + alpha / 255.0f * Color.blue(pixels[i])
l[i] = (r + g + b) / 3.0f
p[i] = (r + g) / 2.0f - b
q[i] = r - g
a[i] = alpha
i++
}
// Encode using the DCT into DC (constant) and normalized AC (varying) terms
val l_channel = Channel(Math.max(3, lx), Math.max(3, ly)).encode(w, h, l)
val p_channel = Channel(3, 3).encode(w, h, p)
val q_channel = Channel(3, 3).encode(w, h, q)
val a_channel = if (hasAlpha) Channel(5, 5).encode(w, h, a) else null
// Write the constants
val isLandscape = w > h
val header24 = (
Math.round(63.0f * l_channel.dc)
or (Math.round(31.5f + 31.5f * p_channel.dc) shl 6)
or (Math.round(31.5f + 31.5f * q_channel.dc) shl 12)
or (Math.round(31.0f * l_channel.scale) shl 18)
or if (hasAlpha) 1 shl 23 else 0
)
val header16 = (
(if (isLandscape) ly else lx)
or (Math.round(63.0f * p_channel.scale) shl 3)
or (Math.round(63.0f * q_channel.scale) shl 9)
or if (isLandscape) 1 shl 15 else 0
)
val ac_start = if (hasAlpha) 6 else 5
val ac_count = (
l_channel.ac.size + p_channel.ac.size + q_channel.ac.size +
if (hasAlpha) a_channel!!.ac.size else 0
)
val hash = ByteArray(ac_start + (ac_count + 1) / 2)
hash[0] = header24.toByte()
hash[1] = (header24 shr 8).toByte()
hash[2] = (header24 shr 16).toByte()
hash[3] = header16.toByte()
hash[4] = (header16 shr 8).toByte()
if (hasAlpha) {
hash[5] = (
Math.round(15.0f * a_channel!!.dc)
or (Math.round(15.0f * a_channel.scale) shl 4)
).toByte()
}
// Write the varying factors
var ac_index = 0
ac_index = l_channel.writeTo(hash, ac_start, ac_index)
ac_index = p_channel.writeTo(hash, ac_start, ac_index)
ac_index = q_channel.writeTo(hash, ac_start, ac_index)
if (hasAlpha) a_channel!!.writeTo(hash, ac_start, ac_index)
return hash
}
private fun resizeKeepingAspectRatio(bitmap: Bitmap, maxSize: Int): Bitmap {
val width = bitmap.width
val height = bitmap.height
val ratio = width.toFloat() / height.toFloat()
val newWidth: Int
val newHeight: Int
if (ratio > 1) {
newWidth = maxSize
newHeight = (maxSize / ratio).toInt()
} else {
newHeight = maxSize
newWidth = (maxSize * ratio).toInt()
}
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
}
private class Channel(var nx: Int, var ny: Int) {
var dc = 0f
var ac: FloatArray
var scale = 0f
init {
var n = 0
for (cy in 0 until ny) {
var cx = if (cy > 0) 0 else 1
while (cx * ny < nx * (ny - cy)) {
n++
cx++
}
}
ac = FloatArray(n)
}
fun encode(w: Int, h: Int, channel: FloatArray): Channel {
var n = 0
val fx = FloatArray(w)
for (cy in 0 until ny) {
var cx = 0
while (cx * ny < nx * (ny - cy)) {
var f = 0f
for (x in 0 until w) fx[x] = Math.cos(Math.PI / w * cx * (x + 0.5f)).toFloat()
for (y in 0 until h) {
val fy = Math.cos(Math.PI / h * cy * (y + 0.5f)).toFloat()
for (x in 0 until w) f += channel[x + y * w] * fx[x] * fy
}
f /= (w * h).toFloat()
if (cx > 0 || cy > 0) {
ac[n++] = f
scale = Math.max(scale, Math.abs(f))
} else {
dc = f
}
cx++
}
}
if (scale > 0) for (i in ac.indices) ac[i] = 0.5f + 0.5f / scale * ac[i]
return this
}
fun writeTo(hash: ByteArray, start: Int, index: Int): Int {
var currentIndex = index
for (v in ac) {
hash[start + (currentIndex shr 1)] = (hash[start + (currentIndex shr 1)].toInt() or (Math.round(15.0f * v) shl (currentIndex and 1 shl 2))).toByte()
currentIndex++
}
return currentIndex
}
}
}

View File

@@ -0,0 +1,31 @@
package expo.modules.image.thumbhash
import android.graphics.Bitmap
import android.util.Base64
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import expo.modules.kotlin.exception.CodedException
class ThumbhashDecodingFailure(thumbhash: String?, cause: Exception?) : CodedException(
message = "Cannot decode provided thumbhash '$thumbhash' $cause"
)
class ThumbhashFetcher(
private val thumbhash: String?
) : DataFetcher<Bitmap> {
override fun cleanup() = Unit
override fun cancel() = Unit
override fun getDataClass(): Class<Bitmap> = Bitmap::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Bitmap>) {
try {
val decodedThumbhash = Base64.decode(thumbhash, Base64.DEFAULT)
val bitmap = ThumbhashDecoder.thumbHashToBitmap(decodedThumbhash)
callback.onDataReady(bitmap)
} catch (e: Exception) {
callback.onLoadFailed(ThumbhashDecodingFailure(thumbhash, e))
}
}
}

View File

@@ -0,0 +1,7 @@
package expo.modules.image.thumbhash
import android.net.Uri
data class ThumbhashModel(
val uri: Uri
)

View File

@@ -0,0 +1,24 @@
package expo.modules.image.thumbhash
import android.graphics.Bitmap
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.signature.ObjectKey
class ThumbhashModelLoader : ModelLoader<ThumbhashModel, Bitmap> {
override fun handles(model: ThumbhashModel): Boolean = true
override fun buildLoadData(model: ThumbhashModel, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
// The URI looks like this: thumbhash:/3OcRJYB4d3h\iIeHeEh3eIhw+j2w
// ThumbHash may include slashes which could break the structure of the URL, so we replace them
// with backslashes on the JS side and revert them back to slashes here, before generating the image.
val thumbhash = model.uri.pathSegments.joinToString(separator = "/").replace("\\", "/")
return ModelLoader.LoadData(
ObjectKey(model),
ThumbhashFetcher(thumbhash)
)
}
}

View File

@@ -0,0 +1,13 @@
package expo.modules.image.thumbhash
import android.graphics.Bitmap
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
class ThumbhashModelLoaderFactory : ModelLoaderFactory<ThumbhashModel, Bitmap> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<ThumbhashModel, Bitmap> =
ThumbhashModelLoader()
override fun teardown() = Unit
}

View File

@@ -0,0 +1,16 @@
package expo.modules.image.thumbhash
import android.content.Context
import android.graphics.Bitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.LibraryGlideModule
@GlideModule
class ThumbhashModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry)
registry.prepend(ThumbhashModel::class.java, Bitmap::class.java, ThumbhashModelLoaderFactory())
}
}

1
node_modules/expo-image/app.plugin.js generated vendored Normal file
View File

@@ -0,0 +1 @@
module.exports = require('./plugin/build/withExpoImage');

18
node_modules/expo-image/build/ExpoImage.d.ts generated vendored Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react';
import { NativeSyntheticEvent } from 'react-native';
import { ImageErrorEventData, ImageLoadEventData, ImageNativeProps, ImageProgressEventData } from './Image.types';
declare class ExpoImage extends React.PureComponent<ImageNativeProps> {
startAnimating: () => Promise<unknown> | unknown;
stopAnimating: () => Promise<unknown> | unknown;
lockResourceAsync: () => Promise<void>;
unlockResourceAsync: () => Promise<void>;
reloadAsync: () => Promise<void>;
onLoadStart: () => void;
onLoad: (event: NativeSyntheticEvent<ImageLoadEventData>) => void;
onProgress: (event: NativeSyntheticEvent<ImageProgressEventData>) => void;
onError: (event: NativeSyntheticEvent<ImageErrorEventData>) => void;
onLoadEnd: () => void;
render(): React.JSX.Element;
}
export default ExpoImage;
//# sourceMappingURL=ExpoImage.d.ts.map

1
node_modules/expo-image/build/ExpoImage.d.ts.map generated vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"ExpoImage.d.ts","sourceRoot":"","sources":["../src/ExpoImage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAsC,MAAM,cAAc,CAAC;AAExF,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,eAAe,CAAC;AAkBvB,cAAM,SAAU,SAAQ,KAAK,CAAC,aAAa,CAAC,gBAAgB,CAAC;IAE3D,cAAc,EAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAClD,aAAa,EAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACjD,iBAAiB,EAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,mBAAmB,EAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,WAAW,EAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAElC,WAAW,aAET;IAEF,MAAM,GAAI,OAAO,oBAAoB,CAAC,kBAAkB,CAAC,UAGvD;IAEF,UAAU,GAAI,OAAO,oBAAoB,CAAC,sBAAsB,CAAC,UAE/D;IAEF,OAAO,GAAI,OAAO,oBAAoB,CAAC,mBAAmB,CAAC,UAGzD;IAEF,SAAS,aAEP;IAEF,MAAM;CAgEP;AAED,eAAe,SAAS,CAAC"}

4
node_modules/expo-image/build/ExpoImage.web.d.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
import React from 'react';
import type { ImageNativeProps } from './Image.types';
export default function ExpoImage({ source, placeholder, contentFit, contentPosition, placeholderContentFit, cachePolicy, onLoad, transition, onError, responsivePolicy, onLoadEnd, onDisplay, priority, loading, blurRadius, recyclingKey, style, nativeViewRef, accessibilityLabel, alt, tintColor, containerViewRef, draggable, ...props }: ImageNativeProps): React.JSX.Element;
//# sourceMappingURL=ExpoImage.web.d.ts.map

1
node_modules/expo-image/build/ExpoImage.web.d.ts.map generated vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"ExpoImage.web.d.ts","sourceRoot":"","sources":["../src/ExpoImage.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,KAAK,EAAE,gBAAgB,EAA6C,MAAM,eAAe,CAAC;AAsDjG,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAChC,MAAM,EACN,WAAW,EACX,UAAU,EACV,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,MAAM,EACN,UAAU,EACV,OAAO,EACP,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,QAAQ,EACR,OAAO,EACP,UAAU,EACV,YAAY,EACZ,KAAK,EACL,aAAa,EACb,kBAAkB,EAClB,GAAG,EACH,SAAS,EACT,gBAAgB,EAChB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,gBAAgB,qBA8FlB"}

134
node_modules/expo-image/build/Image.d.ts generated vendored Normal file
View File

@@ -0,0 +1,134 @@
import React from 'react';
import { type View } from 'react-native';
import ExpoImage from './ExpoImage';
import { ImageCacheConfig, ImageLoadOptions, ImagePrefetchOptions, ImageProps, ImageRef, ImageSource } from './Image.types';
export declare class Image extends React.PureComponent<ImageProps> {
nativeViewRef: React.RefObject<ExpoImage | null>;
containerViewRef: React.RefObject<View | null>;
constructor(props: ImageProps);
getAnimatableRef: () => View | this | null;
/**
* @hidden
*/
static Image: typeof ImageRef;
/**
* Preloads images at the given URLs that can be later used in the image view.
* Preloaded images are cached to the memory and disk by default, so make sure
* to use `disk` (default) or `memory-disk` [cache policy](#cachepolicy).
* @param urls - A URL string or an array of URLs of images to prefetch.
* @param {ImagePrefetchOptions['cachePolicy']} cachePolicy - The cache policy for prefetched images.
* @return A promise resolving to `true` as soon as all images have been
* successfully prefetched. If an image fails to be prefetched, the promise
* will immediately resolve to `false` regardless of whether other images have
* finished prefetching.
*/
static prefetch(urls: string | string[], cachePolicy?: ImagePrefetchOptions['cachePolicy']): Promise<boolean>;
/**
* Preloads images at the given URLs that can be later used in the image view.
* Preloaded images are cached to the memory and disk by default, so make sure
* to use `disk` (default) or `memory-disk` [cache policy](#cachepolicy).
* @param urls - A URL string or an array of URLs of images to prefetch.
* @param options - Options for prefetching images.
* @return A promise resolving to `true` as soon as all images have been
* successfully prefetched. If an image fails to be prefetched, the promise
* will immediately resolve to `false` regardless of whether other images have
* finished prefetching.
*/
static prefetch(urls: string | string[], options?: ImagePrefetchOptions): Promise<boolean>;
/**
* Asynchronously clears all images stored in memory.
* @platform android
* @platform ios
* @return A promise resolving to `true` when the operation succeeds.
* It may resolve to `false` on Android when the activity is no longer available.
* Resolves to `false` on Web.
*/
static clearMemoryCache(): Promise<boolean>;
/**
* Asynchronously clears all images from the disk cache.
* @platform android
* @platform ios
* @return A promise resolving to `true` when the operation succeeds.
* It may resolve to `false` on Android when the activity is no longer available.
* Resolves to `false` on Web.
*/
static clearDiskCache(): Promise<boolean>;
/**
* Asynchronously checks if an image exists in the disk cache and resolves to
* the path of the cached image if it does.
* @param cacheKey - The cache key for the requested image. Unless you have set
* a custom cache key, this will be the source URL of the image.
* @platform android
* @platform ios
* @return A promise resolving to the path of the cached image. It will resolve
* to `null` if the image does not exist in the cache.
*/
static getCachePathAsync(cacheKey: string): Promise<string | null>;
/**
* Configures the image cache. This allows you to manage the cache eviction policy.
* @param config - The cache configuration.
* @platform ios
*/
static configureCache(config: ImageCacheConfig): void;
/**
* Asynchronously generates a [Blurhash](https://blurha.sh) from an image.
* @param source - The image source, either a URL (string) or an ImageRef
* @param numberOfComponents - The number of components to encode the blurhash with.
* Must be between 1 and 9. Defaults to `[4, 3]`.
* @platform android
* @platform ios
* @return A promise resolving to the blurhash string.
*/
static generateBlurhashAsync(source: string | ImageRef, numberOfComponents: [number, number] | {
width: number;
height: number;
}): Promise<string | null>;
/**
* Asynchronously generates a [Thumbhash](https://evanw.github.io/thumbhash/) from an image.
* @param source - The image source, either a URL (string) or an ImageRef
* @platform android
* @platform ios
* @return A promise resolving to the thumbhash string.
*/
static generateThumbhashAsync(source: string | ImageRef): Promise<string>;
/**
* Asynchronously starts playback of the view's image if it is animated.
* @platform android
* @platform ios
*/
startAnimating(): Promise<void>;
/**
* Asynchronously stops the playback of the view's image if it is animated.
* @platform android
* @platform ios
*/
stopAnimating(): Promise<void>;
/**
* Prevents the resource from being reloaded by locking it.
* @platform android
* @platform ios
*/
lockResourceAsync(): Promise<void>;
/**
* Releases the lock on the resource, allowing it to be reloaded.
* @platform android
* @platform ios
*/
unlockResourceAsync(): Promise<void>;
/**
* Reloads the resource, ignoring lock.
* @platform android
* @platform ios
*/
reloadAsync(): Promise<void>;
/**
* Loads an image from the given source to memory and resolves to
* an object that references the native image instance.
* @platform android
* @platform ios
* @platform web
*/
static loadAsync(source: ImageSource | string | number, options?: ImageLoadOptions): Promise<ImageRef>;
render(): React.JSX.Element;
}
//# sourceMappingURL=Image.d.ts.map

1
node_modules/expo-image/build/Image.d.ts.map generated vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../src/Image.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAA+C,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAEtF,OAAO,SAAS,MAAM,aAAa,CAAC;AACpC,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,UAAU,EACV,QAAQ,EACR,WAAW,EAGZ,MAAM,eAAe,CAAC;AA+BvB,qBAAa,KAAM,SAAQ,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC;IACxD,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACjD,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;gBAEnC,KAAK,EAAE,UAAU;IAO7B,gBAAgB,2BAMd;IAEF;;OAEG;IACH,MAAM,CAAC,KAAK,kBAAqB;IAEjC;;;;;;;;;;OAUG;WACU,QAAQ,CACnB,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EACvB,WAAW,CAAC,EAAE,oBAAoB,CAAC,aAAa,CAAC,GAChD,OAAO,CAAC,OAAO,CAAC;IACnB;;;;;;;;;;OAUG;WACU,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBhG;;;;;;;OAOG;WACU,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjD;;;;;;;OAOG;WACU,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/C;;;;;;;;;OASG;WACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIxE;;;;OAIG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI;IAIrD;;;;;;;;OAQG;WACU,qBAAqB,CAChC,MAAM,EAAE,MAAM,GAAG,QAAQ,EACzB,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GACvE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIzB;;;;;;OAMG;WACU,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAI/E;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;;OAIG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAIpC;;;;OAIG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxC;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1C;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAIlC;;;;;;OAMG;WACU,SAAS,CACpB,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,EACrC,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,QAAQ,CAAC;IAKpB,MAAM;CA6EP"}

707
node_modules/expo-image/build/Image.types.d.ts generated vendored Normal file
View File

@@ -0,0 +1,707 @@
import type { NativeModule, SharedRef, SharedRefType } from 'expo';
import { ImageStyle as RNImageStyle, TextStyle, StyleProp, View, ViewProps, ViewStyle, ColorValue } from 'react-native';
import type { SFSymbol } from 'sf-symbols-typescript';
import ExpoImage from './ExpoImage';
export type ImageSource = {
/**
* A string representing the resource identifier for the image,
* which could be an HTTPS address, a local file path, or the name of a static image resource.
*/
uri?: string;
/**
* An object representing the HTTP headers to send along with the request for a remote image.
* On web requires the `Access-Control-Allow-Origin` header returned by the server to include the current domain.
*/
headers?: Record<string, string>;
/**
* Can be specified if known at build time, in which case the value
* will be used to set the default `<Image/>` component dimension.
*/
width?: number | null;
/**
* Can be specified if known at build time, in which case the value
* will be used to set the default `<Image/>` component dimension.
*/
height?: number | null;
/**
* A string used to generate the image [`placeholder`](#placeholder). For example,
* `placeholder={blurhash}`. If `uri` is provided as the value of the `source` prop,
* this is ignored since the `source` can only have `blurhash` or `uri`.
*
* When using the blurhash, you should also provide `width` and `height` (higher values reduce performance),
* otherwise their default value is `16`.
* For more information, see [`woltapp/blurhash`](https://github.com/woltapp/blurhash) repository.
*/
blurhash?: string;
/**
* A string used to generate the image [`placeholder`](#placeholder). For example,
* `placeholder={thumbhash}`. If `uri` is provided as the value of the `source` prop,
* this is ignored since the `source` can only have `thumbhash` or `uri`.
*
* For more information, see [`thumbhash website`](https://evanw.github.io/thumbhash/).
*/
thumbhash?: string;
/**
* The cache key used to query and store this specific image.
* If not provided, the `uri` is used also as the cache key.
*/
cacheKey?: string;
/**
* The max width of the viewport for which this source should be selected.
* Has no effect if `source` prop is not an array or has only 1 element.
* Has no effect if `responsivePolicy` is not set to `static`.
* Ignored if `blurhash` or `thumbhash` is provided (image hashes are never selected if passed in an array).
* @platform web
*/
webMaxViewportWidth?: number;
/**
* Whether the image is animated (an animated GIF or WebP for example).
* @platform android
* @platform ios
*/
isAnimated?: boolean;
};
/**
* @hidden
*/
export type ImageStyle = RNImageStyle;
/**
* Determines how the image should be resized to fit its container.
* @hidden Described in the {@link ImageProps['contentFit']}
*/
export type ImageContentFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
/**
* Determines which format should be used to decode the image.
* It's suggestion for the platform to use the specified format, but it's not guaranteed.
* @hidden Described in the {@link ImageProps['decodeFormat']}
*/
export type ImageDecodeFormat = 'argb' | 'rgb';
/**
* Some props are from React Native Image that Expo Image supports (more or less) for easier migration,
* but all of them are deprecated and might be removed in the future.
*/
export interface ImageProps extends Omit<ViewProps, 'style' | 'children'> {
/** @hidden */
style?: StyleProp<RNImageStyle & {
/**
* Specifies stroke weight for SF symbols.
* @platform ios
*/
fontWeight?: TextStyle['fontWeight'];
/**
* Sets the tint color of SF symbols. This is an alias for `tintColor` that can be used in styles.
* @platform ios
*/
color?: TextStyle['color'];
/**
* Sets the size (width and height) of SF symbols.
* @platform ios
*/
fontSize?: TextStyle['fontSize'];
}>;
/**
* The image source, either a remote URL, a local file resource or a number that is the result of the `require()` function.
* When provided as an array of sources, the source that fits best into the container size and is closest to the screen scale
* will be chosen. In this case it is important to provide `width`, `height` and `scale` properties.
*
* For SF Symbols (iOS), use the `sf:` prefix followed by the symbol name, for example, `sf:star.fill`.
*
* > **Note**: For the complete list of SF Symbols, see [Apple's SF Symbols catalog](https://developer.apple.com/sf-symbols/) or the [`sf-symbols-typescript`](https://github.com/nandorojo/sf-symbols-typescript) library documentation.
*/
source?: ImageSource | `sf:${SFSymbol}` | (string & {}) | number | ImageSource[] | string[] | SharedRefType<'image'> | null;
/**
* An image to display while loading the proper image and no image has been displayed yet or the source is unset.
*
* > **Note**: The default value for placeholder's content fit is 'scale-down', which differs from the source image's default value.
* > Using a lower-resolution placeholder may cause flickering due to scaling differences between it and the final image.
* > To prevent this, you can set the [`placeholderContentFit`](#placeholdercontentfit) to match the [`contentFit`](#contentfit) value.
*/
placeholder?: ImageSource | string | number | ImageSource[] | string[] | SharedRefType<'image'> | null;
/**
* Determines how the image should be resized to fit its container. This property tells the image to fill the container
* in a variety of ways; such as "preserve that aspect ratio" or "stretch up and take up as much space as possible".
* It mirrors the CSS [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) property.
*
* - `'cover'` - The image is sized to maintain its aspect ratio while filling the container box.
* If the image's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit.
*
* - `'contain'` - The image is scaled down or up to maintain its aspect ratio while fitting within the container box.
*
* - `'fill'` - The image is sized to entirely fill the container box. If necessary, the image will be stretched or squished to fit.
*
* - `'none'` - The image is not resized and is centered by default.
* When specified, the exact position can be controlled with [`contentPosition`](#contentposition) prop.
*
* - `'scale-down'` - The image is sized as if `none` or `contain` were specified, whichever would result in a smaller concrete image size.
*
* @default 'cover'
*/
contentFit?: ImageContentFit;
/**
* Determines how the placeholder should be resized to fit its container. Available resize modes are the same as for the [`contentFit`](#contentfit) prop.
* @default 'scale-down'
*/
placeholderContentFit?: ImageContentFit;
/**
* It is used together with [`contentFit`](#contentfit) to specify how the image should be positioned with x/y coordinates inside its own container.
* An equivalent of the CSS [`object-position`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) property.
* @default 'center'
*/
contentPosition?: ImageContentPosition;
/**
* Describes how the image view should transition the contents when switching the image source.\
* If provided as a number, it is the duration in milliseconds of the `'cross-dissolve'` effect.
*/
transition?: ImageTransition | number | null;
/**
* The radius of the blur in points, `0` means no blur effect.
* This effect is not applied to placeholders.
* @default 0
*/
blurRadius?: number;
/**
* A color used to tint template images (a bitmap image where only the opacity matters).
* The color is applied to every non-transparent pixel, causing the image's shape to adopt that color.
* This effect is not applied to placeholders.
*
* Note that `useImage` options parameter also has a `tintColor` field.
* When you have a `useImage` as a `source` use its `tintColor` instead.
* @default null
*/
tintColor?: string | null;
/**
* Priorities for completing loads. If more than one load is queued at a time,
* the load with the higher priority will be started first.
* Priorities are considered best effort, there are no guarantees about the order in which loads will start or finish.
* @default 'normal'
*/
priority?: 'low' | 'normal' | 'high' | null;
/**
* Sets the HTML [`loading`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#loading) attribute on the `<img>` element.
* Has no effect on native platforms.
*
* - `'lazy'` - Defers loading until the image is near the viewport.
* - `'eager'` - Loads the image immediately.
*
* @default 'eager'
* @platform web
*/
loading?: 'lazy' | 'eager';
/**
* Determines whether to cache the image and where: on the disk, in the memory or both.
*
* - `'none'` - Image is not cached at all.
*
* - `'disk'` - Image is queried from the disk cache if exists, otherwise it's downloaded and then stored on the disk.
*
* - `'memory'` - Image is cached in memory. Might be useful when you render a high-resolution picture many times.
* Memory cache may be purged very quickly to prevent high memory usage and the risk of out of memory exceptions.
*
* - `'memory-disk'` - Image is cached in memory, but with a fallback to the disk cache.
*
* @default 'disk'
*/
cachePolicy?: 'none' | 'disk' | 'memory' | 'memory-disk' | /** @hidden */ null;
/**
* Controls the selection of the image source based on the container or viewport size on the web.
*
* If set to `'static'`, the browser selects the correct source based on user's viewport width. Works with static rendering.
* Make sure to set the `'webMaxViewportWidth'` property on each source for best results.
* For example, if an image occupies 1/3 of the screen width, set the `'webMaxViewportWidth'` to 3x the image width.
* The source with the largest `'webMaxViewportWidth'` is used even for larger viewports.
*
* If set to `'initial'`, the component will select the correct source during mount based on container size. Does not work with static rendering.
*
* If set to `'live'`, the component will select the correct source on every resize based on container size. Does not work with static rendering.
*
* @default 'static'
* @platform web
*/
responsivePolicy?: 'live' | 'initial' | 'static';
/**
* Changing this prop resets the image view content to blank or a placeholder before loading and rendering the final image.
* This is especially useful for any kinds of recycling views like [FlashList](https://github.com/shopify/flash-list)
* to prevent showing the previous source before the new one fully loads.
* @default null
* @platform android
* @platform ios
*/
recyclingKey?: string | null;
/**
* Determines if an image should automatically begin playing if it is an
* animated image.
* @default true
* @platform android
* @platform ios
*/
autoplay?: boolean;
/**
* SF Symbol effect animations. Can be a single effect string, an effect object,
* or an array of effect strings and/or objects.
*
* @example
* ```tsx
* // Single effect as string
* sfEffect="bounce"
*
* // Single effect as object with options
* sfEffect={{ effect: "bounce", repeat: -1, scope: "by-layer" }}
*
* // Array of mixed strings and objects
* sfEffect={["bounce", { effect: "pulse", repeat: -1 }]}
* ```
*
* @platform ios 17.0+
*/
sfEffect?: SFSymbolEffect | null;
/**
* Called when the image starts to load.
*/
onLoadStart?: () => void;
/**
* Called when the image load completes successfully.
*/
onLoad?: (event: ImageLoadEventData) => void;
/**
* Called when the image is loading. Can be called multiple times before the image has finished loading.
* The event object provides details on how many bytes were loaded so far and what's the expected total size.
*/
onProgress?: (event: ImageProgressEventData) => void;
/**
* Called on an image fetching error.
*/
onError?: (event: ImageErrorEventData) => void;
/**
* Called when the image load either succeeds or fails.
*/
onLoadEnd?: () => void;
/**
* Called when the image view successfully rendered the source image.
*/
onDisplay?: () => void;
/**
* @deprecated Provides compatibility for [`defaultSource` from React Native Image](https://reactnative.dev/docs/image#defaultsource).
* Use [`placeholder`](#placeholder) prop instead.
*/
defaultSource?: ImageSource | null;
/**
* @deprecated Provides compatibility for [`loadingIndicatorSource` from React Native Image](https://reactnative.dev/docs/image#loadingindicatorsource).
* Use [`placeholder`](#placeholder) prop instead.
*/
loadingIndicatorSource?: ImageSource | null;
/**
* @deprecated Provides compatibility for [`resizeMode` from React Native Image](https://reactnative.dev/docs/image#resizemode).
* Note that `"repeat"` option is not supported at all.
* Use the more powerful [`contentFit`](#contentfit) and [`contentPosition`](#contentposition) props instead.
*/
resizeMode?: 'cover' | 'contain' | 'stretch' | 'repeat' | 'center';
/**
* @deprecated Provides compatibility for [`fadeDuration` from React Native Image](https://reactnative.dev/docs/image#fadeduration-android).
* Instead use [`transition`](#transition) with the provided duration.
*/
fadeDuration?: number;
/**
* Whether this View should be focusable with a non-touch input device and receive focus with a hardware keyboard.
* @default false
* @platform android
*/
focusable?: boolean;
/**
* When true, indicates that the view is an accessibility element.
* When a view is an accessibility element, it groups its children into a single selectable component.
*
* On Android, the `accessible` property will be translated into the native `isScreenReaderFocusable`,
* so it's only affecting the screen readers behaviour.
* @default false
* @platform android
* @platform ios
*/
accessible?: boolean;
/**
* The text that's read by the screen reader when the user interacts with the image. Sets the `alt` tag on web which is used for web crawlers and link traversal.
* @default undefined
*/
accessibilityLabel?: string;
/**
* The text that's read by the screen reader when the user interacts with the image. Sets the `alt` tag on web which is used for web crawlers and link traversal. Is an alias for `accessibilityLabel`.
*
* @alias accessibilityLabel
* @default undefined
*/
alt?: string;
/**
* Enables Live Text interaction with the image. Check official [Apple documentation](https://developer.apple.com/documentation/visionkit/enabling_live_text_interactions_with_images) for more details.
* @default false
* @platform ios 16.0+
*/
enableLiveTextInteraction?: boolean;
/**
* Whether the image should be downscaled to match the size of the view container.
* Turning off this functionality could negatively impact the application's performance, particularly when working with large assets.
* However, it would result in smoother image resizing, and end-users would always have access to the highest possible asset quality.
*
* Downscaling is never used when the `contentFit` prop is set to `none` or `fill`.
* @default true
*/
allowDownscaling?: boolean;
/**
* The format in which the image data should be decoded.
* It's not guaranteed that the platform will use the specified format.
*
* - `'argb'` - The image is decoded into a 32-bit color space with alpha channel (https://developer.android.com/reference/android/graphics/Bitmap.Config#ARGB_8888).
*
* - `'rgb'` - The image is decoded into a 16-bit color space without alpha channel (https://developer.android.com/reference/android/graphics/Bitmap.Config#RGB_565).
*
* @default 'argb'
* @platform android
*/
decodeFormat?: ImageDecodeFormat;
/**
* Whether to use the Apple's default WebP codec.
*
* Set this prop to `false` to use the official standard-compliant [libwebp](https://github.com/webmproject/libwebp) codec for WebP images.
* The default implementation from Apple is faster and uses less memory but may render animated images with incorrect blending or play them at the wrong framerate.
* @see https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#awebp-coder
*
* @default true
* @platform ios
*/
useAppleWebpCodec?: boolean;
/**
* Force early resizing of the image to match the container size.
* This option helps to reduce the memory usage of the image view, especially when the image is larger than the container.
* It may affect the `resizeType` and `contentPosition` properties when the image view is resized dynamically.
*
* @default false
* @platform ios
*/
enforceEarlyResizing?: boolean;
/**
* Controls whether the image view can leverage the extended dynamic range (EDR). Use this prop if you want to support high dynamic range (HDR) images,
* otherwise all images are rendered as standard dynamic range (SDR).
*
* @default false
* @platform ios 17.0+
* @platform tvos 17.0+
*/
preferHighDynamicRange?: boolean;
/**
* Whether the `img` element is draggable on web.
* @default undefined
* @platform web
*/
draggable?: boolean;
}
/**
* It narrows down some props to types expected by the native/web side.
* @hidden
*/
export interface ImageNativeProps extends ImageProps {
style?: RNImageStyle;
source?: ImageSource[] | SharedRefType<'image'>;
placeholder?: ImageSource[] | SharedRefType<'image'>;
contentPosition?: ImageContentPositionObject;
transition?: ImageTransition | null;
autoplay?: boolean;
sfEffect?: SFSymbolEffectObject[] | null;
nativeViewRef?: React.RefObject<ExpoImage | null>;
containerViewRef?: React.RefObject<View | null>;
symbolWeight?: string | null;
symbolSize?: number | null;
}
/**
* A value that represents the relative position of a single axis.
*
* If `number`, it is a distance in points (logical pixels) from the respective edge.\
* If `string`, it must be a percentage value where `'100%'` is the difference in size between the container and the image along the respective axis,
* or `'center'` which is an alias for `'50%'` that is the default value. You can read more regarding percentages on the MDN docs for
* [`background-position`](https://developer.mozilla.org/en-US/docs/Web/CSS/background-position#regarding_percentages) that describes this concept well.
*/
export type ImageContentPositionValue = number | string | `${number}%` | `${number}` | 'center';
/**
* Specifies the position of the image inside its container. One value controls the x-axis and the second value controls the y-axis.
*
* Additionally, it supports stringified shorthand form that specifies the edges to which to align the image content:\
* `'center'`, `'top'`, `'right'`, `'bottom'`, `'left'`, `'top center'`, `'top right'`, `'top left'`, `'right center'`, `'right top'`,
* `'right bottom'`, `'bottom center'`, `'bottom right'`, `'bottom left'`, `'left center'`, `'left top'`, `'left bottom'`.\
* If only one keyword is provided, then the other dimension is set to `'center'` (`'50%'`), so the image is placed in the middle of the specified edge.\
* As an example, `'top right'` is the same as `{ top: 0, right: 0 }` and `'bottom'` is the same as `{ bottom: 0, left: '50%' }`.
*/
export type ImageContentPosition =
/**
* An object that positions the image relatively to the top-right corner.
*/
{
top?: ImageContentPositionValue;
right?: ImageContentPositionValue;
}
/**
* An object that positions the image relatively to the top-left corner.
*/
| {
top?: ImageContentPositionValue;
left?: ImageContentPositionValue;
}
/**
* An object that positions the image relatively to the bottom-right corner.
*/
| {
bottom?: ImageContentPositionValue;
right?: ImageContentPositionValue;
}
/**
* An object that positions the image relatively to the bottom-left corner.
*/
| {
bottom?: ImageContentPositionValue;
left?: ImageContentPositionValue;
} | ImageContentPositionString;
/**
* It allows you to use an image as a background while rendering other content on top of it.
* It extends all `Image` props but provides separate styling controls for the container and the background image itself.
*/
export interface ImageBackgroundProps extends Omit<ImageProps, 'style'> {
/** The style of the image container. */
style?: StyleProp<ViewStyle> | undefined;
/** Style object for the image. */
imageStyle?: StyleProp<RNImageStyle> | undefined;
/** @hidden */
children?: React.ReactNode | undefined;
}
/**
* @hidden It's described as part of {@link ImageContentPosition}.
*/
export type ImageContentPositionString = 'center' | 'top' | 'right' | 'bottom' | 'left' | 'top center' | 'top right' | 'top left' | 'right center' | 'right top' | 'right bottom' | 'bottom center' | 'bottom right' | 'bottom left' | 'left center' | 'left top' | 'left bottom';
type OnlyObject<T> = T extends object ? T : never;
/**
* @hidden It's a conditional type that matches only objects of {@link ImageContentPosition}.
*/
export type ImageContentPositionObject = OnlyObject<ImageContentPosition>;
/**
* The type of SF Symbol effect animation.
* @platform ios 17.0+
*/
export type SFSymbolEffectType = 'bounce' | 'bounce/up' | 'bounce/down' | 'pulse' | 'variable-color' | 'variable-color/iterative' | 'variable-color/cumulative' | 'scale' | 'scale/up' | 'scale/down' | 'appear' | 'disappear' | 'wiggle' | 'rotate' | 'breathe' | 'draw/on' | 'draw/off';
/**
* An object that describes an SF Symbol effect animation.
* @platform ios 17.0+
*/
export type SFSymbolEffectObject = {
/**
* The type of SF Symbol effect animation.
*
* - `'bounce'` - The symbol bounces.
* - `'bounce/up'` / `'bounce/down'` - Directional bounce.
* - `'pulse'` - The symbol fades in and out.
* - `'variable-color'` - The symbol's color layers animate sequentially.
* - `'variable-color/iterative'` / `'variable-color/cumulative'` - Variable color modes.
* - `'scale'` - The symbol scales up and down.
* - `'scale/up'` / `'scale/down'` - Directional scale.
* - `'appear'` - The symbol animates into view.
* - `'disappear'` - The symbol animates out of view.
*
* For iOS 18+:
* - `'wiggle'` - The symbol wiggles.
* - `'rotate'` - The symbol rotates.
* - `'breathe'` - The symbol breathes (pulsing scale effect).
*
* For iOS 26+:
* - `'draw/on'` - The symbol layers animate like being drawn.
* - `'draw/off'` - The symbol layers animate like being erased.
*/
effect: SFSymbolEffectType;
/**
* The number of times to repeat the effect.
* - `-1` - Repeat indefinitely
* - `0` - No repeat, play once (default)
* - `1+` - Repeat the specified number of times
*
* @default 0
*/
repeat?: number;
/**
* Controls how the effect animates across symbol layers.
* - `'by-layer'` - Animates each layer of the symbol individually.
* - `'whole-symbol'` - Animates the entire symbol as one unit.
*
* @default undefined (uses system default)
*/
scope?: 'by-layer' | 'whole-symbol' | null;
};
/**
* SF Symbol effect configuration. Can be a single effect string, an effect object,
* or an array of effect strings and/or objects.
*
* @example
* ```tsx
* // Single effect as string
* sfEffect="bounce"
*
* // Single effect as object with options
* sfEffect={{ effect: "bounce", repeat: -1, scope: "by-layer" }}
*
* // Array of mixed strings and objects
* sfEffect={["bounce", { effect: "pulse", repeat: -1 }]}
* ```
*
* @platform ios 17.0+
*/
export type SFSymbolEffect = SFSymbolEffectType | SFSymbolEffectObject | (SFSymbolEffectType | SFSymbolEffectObject)[];
/**
* An object that describes the smooth transition when switching the image source.
*/
export type ImageTransition = {
/**
* The duration of the transition in milliseconds.
* @default 0
*/
duration?: number;
/**
* Specifies the speed curve of the transition effect and how intermediate values are calculated.
* @default 'ease-in-out'
*/
timing?: 'ease-in-out' | 'ease-in' | 'ease-out' | 'linear';
/**
* An animation effect used for transition.
* @default 'cross-dissolve'
*
* On Android, only `'cross-dissolve'` is supported.
* On Web, `'curl-up'` and `'curl-down'` effects are not supported.
*
* For SF Symbols (iOS 17+), use the `sf:` effects to animate
* when the symbol source changes:
* - `'sf:replace'` - The symbol animates when replaced with another symbol.
* - `'sf:down-up'` - New symbol slides in from bottom.
* - `'sf:up-up'` - New symbol slides in from top.
* - `'sf:off-up'` - Cross-dissolve transition between symbols.
*
* For other SF Symbol animations (bounce, pulse, scale, and so on), use the `sfEffect` prop instead.
*/
effect?: 'cross-dissolve' | 'flip-from-top' | 'flip-from-right' | 'flip-from-bottom' | 'flip-from-left' | 'curl-up' | 'curl-down' | 'sf:replace' | 'sf:down-up' | 'sf:up-up' | 'sf:off-up' | null;
};
export type ImageLoadEventData = {
cacheType: 'none' | 'disk' | 'memory';
source: {
url: string;
width: number;
height: number;
mediaType: string | null;
isAnimated?: boolean;
};
};
export type ImageProgressEventData = {
loaded: number;
total: number;
};
export type ImageErrorEventData = {
error: string;
};
export type ImagePrefetchOptions = {
/**
* The cache policy for prefetched images.
* @default 'memory-disk'
*/
cachePolicy?: 'disk' | 'memory-disk' | 'memory';
/**
* A map of headers to use when prefetching the images.
*/
headers?: Record<string, string>;
};
/**
* An object that is a reference to a native image instance [Drawable](https://developer.android.com/reference/android/graphics/drawable/Drawable)
* on Android and [UIImage](https://developer.apple.com/documentation/uikit/uiimage) on iOS.
* Instances of this class can be passed as a source to the [Image](#image) component in which case the image is rendered immediately
* since its native representation is already available in the memory.
*/
export declare class ImageRef extends SharedRef<'image'> {
/**
* Logical width of the image. Multiply it by the value in the `scale` property to get the width in pixels.
*/
readonly width: number;
/**
* Logical height of the image. Multiply it by the value in the `scale` property to get the height in pixels.
*/
readonly height: number;
/**
* On iOS, if you load an image from a file whose name includes the `@2x` modifier, the scale is set to **2.0**. All other images are assumed to have a scale factor of **1.0**.
* On Android, it calculates the scale based on the bitmap density divided by screen density.
*
* On all platforms, if you multiply the logical size of the image by this value, you get the dimensions of the image in pixels.
*/
readonly scale: number;
/**
* Media type (also known as MIME type) of the image, based on its format.
* Returns `null` when the format is unknown or not supported.
* @platform ios
*/
readonly mediaType: string | null;
/**
* Whether the referenced image is an animated image.
*/
readonly isAnimated?: boolean;
}
/**
* @hidden
*/
export declare class ImageNativeModule extends NativeModule {
Image: typeof ImageRef;
loadAsync(source: ImageSource, options?: ImageLoadOptions): Promise<ImageRef>;
prefetch(urls: string[], cachePolicy: ImagePrefetchOptions['cachePolicy'], headers?: Record<string, string>): Promise<boolean>;
clearMemoryCache(): Promise<boolean>;
clearDiskCache(): Promise<boolean>;
configureCache(config: ImageCacheConfig): void;
getCachePathAsync(cacheKey: string): Promise<string | null>;
generateBlurhashAsync(source: string | ImageRef, numberOfComponents: [number, number] | {
width: number;
height: number;
}): Promise<string | null>;
generateThumbhashAsync(source: string | ImageRef): Promise<string>;
}
/**
* An object with options for the [`useImage`](#useimage) hook.
*/
export type ImageLoadOptions = {
/**
* If provided, the image will be automatically resized to not exceed this width in pixels, preserving its aspect ratio.
*/
maxWidth?: number;
/**
* If provided, the image will be automatically resized to not exceed this height in pixels, preserving its aspect ratio.
*/
maxHeight?: number;
/**
* A color used to tint template images (a bitmap image where only the opacity matters).
* The color is applied to every non-transparent pixel, causing the image's shape to adopt that color.
* @default null
*/
tintColor?: ColorValue | number;
/**
* Function to call when the image has failed to load. In addition to the error, it also provides a function that retries loading the image.
*/
onError?(error: Error, retry: () => void): void;
};
/**
* An object containing options for the [`configureCache`](#configurecacheconfig) function.
* See [`SDImageCacheConfig`](https://sdwebimage.github.io/documentation/sdwebimage/sdimagecacheconfig) for more information.
* @platform ios
*/
export type ImageCacheConfig = {
/**
* The maximum size of the disk cache, in bytes.
* Defaults to 0, which means there is no cache size limit.
*/
maxDiskSize?: number;
/**
* The maximum "total cost" of the in-memory image cache. The cost function is the bytes size held in memory,
* not simply the pixel count. For example, a typical ARGB8888 image uses 4 bytes (32 bits) per pixel.
* Defaults to 0, which means there is no memory cost limit.
*/
maxMemoryCost?: number;
/**
* The maximum number of objects the in-memory image cache should hold.
* Defaults to 0, which means there is no memory count limit.
*/
maxMemoryCount?: number;
};
export {};
//# sourceMappingURL=Image.types.d.ts.map

1
node_modules/expo-image/build/Image.types.d.ts.map generated vendored Normal file

File diff suppressed because one or more lines are too long

4
node_modules/expo-image/build/ImageBackground.d.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
import React from 'react';
import { ImageBackgroundProps } from './Image.types';
export declare function ImageBackground({ style, imageStyle, children, ...props }: ImageBackgroundProps): React.JSX.Element;
//# sourceMappingURL=ImageBackground.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ImageBackground.d.ts","sourceRoot":"","sources":["../src/ImageBackground.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,oBAAoB,qBAO9F"}

4
node_modules/expo-image/build/ImageModule.d.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
import type { ImageNativeModule } from './Image.types';
declare const _default: ImageNativeModule;
export default _default;
//# sourceMappingURL=ImageModule.d.ts.map

1
node_modules/expo-image/build/ImageModule.d.ts.map generated vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"ImageModule.d.ts","sourceRoot":"","sources":["../src/ImageModule.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;;AAEvD,wBAAmE"}

19
node_modules/expo-image/build/ImageModule.web.d.ts generated vendored Normal file
View File

@@ -0,0 +1,19 @@
import { NativeModule } from 'expo-modules-core';
import { ImageCacheConfig, type ImageNativeModule, ImageRef, ImageSource } from './Image.types';
declare class ImageModule extends NativeModule implements ImageNativeModule {
Image: typeof ImageRef;
prefetch(urls: string | string[], _: unknown, __: unknown): Promise<boolean>;
clearMemoryCache(): Promise<boolean>;
clearDiskCache(): Promise<boolean>;
configureCache(_: ImageCacheConfig): void;
loadAsync(source: ImageSource): Promise<ImageRef>;
getCachePathAsync(_: string): Promise<string | null>;
generateBlurhashAsync(_: string | ImageRef, __: [number, number] | {
width: number;
height: number;
}): Promise<string | null>;
generateThumbhashAsync(_: string | ImageRef): Promise<string>;
}
declare const _default: typeof ImageModule;
export default _default;
//# sourceMappingURL=ImageModule.web.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ImageModule.web.d.ts","sourceRoot":"","sources":["../src/ImageModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAqB,MAAM,mBAAmB,CAAC;AAEpE,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGhG,cAAM,WAAY,SAAQ,YAAa,YAAW,iBAAiB;IACjE,KAAK,EAAE,OAAO,QAAQ,CAAe;IAE/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB5E,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAIpC,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAIxC,cAAc,CAAC,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAEnC,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwBvD,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIpD,qBAAqB,CACnB,CAAC,EAAE,MAAM,GAAG,QAAQ,EACpB,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GACvD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIzB,sBAAsB,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;CAG9D;;AAeD,wBAA2D"}

5
node_modules/expo-image/build/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
export * from './Image.types';
export { Image } from './Image';
export { ImageBackground } from './ImageBackground';
export { useImage } from './useImage';
//# sourceMappingURL=index.d.ts.map

1
node_modules/expo-image/build/index.d.ts.map generated vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}

40
node_modules/expo-image/build/useImage.d.ts generated vendored Normal file
View File

@@ -0,0 +1,40 @@
import { DependencyList } from 'react';
import type { ImageLoadOptions, ImageRef, ImageSource } from './Image.types';
/**
* A hook that loads an image from the given source and returns a reference
* to the native image instance, or `null` until the first image is successfully loaded.
*
* It loads a new image every time the `uri` of the provided source changes.
* To trigger reloads in some other scenarios, you can provide an additional dependency list.
*
* > **warning** Avoid using this hook for large images without specifying size constraints,
* > as it may cause crashes due to excessive memory usage. It is recommended to use either
* > `maxWidth` or `maxHeight` option to scale down the image appropriately for your use case.
*
* @platform android
* @platform ios
* @platform web
*
* @example
* ```ts
* import { useImage, Image } from 'expo-image';
* import { Text } from 'react-native';
*
* export default function MyImage() {
* const image = useImage('https://picsum.photos/1000/800', {
* maxWidth: 800,
* onError(error, retry) {
* console.error('Loading failed:', error.message);
* }
* });
*
* if (!image) {
* return <Text>Image is loading...</Text>;
* }
*
* return <Image source={image} style={{ width: image.width / 2, height: image.height / 2 }} />;
* }
* ```
*/
export declare function useImage(source: ImageSource | string | number, options?: ImageLoadOptions, dependencies?: DependencyList): ImageRef | null;
//# sourceMappingURL=useImage.d.ts.map

1
node_modules/expo-image/build/useImage.d.ts.map generated vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"useImage.d.ts","sourceRoot":"","sources":["../src/useImage.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAA+B,MAAM,OAAO,CAAC;AAGpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAG7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,EACrC,OAAO,GAAE,gBAAqB,EAC9B,YAAY,GAAE,cAAmB,GAChC,QAAQ,GAAG,IAAI,CAgDjB"}

24
node_modules/expo-image/build/utils.d.ts generated vendored Normal file
View File

@@ -0,0 +1,24 @@
import type { SharedRefType } from 'expo';
import { type ImageResizeMode } from 'react-native';
import { ImageContentFit, ImageContentPosition, ImageContentPositionObject, ImageProps, ImageTransition } from './Image.types';
/**
* If the `contentFit` is not provided, it's resolved from the equivalent `resizeMode` prop
* that we support to provide compatibility with React Native Image.
* For SF Symbols, the default is `'contain'` instead of `'cover'`.
*/
export declare function resolveContentFit(contentFit?: ImageContentFit, resizeMode?: ImageResizeMode, isSFSymbol?: boolean): ImageContentFit;
/**
* It resolves a stringified form of the `contentPosition` prop to an object,
* which is the only form supported in the native code.
*/
export declare function resolveContentPosition(contentPosition?: ImageContentPosition): ImageContentPositionObject;
/**
* If `transition` or `fadeDuration` is a number, it's resolved to a cross dissolve transition with the given duration.
* When `fadeDuration` is used, it logs an appropriate deprecation warning.
*/
export declare function resolveTransition(transition?: ImageProps['transition'], fadeDuration?: ImageProps['fadeDuration']): ImageTransition | null;
/**
* Checks whether the given value is an instance of the `SharedRef<'image'>` class.
*/
export declare function isImageRef(value: any): value is SharedRefType<'image'>;
//# sourceMappingURL=utils.d.ts.map

1
node_modules/expo-image/build/utils.d.ts.map generated vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,0BAA0B,EAE1B,UAAU,EACV,eAAe,EAChB,MAAM,eAAe,CAAC;AAMvB;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,CAAC,EAAE,eAAe,EAC5B,UAAU,CAAC,EAAE,eAAe,EAC5B,UAAU,CAAC,EAAE,OAAO,GACnB,eAAe,CAiCjB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,eAAe,CAAC,EAAE,oBAAoB,GACrC,0BAA0B,CAiC5B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,CAAC,EAAE,UAAU,CAAC,YAAY,CAAC,EACrC,YAAY,CAAC,EAAE,UAAU,CAAC,cAAc,CAAC,GACxC,eAAe,GAAG,IAAI,CAYxB;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,GAAG,GAAG,KAAK,IAAI,aAAa,CAAC,OAAO,CAAC,CAEtE"}

View File

@@ -0,0 +1,24 @@
import { PackagerAsset } from '@react-native/assets-registry/registry';
export type ResolvedAssetSource = {
__packager_asset: boolean;
width?: number;
height?: number;
uri: string;
scale: number;
};
export default class AssetSourceResolver {
serverUrl: string;
jsbundleUrl?: string | null;
asset: PackagerAsset;
constructor(serverUrl: string | undefined | null, jsbundleUrl: string | undefined | null, asset: PackagerAsset);
isLoadedFromServer(): boolean;
isLoadedFromFileSystem(): boolean;
defaultAsset(): ResolvedAssetSource;
/**
* @returns absolute remote URL for the hosted asset.
*/
assetServerURL(): ResolvedAssetSource;
fromSource(source: string): ResolvedAssetSource;
static pickScale(scales: number[], deviceScale: number): number;
}
//# sourceMappingURL=AssetSourceResolver.web.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"AssetSourceResolver.web.d.ts","sourceRoot":"","sources":["../../src/utils/AssetSourceResolver.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAC;AAIvE,MAAM,MAAM,mBAAmB,GAAG;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAcF,MAAM,CAAC,OAAO,OAAO,mBAAmB;IACtC,SAAS,EAAE,MAAM,CAAC;IAGlB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B,KAAK,EAAE,aAAa,CAAC;gBAGnB,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACpC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACtC,KAAK,EAAE,aAAa;IAQtB,kBAAkB,IAAI,OAAO;IAK7B,sBAAsB,IAAI,OAAO;IAIjC,YAAY,IAAI,mBAAmB;IAInC;;OAEG;IACH,cAAc,IAAI,mBAAmB;IAUrC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,mBAAmB;IAU/C,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM;CAQhE"}

View File

@@ -0,0 +1,3 @@
export declare const decode83: (str: string) => number;
export declare const encode83: (n: number, length: number) => string;
//# sourceMappingURL=base83.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"base83.d.ts","sourceRoot":"","sources":["../../../src/utils/blurhash/base83.ts"],"names":[],"mappings":"AAsFA,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM,WAQnC,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,QAAQ,MAAM,KAAG,MAOpD,CAAC"}

View File

@@ -0,0 +1,7 @@
export declare const isBlurhashValid: (blurhash: string) => {
result: boolean;
errorReason?: string;
};
declare const decode: (blurhash: string, width: number, height: number, punch?: number) => Uint8ClampedArray<ArrayBuffer>;
export default decode;
//# sourceMappingURL=decode.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../../src/utils/blurhash/decode.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,KAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAQzF,CAAC;AAuBF,QAAA,MAAM,MAAM,GAAI,UAAU,MAAM,EAAE,OAAO,MAAM,EAAE,QAAQ,MAAM,EAAE,QAAQ,MAAM,mCAsD9E,CAAC;AAEF,eAAe,MAAM,CAAC"}

View File

@@ -0,0 +1,4 @@
export declare class ValidationError extends Error {
constructor(message: string);
}
//# sourceMappingURL=error.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../../src/utils/blurhash/error.ts"],"names":[],"mappings":"AAAA,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAK5B"}

View File

@@ -0,0 +1,8 @@
export declare function useBlurhash(blurhash: {
uri?: string;
width?: number | null;
height?: number | null;
} | undefined | null, punch?: number): readonly [{
uri: string;
} | null, boolean | ""];
//# sourceMappingURL=useBlurhash.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"useBlurhash.d.ts","sourceRoot":"","sources":["../../../src/utils/blurhash/useBlurhash.tsx"],"names":[],"mappings":"AAiBA,wBAAgB,WAAW,CACzB,QAAQ,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,SAAS,GAAG,IAAI,EAC5F,KAAK,GAAE,MAAU;;wBAiElB"}

View File

@@ -0,0 +1,5 @@
export declare const sRGBToLinear: (value: number) => number;
export declare const linearTosRGB: (value: number) => number;
export declare const sign: (n: number) => 1 | -1;
export declare const signPow: (val: number, exp: number) => number;
//# sourceMappingURL=utils.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/utils/blurhash/utils.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,WAOzC,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,WAOzC,CAAC;AAEF,eAAO,MAAM,IAAI,GAAI,GAAG,MAAM,WAAqB,CAAC;AAEpD,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,EAAE,KAAK,MAAM,WAA6C,CAAC"}

Some files were not shown because too many files have changed in this diff Show More