@symbiote-native/image 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -2
  3. package/build/angular/image/index.d.ts +20 -0
  4. package/build/angular/image/index.js +113 -0
  5. package/build/angular/image-background/index.d.ts +14 -0
  6. package/build/angular/image-background/index.js +109 -0
  7. package/build/angular/index.d.ts +4 -0
  8. package/build/angular/index.js +6 -0
  9. package/build/angular/inject-image.d.ts +8 -0
  10. package/build/angular/inject-image.js +20 -0
  11. package/build/core/constants.d.ts +3 -0
  12. package/build/core/constants.js +4 -0
  13. package/build/core/hash-strings.d.ts +5 -0
  14. package/build/core/hash-strings.js +22 -0
  15. package/build/core/image-api.d.ts +18 -0
  16. package/build/core/image-api.js +30 -0
  17. package/build/core/image-background.d.ts +11 -0
  18. package/build/core/image-background.js +29 -0
  19. package/build/core/image-loader.d.ts +20 -0
  20. package/build/core/image-loader.js +42 -0
  21. package/build/core/image-ref-loader.d.ts +4 -0
  22. package/build/core/image-ref-loader.js +10 -0
  23. package/build/core/image-view-events.d.ts +9 -0
  24. package/build/core/image-view-events.js +34 -0
  25. package/build/core/image-view.d.ts +14 -0
  26. package/build/core/image-view.js +113 -0
  27. package/build/core/index.d.ts +10 -0
  28. package/build/core/index.js +7 -0
  29. package/build/core/native-module.d.ts +27 -0
  30. package/build/core/native-module.js +3 -0
  31. package/build/core/native-style.d.ts +19 -0
  32. package/build/core/native-style.js +65 -0
  33. package/build/core/props.d.ts +11 -0
  34. package/build/core/props.js +87 -0
  35. package/build/core/sources.d.ts +8 -0
  36. package/build/core/sources.js +61 -0
  37. package/build/core/types.d.ts +186 -0
  38. package/build/core/types.js +1 -0
  39. package/build/react/image-background.d.ts +7 -0
  40. package/build/react/image-background.js +7 -0
  41. package/build/react/image.d.ts +8 -0
  42. package/build/react/image.js +6 -0
  43. package/build/react/index.d.ts +6 -0
  44. package/build/react/index.js +5 -0
  45. package/build/react/use-image.d.ts +8 -0
  46. package/build/react/use-image.js +20 -0
  47. package/build/solid/image-background.d.ts +7 -0
  48. package/build/solid/image-background.js +4 -0
  49. package/build/solid/image.d.ts +45 -0
  50. package/build/solid/image.js +4 -0
  51. package/build/solid/index.d.ts +5 -0
  52. package/build/solid/index.js +5 -0
  53. package/build/solid/use-image.d.ts +8 -0
  54. package/build/solid/use-image.js +16 -0
  55. package/build/svelte/image-background-props.d.ts +5 -0
  56. package/build/svelte/image-background-props.js +1 -0
  57. package/build/svelte/image-background.svelte +10 -0
  58. package/build/svelte/image-background.svelte.d.ts +4 -0
  59. package/build/svelte/image.svelte +26 -0
  60. package/build/svelte/image.svelte.d.ts +10 -0
  61. package/build/svelte/index.d.ts +6 -0
  62. package/build/svelte/index.js +5 -0
  63. package/build/svelte/use-image.svelte.d.ts +10 -0
  64. package/build/svelte/use-image.svelte.js +26 -0
  65. package/build/vue/image-background.d.ts +3 -0
  66. package/build/vue/image-background.js +12 -0
  67. package/build/vue/image.d.ts +2 -0
  68. package/build/vue/image.js +4 -0
  69. package/build/vue/index.d.ts +4 -0
  70. package/build/vue/index.js +5 -0
  71. package/build/vue/use-image.d.ts +8 -0
  72. package/build/vue/use-image.js +18 -0
  73. package/build-ngc/angular/image/index.d.ts +23 -0
  74. package/build-ngc/angular/image/index.js +72 -0
  75. package/build-ngc/angular/image/index.js.map +1 -0
  76. package/build-ngc/angular/image-background/index.d.ts +17 -0
  77. package/build-ngc/angular/image-background/index.js +70 -0
  78. package/build-ngc/angular/image-background/index.js.map +1 -0
  79. package/build-ngc/angular/index.d.ts +4 -0
  80. package/build-ngc/angular/index.js +7 -0
  81. package/build-ngc/angular/index.js.map +1 -0
  82. package/build-ngc/angular/inject-image.d.ts +8 -0
  83. package/build-ngc/angular/inject-image.js +23 -0
  84. package/build-ngc/angular/inject-image.js.map +1 -0
  85. package/build-ngc/core/constants.d.ts +3 -0
  86. package/build-ngc/core/constants.js +5 -0
  87. package/build-ngc/core/constants.js.map +1 -0
  88. package/build-ngc/core/hash-strings.d.ts +5 -0
  89. package/build-ngc/core/hash-strings.js +23 -0
  90. package/build-ngc/core/hash-strings.js.map +1 -0
  91. package/build-ngc/core/image-api.d.ts +18 -0
  92. package/build-ngc/core/image-api.js +31 -0
  93. package/build-ngc/core/image-api.js.map +1 -0
  94. package/build-ngc/core/image-background.d.ts +11 -0
  95. package/build-ngc/core/image-background.js +30 -0
  96. package/build-ngc/core/image-background.js.map +1 -0
  97. package/build-ngc/core/image-loader.d.ts +20 -0
  98. package/build-ngc/core/image-loader.js +43 -0
  99. package/build-ngc/core/image-loader.js.map +1 -0
  100. package/build-ngc/core/image-ref-loader.d.ts +4 -0
  101. package/build-ngc/core/image-ref-loader.js +11 -0
  102. package/build-ngc/core/image-ref-loader.js.map +1 -0
  103. package/build-ngc/core/image-view-events.d.ts +9 -0
  104. package/build-ngc/core/image-view-events.js +35 -0
  105. package/build-ngc/core/image-view-events.js.map +1 -0
  106. package/build-ngc/core/image-view.d.ts +14 -0
  107. package/build-ngc/core/image-view.js +114 -0
  108. package/build-ngc/core/image-view.js.map +1 -0
  109. package/build-ngc/core/index.d.ts +10 -0
  110. package/build-ngc/core/index.js +8 -0
  111. package/build-ngc/core/index.js.map +1 -0
  112. package/build-ngc/core/native-module.d.ts +27 -0
  113. package/build-ngc/core/native-module.js +4 -0
  114. package/build-ngc/core/native-module.js.map +1 -0
  115. package/build-ngc/core/native-style.d.ts +19 -0
  116. package/build-ngc/core/native-style.js +66 -0
  117. package/build-ngc/core/native-style.js.map +1 -0
  118. package/build-ngc/core/props.d.ts +11 -0
  119. package/build-ngc/core/props.js +88 -0
  120. package/build-ngc/core/props.js.map +1 -0
  121. package/build-ngc/core/sources.d.ts +8 -0
  122. package/build-ngc/core/sources.js +62 -0
  123. package/build-ngc/core/sources.js.map +1 -0
  124. package/build-ngc/core/types.d.ts +186 -0
  125. package/build-ngc/core/types.js +2 -0
  126. package/build-ngc/core/types.js.map +1 -0
  127. package/native-link.json +13 -0
  128. package/package.json +145 -3
  129. package/src/angular/image/index.ts +79 -0
  130. package/src/angular/image-background/index.ts +72 -0
  131. package/src/angular/index.ts +7 -0
  132. package/src/angular/inject-image.ts +29 -0
  133. package/src/core/constants.ts +6 -0
  134. package/src/core/hash-strings.ts +29 -0
  135. package/src/core/image-api.ts +77 -0
  136. package/src/core/image-background.ts +47 -0
  137. package/src/core/image-loader.ts +72 -0
  138. package/src/core/image-ref-loader.ts +16 -0
  139. package/src/core/image-view-events.ts +47 -0
  140. package/src/core/image-view.ts +173 -0
  141. package/src/core/index.ts +50 -0
  142. package/src/core/native-module.ts +44 -0
  143. package/src/core/native-style.ts +96 -0
  144. package/src/core/props.ts +123 -0
  145. package/src/core/sources.ts +75 -0
  146. package/src/core/types.ts +275 -0
  147. package/src/react/image-background.ts +16 -0
  148. package/src/react/image.ts +17 -0
  149. package/src/react/index.ts +8 -0
  150. package/src/react/use-image.ts +29 -0
  151. package/src/solid/image-background.ts +12 -0
  152. package/src/solid/image.ts +9 -0
  153. package/src/solid/index.ts +7 -0
  154. package/src/solid/use-image.ts +30 -0
  155. package/src/svelte/image-background-props.ts +6 -0
  156. package/src/svelte/image-background.svelte +10 -0
  157. package/src/svelte/image.svelte +26 -0
  158. package/src/svelte/index.ts +8 -0
  159. package/src/svelte/svelte-ambient.d.ts +3 -0
  160. package/src/svelte/svelte-compile.test-helper.ts +6 -0
  161. package/src/svelte/use-image.svelte.ts +38 -0
  162. package/src/vue/image-background.ts +17 -0
  163. package/src/vue/image.ts +5 -0
  164. package/src/vue/index.ts +6 -0
  165. package/src/vue/use-image.ts +38 -0
@@ -0,0 +1,173 @@
1
+ import {
2
+ requireNativeModule,
3
+ requireNativeViewManager,
4
+ } from 'expo-modules-core';
5
+ import { el } from '@symbiote-native/components';
6
+ import type { IDescriptor } from '@symbiote-native/components';
7
+ import {
8
+ defineExpoNativeView,
9
+ defineExpoViewMethods,
10
+ getNativeTag,
11
+ isDevBuild,
12
+ styleOfProps,
13
+ } from '@symbiote-native/engine';
14
+ import type { ISymbioteNode } from '@symbiote-native/engine';
15
+ import { IMAGE_MODULE_NAME, SF_SYMBOL_PREFIX } from './constants';
16
+ import { toImageEvents } from './image-view-events';
17
+ import {
18
+ nativeColorProps,
19
+ platformStyle,
20
+ splitImageStyle,
21
+ toNativeColor,
22
+ } from './native-style';
23
+ import type { IImageStyleParts } from './native-style';
24
+ import {
25
+ resolveContentFit,
26
+ resolveContentPosition,
27
+ resolveSfEffect,
28
+ resolveTransition,
29
+ warnOnce,
30
+ } from './props';
31
+ import { resolveSources } from './sources';
32
+ import type { IImageViewHandle, IImageViewProps } from './types';
33
+
34
+ // Registered on render: a barrel side effect is lost in a release build
35
+ const imageView = defineExpoNativeView(
36
+ requireNativeViewManager,
37
+ IMAGE_MODULE_NAME,
38
+ );
39
+
40
+ const callViewFunction = defineExpoViewMethods(
41
+ requireNativeModule,
42
+ IMAGE_MODULE_NAME,
43
+ );
44
+
45
+ export const imageViewName = imageView.name;
46
+
47
+ export const ensureImageViewRegistered = imageView.ensureRegistered;
48
+
49
+ export type IImageView = {
50
+ handle: IImageViewHandle;
51
+ /** `null` means the view cannot register and the caller renders nothing */
52
+ render(props: IImageViewProps): IDescriptor | null;
53
+ };
54
+
55
+ type IGetNode = () => ISymbioteNode | null | undefined;
56
+
57
+ // Props the view reads itself and native has no use for
58
+ const OWN_PROPS = [
59
+ 'style',
60
+ 'source',
61
+ 'placeholder',
62
+ 'contentFit',
63
+ 'contentPosition',
64
+ 'transition',
65
+ 'fadeDuration',
66
+ 'resizeMode',
67
+ 'defaultSource',
68
+ 'loadingIndicatorSource',
69
+ 'sfEffect',
70
+ 'alt',
71
+ ] as const satisfies readonly (keyof IImageViewProps)[];
72
+
73
+ function hasSfSymbol(sources: ReturnType<typeof resolveSources>): boolean {
74
+ return (
75
+ Array.isArray(sources) &&
76
+ sources.some(source => source.uri?.startsWith(`${SF_SYMBOL_PREFIX}/`))
77
+ );
78
+ }
79
+
80
+ function withoutOwnProps(props: IImageViewProps): Record<string, unknown> {
81
+ const own: readonly string[] = OWN_PROPS;
82
+ return Object.fromEntries(
83
+ Object.entries(props).filter(([key]) => !own.includes(key)),
84
+ );
85
+ }
86
+
87
+ function placeholderOf(props: IImageViewProps) {
88
+ const { defaultSource, loadingIndicatorSource } = props;
89
+ if (defaultSource || loadingIndicatorSource) {
90
+ warnOnce(
91
+ '`defaultSource` and `loadingIndicatorSource` props are deprecated, use `placeholder`',
92
+ );
93
+ }
94
+ return resolveSources(
95
+ props.placeholder ?? defaultSource ?? loadingIndicatorSource,
96
+ );
97
+ }
98
+
99
+ // For an SF Symbol `fontSize` sizes both the symbol and its container
100
+ function symbolProps(parts: IImageStyleParts, isSfSymbol: boolean) {
101
+ if (!isSfSymbol) return { symbolWeight: null, symbolSize: null };
102
+ const { fontWeight, fontSize } = parts;
103
+ return {
104
+ symbolWeight: fontWeight === undefined ? null : String(fontWeight),
105
+ symbolSize: fontSize || null,
106
+ };
107
+ }
108
+
109
+ function sizedStyle(parts: IImageStyleParts, isSfSymbol: boolean) {
110
+ const { fontSize, rest } = parts;
111
+ return isSfSymbol && fontSize
112
+ ? { width: fontSize, height: fontSize, ...rest }
113
+ : rest;
114
+ }
115
+
116
+ function nativeProps(props: IImageViewProps): Record<string, unknown> {
117
+ const parts = splitImageStyle(styleOfProps(props));
118
+ const source = resolveSources(props.source);
119
+ const isSfSymbol = hasSfSymbol(source);
120
+ const sized = sizedStyle(parts, isSfSymbol);
121
+ const style = platformStyle(sized);
122
+ const symbolColor = isSfSymbol ? parts.color : undefined;
123
+ return {
124
+ ...withoutOwnProps(props),
125
+ ...style,
126
+ accessibilityLabel: props.accessibilityLabel ?? props.alt,
127
+ style,
128
+ source,
129
+ placeholder: placeholderOf(props),
130
+ contentFit: resolveContentFit(
131
+ props.contentFit,
132
+ props.resizeMode ?? parts.resizeMode,
133
+ isSfSymbol,
134
+ ),
135
+ contentPosition: resolveContentPosition(props.contentPosition),
136
+ transition: resolveTransition(props.transition, props.fadeDuration),
137
+ sfEffect: resolveSfEffect(props.sfEffect),
138
+ ...symbolProps(parts, isSfSymbol),
139
+ ...nativeColorProps(sized),
140
+ tintColor: toNativeColor(props.tintColor || symbolColor || style.tintColor),
141
+ ...toImageEvents(props),
142
+ };
143
+ }
144
+
145
+ function createImageViewHandle(getNode: IGetNode): IImageViewHandle {
146
+ // Like upstream's `ref.current?.`, a view that has not committed yet answers nothing
147
+ async function call(method: string): Promise<void> {
148
+ const node = getNode();
149
+ if (!node || getNativeTag(node) === undefined) return;
150
+ await callViewFunction(node, method, []);
151
+ }
152
+ return {
153
+ startAnimating: () => call('startAnimating'),
154
+ stopAnimating: () => call('stopAnimating'),
155
+ lockResourceAsync: () => call('lockResourceAsync'),
156
+ unlockResourceAsync: () => call('unlockResourceAsync'),
157
+ reloadAsync: () => call('reloadAsync'),
158
+ };
159
+ }
160
+
161
+ /** One per mounted view, `getNode` answers the host node once it exists */
162
+ export function createImageView(getNode: IGetNode): IImageView {
163
+ return {
164
+ handle: createImageViewHandle(getNode),
165
+ render: props => {
166
+ if (!ensureImageViewRegistered()) {
167
+ if (isDevBuild()) console.warn("'Image' is not available.");
168
+ return null;
169
+ }
170
+ return el(imageViewName(), nativeProps(props));
171
+ },
172
+ };
173
+ }
@@ -0,0 +1,50 @@
1
+ export { IMAGE_MODULE_NAME } from './constants';
2
+ export {
3
+ clearDiskCache,
4
+ clearMemoryCache,
5
+ configureCache,
6
+ generateBlurhashAsync,
7
+ generateThumbhashAsync,
8
+ getCachePathAsync,
9
+ loadImageAsync,
10
+ prefetchImages,
11
+ readFromCacheAsync,
12
+ writeToCacheAsync,
13
+ } from './image-api';
14
+ export { renderImageBackground } from './image-background';
15
+ export type { IImageBackgroundProps } from './image-background';
16
+ export { createImageRefLoader } from './image-ref-loader';
17
+ export { createImageLoader } from './image-loader';
18
+ export type { IImageLoader, IImageLoaderConfig } from './image-loader';
19
+ export {
20
+ createImageView,
21
+ ensureImageViewRegistered,
22
+ imageViewName,
23
+ type IImageView,
24
+ } from './image-view';
25
+ export { isImageRef, resolveSource, resolveSources } from './sources';
26
+ export type {
27
+ IImageCacheConfig,
28
+ IImageContentFit,
29
+ IImageContentPosition,
30
+ IImageContentPositionObject,
31
+ IImageContentPositionString,
32
+ IImageContentPositionValue,
33
+ IImageDecodeFormat,
34
+ IImageErrorEventData,
35
+ IImageLoadEventData,
36
+ IImageLoadOptions,
37
+ IImagePrefetchOptions,
38
+ IImageProgressEventData,
39
+ IImageResizeMode,
40
+ IImageSource,
41
+ IImageSources,
42
+ IImageStyle,
43
+ IImageTransition,
44
+ IImageViewHandle,
45
+ IImageViewProps,
46
+ ISfSymbolEffect,
47
+ ISfSymbolEffectObject,
48
+ ISfSymbolEffectType,
49
+ ImageRef,
50
+ } from './types';
@@ -0,0 +1,44 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import type { EventSubscription } from 'expo-modules-core';
3
+ import { IMAGE_MODULE_NAME } from './constants';
4
+ import type {
5
+ IImageCacheConfig,
6
+ IImageLoadOptions,
7
+ IImagePrefetchOptions,
8
+ IImageSource,
9
+ ImageRef,
10
+ } from './types';
11
+
12
+ export type INativeImageModule = {
13
+ Image: typeof ImageRef;
14
+ loadAsync(
15
+ source: IImageSource,
16
+ options?: Omit<IImageLoadOptions, 'onError' | 'tintColor'> & {
17
+ /** What `processColor` answers */
18
+ tintColor?: unknown;
19
+ },
20
+ ): Promise<ImageRef>;
21
+ prefetch(
22
+ urls: string[],
23
+ cachePolicy: IImagePrefetchOptions['cachePolicy'],
24
+ headers?: Record<string, string>,
25
+ ): Promise<boolean>;
26
+ clearMemoryCache(): Promise<boolean>;
27
+ clearDiskCache(): Promise<boolean>;
28
+ configureCache(config: IImageCacheConfig): void;
29
+ getCachePathAsync(cacheKey: string): Promise<string | null>;
30
+ writeToCacheAsync(source: string | ImageRef, cacheKey: string): Promise<void>;
31
+ readFromCacheAsync(cacheKey: string): Promise<ImageRef | null>;
32
+ generateBlurhashAsync(
33
+ source: string | ImageRef,
34
+ numberOfComponents: [number, number] | { width: number; height: number },
35
+ ): Promise<string | null>;
36
+ generateThumbhashAsync(source: string | ImageRef): Promise<string>;
37
+ addListener(
38
+ eventName: string,
39
+ listener: (event: { url: string; width: number; height: number }) => void,
40
+ ): EventSubscription;
41
+ };
42
+
43
+ export const expoImage =
44
+ requireNativeModule<INativeImageModule>(IMAGE_MODULE_NAME);
@@ -0,0 +1,96 @@
1
+ import { Platform } from 'expo-modules-core';
2
+ import { flattenStyle, processColor } from '@symbiote-native/engine';
3
+ import type { IColorValue, IViewStyle } from '@symbiote-native/engine';
4
+ import type { IImageResizeMode } from './types';
5
+
6
+ type IStyleBag = Record<string, unknown>;
7
+
8
+ const SHADOW_KEYS = [
9
+ 'shadowColor',
10
+ 'shadowOffset',
11
+ 'shadowOpacity',
12
+ 'shadowRadius',
13
+ ] as const satisfies readonly (keyof IViewStyle)[];
14
+
15
+ const BORDER_COLOR_KEYS = [
16
+ 'borderColor',
17
+ 'borderStartColor',
18
+ 'borderEndColor',
19
+ 'borderLeftColor',
20
+ 'borderRightColor',
21
+ 'borderTopColor',
22
+ 'borderBottomColor',
23
+ ] as const satisfies readonly (keyof IViewStyle)[];
24
+
25
+ const RESIZE_MODES = new Set<unknown>([
26
+ 'cover',
27
+ 'contain',
28
+ 'stretch',
29
+ 'repeat',
30
+ 'center',
31
+ ] as const satisfies readonly IImageResizeMode[]);
32
+
33
+ export type IImageStyleParts = {
34
+ resizeMode?: IImageResizeMode;
35
+ fontWeight?: string | number;
36
+ fontSize?: number;
37
+ color?: unknown;
38
+ /** What is left once the text-like keys are taken out */
39
+ rest: IStyleBag;
40
+ };
41
+
42
+ function isResizeMode(value: unknown): value is IImageResizeMode {
43
+ return RESIZE_MODES.has(value);
44
+ }
45
+
46
+ function isColorValue(value: unknown): value is IColorValue {
47
+ return (
48
+ typeof value === 'string' ||
49
+ typeof value === 'number' ||
50
+ (typeof value === 'object' && value !== null)
51
+ );
52
+ }
53
+
54
+ /** An Expo view has no attribute processor for colors, so they go through here */
55
+ export function toNativeColor(value: unknown): unknown {
56
+ return isColorValue(value) ? processColor(value) : undefined;
57
+ }
58
+
59
+ /** `resizeMode` and the SF Symbol keys are props to native, not style */
60
+ export function splitImageStyle(style: unknown): IImageStyleParts {
61
+ const { resizeMode, fontWeight, fontSize, color, ...rest } =
62
+ flattenStyle(style);
63
+ const isWeight =
64
+ typeof fontWeight === 'string' || typeof fontWeight === 'number';
65
+ return {
66
+ resizeMode: isResizeMode(resizeMode) ? resizeMode : undefined,
67
+ fontWeight: isWeight ? fontWeight : undefined,
68
+ fontSize: typeof fontSize === 'number' ? fontSize : undefined,
69
+ color,
70
+ rest,
71
+ };
72
+ }
73
+
74
+ function without(bag: IStyleBag, keys: readonly string[]): IStyleBag {
75
+ return Object.fromEntries(
76
+ Object.entries(bag).filter(([key]) => !keys.includes(key)),
77
+ );
78
+ }
79
+
80
+ /** Android shadows by `elevation` and paints the background itself, iOS by `shadow*` */
81
+ export function platformStyle(style: IStyleBag): IStyleBag {
82
+ return Platform.select({
83
+ android: without(style, [...SHADOW_KEYS, 'backgroundColor']),
84
+ default: without(style, ['elevation']),
85
+ });
86
+ }
87
+
88
+ /** `backgroundColor` and the border colors in the form native reads */
89
+ export function nativeColorProps(style: IStyleBag): IStyleBag {
90
+ return Object.fromEntries(
91
+ ['backgroundColor', ...BORDER_COLOR_KEYS].map(key => [
92
+ key,
93
+ toNativeColor(style[key]),
94
+ ]),
95
+ );
96
+ }
@@ -0,0 +1,123 @@
1
+ import type {
2
+ IImageContentFit,
3
+ IImageContentPosition,
4
+ IImageContentPositionObject,
5
+ IImageContentPositionString,
6
+ IImageResizeMode,
7
+ IImageTransition,
8
+ IImageViewProps,
9
+ ISfSymbolEffectObject,
10
+ } from './types';
11
+
12
+ const CENTER: IImageContentPositionObject = { top: '50%', left: '50%' };
13
+
14
+ const POSITION_BY_KEYWORD: Record<
15
+ IImageContentPositionString,
16
+ IImageContentPositionObject
17
+ > = {
18
+ center: CENTER,
19
+ top: { top: 0, left: '50%' },
20
+ right: { top: '50%', right: 0 },
21
+ bottom: { bottom: 0, left: '50%' },
22
+ left: { top: '50%', left: 0 },
23
+ 'top center': { top: 0, left: '50%' },
24
+ 'top right': { top: 0, right: 0 },
25
+ 'top left': { top: 0, left: 0 },
26
+ 'right center': { top: '50%', right: 0 },
27
+ 'right top': { top: 0, right: 0 },
28
+ 'right bottom': { bottom: 0, right: 0 },
29
+ 'bottom center': { bottom: 0, left: '50%' },
30
+ 'bottom right': { bottom: 0, right: 0 },
31
+ 'bottom left': { bottom: 0, left: 0 },
32
+ 'left center': { top: '50%', left: 0 },
33
+ 'left top': { top: 0, left: 0 },
34
+ 'left bottom': { bottom: 0, left: 0 },
35
+ };
36
+
37
+ const warned = new Set<string>();
38
+
39
+ export function warnOnce(message: string): void {
40
+ if (warned.has(message)) {
41
+ return;
42
+ }
43
+ warned.add(message);
44
+ console.warn(`[expo-image]: ${message}`);
45
+ }
46
+
47
+ export function resetDeprecationWarnings(): void {
48
+ warned.clear();
49
+ }
50
+
51
+ function fitFromResizeMode(resizeMode: IImageResizeMode): IImageContentFit {
52
+ warnOnce('Prop "resizeMode" is deprecated, use "contentFit" instead');
53
+ switch (resizeMode) {
54
+ case 'contain':
55
+ case 'cover':
56
+ return resizeMode;
57
+ case 'stretch':
58
+ return 'fill';
59
+ case 'center':
60
+ return 'scale-down';
61
+ case 'repeat':
62
+ warnOnce('Resize mode "repeat" is no longer supported');
63
+ return 'cover';
64
+ }
65
+ }
66
+
67
+ /** An SF Symbol defaults to `contain` to keep its aspect ratio */
68
+ export function resolveContentFit(
69
+ contentFit?: IImageContentFit,
70
+ resizeMode?: IImageResizeMode,
71
+ isSfSymbol?: boolean,
72
+ ): IImageContentFit {
73
+ if (contentFit) {
74
+ return contentFit;
75
+ }
76
+ if (resizeMode) {
77
+ return fitFromResizeMode(resizeMode);
78
+ }
79
+ return isSfSymbol ? 'contain' : 'cover';
80
+ }
81
+
82
+ /** Native reads only the two-edge object form */
83
+ export function resolveContentPosition(
84
+ contentPosition?: IImageContentPosition,
85
+ ): IImageContentPositionObject {
86
+ if (typeof contentPosition !== 'string') {
87
+ return contentPosition ?? CENTER;
88
+ }
89
+ const resolved = POSITION_BY_KEYWORD[contentPosition];
90
+ if (!resolved) {
91
+ warnOnce(`Content position "${contentPosition}" is invalid`);
92
+ return CENTER;
93
+ }
94
+ return resolved;
95
+ }
96
+
97
+ /** Every form of `sfEffect` becomes a list of effect objects */
98
+ export function resolveSfEffect(
99
+ sfEffect: IImageViewProps['sfEffect'],
100
+ ): ISfSymbolEffectObject[] | null {
101
+ if (sfEffect == null) {
102
+ return null;
103
+ }
104
+ const effects = Array.isArray(sfEffect) ? sfEffect : [sfEffect];
105
+ return effects.map(item =>
106
+ typeof item === 'string' ? { effect: item } : item,
107
+ );
108
+ }
109
+
110
+ /** A number is the duration of a cross dissolve */
111
+ export function resolveTransition(
112
+ transition?: IImageViewProps['transition'],
113
+ fadeDuration?: IImageViewProps['fadeDuration'],
114
+ ): IImageTransition | null {
115
+ if (typeof transition === 'number') {
116
+ return { duration: transition };
117
+ }
118
+ if (!transition && typeof fadeDuration === 'number') {
119
+ warnOnce('Prop "fadeDuration" is deprecated, use "transition" instead');
120
+ return { duration: fadeDuration };
121
+ }
122
+ return transition ?? null;
123
+ }
@@ -0,0 +1,75 @@
1
+ import { resolveAssetSource } from '@symbiote-native/engine';
2
+ import { SharedRef } from 'expo-modules-core';
3
+ import { SF_SYMBOL_PREFIX } from './constants';
4
+ import { resolveBlurhashString, resolveThumbhashString } from './hash-strings';
5
+ import type { IImageSource, IImageSources } from './types';
6
+
7
+ const BLURHASH_PATTERN =
8
+ /^(blurhash:\/)+[\w#$%*+,\-.:;=?@[\]^_{}|~]+(\/[\d.]+)*$/;
9
+ const THUMBHASH_PREFIX = 'thumbhash:/';
10
+
11
+ type IImageSourceInput = IImageSource | string | number | null | undefined;
12
+
13
+ // Any native image reference, an `ImageRef` or a `VideoThumbnail`, the view takes both
14
+ export function isImageRef(value: unknown): value is SharedRef<'image'> {
15
+ return value instanceof SharedRef && value.nativeRefType === 'image';
16
+ }
17
+
18
+ function sharedIdOf(ref: SharedRef<'image'>): number {
19
+ const id: unknown = Reflect.get(ref, '__expo_shared_object_id__');
20
+ return typeof id === 'number' ? id : 0;
21
+ }
22
+
23
+ function isImageSource(value: unknown): value is IImageSource {
24
+ return typeof value === 'object' && value !== null;
25
+ }
26
+
27
+ function nativeAssetSource(id: number): IImageSource | null {
28
+ const asset = resolveAssetSource(id);
29
+ return isImageSource(asset) ? asset : null;
30
+ }
31
+
32
+ export function resolveSource(source: IImageSourceInput): IImageSource | null {
33
+ if (typeof source === 'string') {
34
+ if (BLURHASH_PATTERN.test(source)) {
35
+ return resolveBlurhashString(source);
36
+ }
37
+ if (source.startsWith(THUMBHASH_PREFIX)) {
38
+ return resolveThumbhashString(source);
39
+ }
40
+ if (source.startsWith(SF_SYMBOL_PREFIX)) {
41
+ return {
42
+ uri: `${SF_SYMBOL_PREFIX}/${source.slice(SF_SYMBOL_PREFIX.length)}`,
43
+ };
44
+ }
45
+ return { uri: source };
46
+ }
47
+ if (typeof source === 'number') {
48
+ return nativeAssetSource(source);
49
+ }
50
+ if (source?.blurhash || source?.thumbhash) {
51
+ const { blurhash, thumbhash, ...rest } = source;
52
+ const resolved = thumbhash
53
+ ? resolveThumbhashString(thumbhash)
54
+ : resolveBlurhashString(blurhash ?? '');
55
+ return { ...resolved, ...rest };
56
+ }
57
+ return source ?? null;
58
+ }
59
+
60
+ function isPresent(source: IImageSource | null): source is IImageSource {
61
+ return source !== null;
62
+ }
63
+
64
+ /** The shape the native view takes: a list of sources or a shared object id */
65
+ export function resolveSources(
66
+ sources: IImageSources | undefined,
67
+ ): IImageSource[] | number {
68
+ if (Array.isArray(sources)) {
69
+ return sources.map(resolveSource).filter(isPresent);
70
+ }
71
+ if (isImageRef(sources)) {
72
+ return sharedIdOf(sources);
73
+ }
74
+ return [resolveSource(sources)].filter(isPresent);
75
+ }