@octane-xplat/image 0.0.0-stage → 0.11.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -2
  3. package/dist/native/Image.tsrx.js +264 -0
  4. package/dist/native/cache.js +64 -0
  5. package/dist/native/hash-image.js +285 -0
  6. package/dist/native/image-binding.js +103 -0
  7. package/dist/native/image-content.js +207 -0
  8. package/dist/native/index.js +3 -0
  9. package/dist/native/octane-client-build.json +8 -0
  10. package/dist/native/prefetch-batch.js +27 -0
  11. package/dist/web/Image.web.tsrx.js +123 -0
  12. package/dist/web/cache.web.js +35 -0
  13. package/dist/web/hash-image.js +285 -0
  14. package/dist/web/image-content.js +134 -0
  15. package/dist/web/index.js +3 -0
  16. package/dist/web/octane-client-build.json +8 -0
  17. package/dist/web/prefetch-batch.js +27 -0
  18. package/package.json +83 -3
  19. package/src/Image.macos.tsrx +24 -0
  20. package/src/Image.tsrx +269 -0
  21. package/src/Image.web.tsrx +176 -0
  22. package/src/cache.macos.ts +31 -0
  23. package/src/cache.ts +93 -0
  24. package/src/cache.web.ts +49 -0
  25. package/src/hash-image.ts +313 -0
  26. package/src/image-binding.ts +170 -0
  27. package/src/image-content.ts +310 -0
  28. package/src/index.macos.ts +26 -0
  29. package/src/index.ts +28 -0
  30. package/src/index.web.ts +28 -0
  31. package/src/native-elements.d.ts +8 -0
  32. package/src/prefetch-batch.ts +50 -0
  33. package/src/prefetch.web.test.ts +105 -0
  34. package/src/props.ts +119 -0
  35. package/types/generated/.tsrx-typegen-manifest.json +16 -0
  36. package/types/generated/Image.d.ts +16 -0
  37. package/types/generated/Image.web.d.ts +12 -0
  38. package/types/generated/cache.d.ts +28 -0
  39. package/types/generated/cache.web.d.ts +16 -0
  40. package/types/generated/hash-image.d.ts +14 -0
  41. package/types/generated/image-binding.d.ts +40 -0
  42. package/types/generated/image-content.d.ts +57 -0
  43. package/types/generated/index.d.ts +3 -0
  44. package/types/generated/index.web.d.ts +3 -0
  45. package/types/generated/prefetch-batch.d.ts +10 -0
  46. package/types/generated/props.d.ts +99 -0
  47. package/types/index.macos.d.ts +28 -0
@@ -0,0 +1,310 @@
1
+ import type {
2
+ ImageContentFit,
3
+ ImageContentPosition,
4
+ ImageContentPositionObject,
5
+ ImageContentPositionValue,
6
+ ImageSourceLike,
7
+ } from './props'
8
+
9
+ /** VENDORED from packages/ui/src/image-content.ts — keep in sync.
10
+ * Shared `contentFit`/`contentPosition` + multi-source math for `Image` —
11
+ * pure functions used by every leaf (docs/notes/expo-image-study.md L7/L8).
12
+ * Semantics mirror CSS `object-fit`/`object-position`, which is also
13
+ * expo-image's contract: the leaves compute the drawn-content rect
14
+ * themselves instead of delegating to platform scaleType/contentMode,
15
+ * so all platforms get the same answer.
16
+ *
17
+ * The default fit is 'cover' everywhere (the expo/RN default). Today's
18
+ * platform defaults disagree — web <img> is 'fill' and NS stretch is
19
+ * 'aspectFit' — so an un-propped Image was silently divergent. */
20
+
21
+ export const DEFAULT_CONTENT_FIT: ImageContentFit = 'cover'
22
+
23
+ export function resolveContentFit(fit?: ImageContentFit): ImageContentFit {
24
+ return fit ?? DEFAULT_CONTENT_FIT
25
+ }
26
+
27
+ /** expo-image's keyword shorthands; the object form positions relative to
28
+ * any corner pair ({top,left}, {top,right}, {bottom,left}, {bottom,right}). */
29
+ const POSITION_KEYWORDS: Record<string, ImageContentPositionObject> = {
30
+ center: { top: '50%', left: '50%' },
31
+ top: { top: 0, left: '50%' },
32
+ right: { top: '50%', right: 0 },
33
+ bottom: { bottom: 0, left: '50%' },
34
+ left: { top: '50%', left: 0 },
35
+ 'top center': { top: 0, left: '50%' },
36
+ 'top right': { top: 0, right: 0 },
37
+ 'top left': { top: 0, left: 0 },
38
+ 'right center': { top: '50%', right: 0 },
39
+ 'right top': { top: 0, right: 0 },
40
+ 'right bottom': { bottom: 0, right: 0 },
41
+ 'bottom center': { bottom: 0, left: '50%' },
42
+ 'bottom right': { bottom: 0, right: 0 },
43
+ 'bottom left': { bottom: 0, left: 0 },
44
+ 'left center': { top: '50%', left: 0 },
45
+ 'left top': { top: 0, left: 0 },
46
+ 'left bottom': { bottom: 0, left: 0 },
47
+ }
48
+
49
+ export function resolveContentPosition(
50
+ position?: ImageContentPosition,
51
+ ): ImageContentPositionObject {
52
+ if (typeof position === 'string') {
53
+ return POSITION_KEYWORDS[position] ?? POSITION_KEYWORDS.center
54
+ }
55
+
56
+ return position ?? POSITION_KEYWORDS.center
57
+ }
58
+
59
+ type AxisValue = { percent: number } | { dips: number }
60
+
61
+ function parseAxisValue(value: ImageContentPositionValue | undefined): AxisValue | null {
62
+ if (value == null) {
63
+ return null
64
+ }
65
+
66
+ if (typeof value === 'number') {
67
+ return { dips: value }
68
+ }
69
+
70
+ const text = String(value).trim()
71
+ if (text === 'center') {
72
+ return { percent: 50 }
73
+ }
74
+
75
+ if (text.endsWith('%')) {
76
+ const parsed = Number.parseFloat(text)
77
+ return Number.isNaN(parsed) ? null : { percent: parsed }
78
+ }
79
+
80
+ const parsed = Number.parseFloat(text)
81
+ return Number.isNaN(parsed) ? null : { dips: parsed }
82
+ }
83
+
84
+ /** Offset of the drawn content's start edge inside the view along one axis.
85
+ * expo-image `calcTranslation`: percentages run over the free space
86
+ * (view − content), absolute dips measure from the named edge, and the
87
+ * reverse edge (right/bottom) counts back from the far side. */
88
+ export function contentAxisOffset(
89
+ viewSize: number,
90
+ contentSize: number,
91
+ start: ImageContentPositionValue | undefined,
92
+ end: ImageContentPositionValue | undefined,
93
+ ): number {
94
+ const from = parseAxisValue(start)
95
+ if (from) {
96
+ return 'percent' in from ? (from.percent / 100) * (viewSize - contentSize) : from.dips
97
+ }
98
+
99
+ const to = parseAxisValue(end)
100
+ if (to) {
101
+ return 'percent' in to
102
+ ? (1 - to.percent / 100) * (viewSize - contentSize)
103
+ : viewSize - contentSize - to.dips
104
+ }
105
+
106
+ return (viewSize - contentSize) / 2
107
+ }
108
+
109
+ function centeredAxis(
110
+ start: ImageContentPositionValue | undefined,
111
+ end: ImageContentPositionValue | undefined,
112
+ ): boolean {
113
+ const from = parseAxisValue(start)
114
+ if (from) {
115
+ return 'percent' in from && from.percent === 50
116
+ }
117
+
118
+ const to = parseAxisValue(end)
119
+ if (to) {
120
+ return 'percent' in to && to.percent === 50
121
+ }
122
+
123
+ return true
124
+ }
125
+
126
+ export function isCenteredContentPosition(position: ImageContentPositionObject): boolean {
127
+ return (
128
+ centeredAxis(position.left, position.right) && centeredAxis(position.top, position.bottom)
129
+ )
130
+ }
131
+
132
+ /** The rect the bitmap occupies inside the view box, in the same units the
133
+ * caller measures with (native: dips; web: CSS px via object-fit anyway —
134
+ * only the native leaf calls this). Every length unit is dips except the
135
+ * intrinsic size, which the leaf must supply in dips too. */
136
+ export function contentRect(
137
+ fit: ImageContentFit,
138
+ position: ImageContentPositionObject,
139
+ viewWidth: number,
140
+ viewHeight: number,
141
+ imageWidth: number,
142
+ imageHeight: number,
143
+ ): { x: number; y: number; width: number; height: number } {
144
+ let width: number
145
+ let height: number
146
+ switch (fit) {
147
+ case 'fill':
148
+ width = viewWidth
149
+ height = viewHeight
150
+ break
151
+ case 'cover': {
152
+ const scale = Math.max(viewWidth / imageWidth, viewHeight / imageHeight)
153
+ width = imageWidth * scale
154
+ height = imageHeight * scale
155
+ break
156
+ }
157
+ case 'scale-down': {
158
+ const scale = Math.min(1, Math.min(viewWidth / imageWidth, viewHeight / imageHeight))
159
+ width = imageWidth * scale
160
+ height = imageHeight * scale
161
+ break
162
+ }
163
+ case 'none':
164
+ width = imageWidth
165
+ height = imageHeight
166
+ break
167
+ default: {
168
+ // 'contain'
169
+ const scale = Math.min(viewWidth / imageWidth, viewHeight / imageHeight)
170
+ width = imageWidth * scale
171
+ height = imageHeight * scale
172
+ }
173
+ }
174
+
175
+ return {
176
+ x: contentAxisOffset(viewWidth, width, position.left, position.right),
177
+ y: contentAxisOffset(viewHeight, height, position.top, position.bottom),
178
+ width,
179
+ height,
180
+ }
181
+ }
182
+
183
+ /** True when no platform stretch/scaleType can express the request and the
184
+ * leaf must measure + lay out the image itself: 'none' (iOS maps NS
185
+ * stretch='none' to TopLeft, not CSS centered), 'scale-down' (needs the
186
+ * intrinsic-vs-view compare), or any non-center position. 'fill' is
187
+ * exempt — its rect is always the whole box, so position can't matter. */
188
+ export function needsContentRect(
189
+ fit: ImageContentFit,
190
+ position: ImageContentPositionObject,
191
+ ): boolean {
192
+ if (fit === 'none' || fit === 'scale-down') {
193
+ return true
194
+ }
195
+
196
+ return fit !== 'fill' && !isCenteredContentPosition(position)
197
+ }
198
+
199
+ /** NS `stretch` for the fast path — only reached for fits the platform
200
+ * vocabulary expresses identically (contain/cover/fill centered). */
201
+ export function contentFitToStretch(fit: ImageContentFit): 'aspectFit' | 'aspectFill' | 'fill' {
202
+ switch (fit) {
203
+ case 'contain':
204
+ return 'aspectFit'
205
+ case 'fill':
206
+ return 'fill'
207
+ default:
208
+ return 'aspectFill'
209
+ }
210
+ }
211
+
212
+ /** L8 — expo `getBestSource`: the source whose declared pixel count
213
+ * (`width·height·scale²`, dips × density) is closest to the view's pixel
214
+ * count. Needs a real measured size; one source or no size picks nothing
215
+ * to decide. Unsized sources score worst and are skipped when any sized
216
+ * candidate exists. */
217
+ export function bestImageSource(
218
+ sources: readonly ImageSourceLike[],
219
+ viewWidth: number,
220
+ viewHeight: number,
221
+ screenScale: number,
222
+ ): ImageSourceLike | null {
223
+ if (sources.length === 0 || viewWidth <= 0 || viewHeight <= 0) {
224
+ return null
225
+ }
226
+
227
+ if (sources.length === 1) {
228
+ return sources[0]
229
+ }
230
+
231
+ const target = viewWidth * viewHeight * screenScale * screenScale
232
+ let best: ImageSourceLike | null = null
233
+ let bestFit = Infinity
234
+ for (const source of sources) {
235
+ const pixelCount = (source.width ?? 0) * (source.height ?? 0) * (source.scale ?? 1) ** 2
236
+ const fit = Math.abs(1 - pixelCount / target)
237
+ if (fit < bestFit) {
238
+ best = source
239
+ bestFit = fit
240
+ }
241
+ }
242
+
243
+ return best
244
+ }
245
+
246
+ function ensureWebUnits(value: ImageContentPositionValue): string {
247
+ const text = String(value).trim()
248
+ return text.endsWith('%') ? text : `${text}px`
249
+ }
250
+
251
+ /** Browser `object-position` for a resolved position — mirrors expo-image
252
+ * web's `getObjectPositionFromContentPositionObject`. */
253
+ export function objectPositionCSSValue(position: ImageContentPositionObject): string {
254
+ const resolved: ImageContentPositionObject = { ...position }
255
+ if (resolved.top == null && resolved.bottom == null) {
256
+ resolved.top = '50%'
257
+ }
258
+
259
+ if (resolved.left == null && resolved.right == null) {
260
+ resolved.left = '50%'
261
+ }
262
+
263
+ return (
264
+ (['top', 'bottom', 'left', 'right'] as const)
265
+ .map((edge) => (resolved[edge] != null ? `${edge} ${ensureWebUnits(resolved[edge]!)}` : ''))
266
+ .filter((part) => part !== '')
267
+ .join(' ') || '50% 50%'
268
+ )
269
+ }
270
+
271
+ /** Browser `srcset` candidates for a source array: `w` descriptors (real
272
+ * file px = width·scale) when every source is sized, `x` descriptors when
273
+ * every source only carries density. `src` falls back to the largest
274
+ * declared pixel count — guaranteed coverage where srcset is ignored. */
275
+ export function imageSrcSet(sources: readonly ImageSourceLike[]): {
276
+ src: string
277
+ srcSet?: string
278
+ } {
279
+ if (sources.length === 0) {
280
+ return { src: '' }
281
+ }
282
+
283
+ const everyWidth = sources.every(
284
+ (source) => typeof source.width === 'number' && source.width > 0,
285
+ )
286
+
287
+ const everyScale =
288
+ !everyWidth &&
289
+ sources.every((source) => typeof source.scale === 'number' && source.scale > 0)
290
+
291
+ const srcSet = everyWidth
292
+ ? sources
293
+ .map((source) => `${source.uri} ${Math.round(source.width! * (source.scale ?? 1))}w`)
294
+ .join(', ')
295
+ : everyScale
296
+ ? sources.map((source) => `${source.uri} ${source.scale}x`).join(', ')
297
+ : undefined
298
+
299
+ let fallback = sources[0]
300
+ let bestCount = -Infinity
301
+ for (const source of sources) {
302
+ const count = (source.width ?? 0) * (source.height ?? 0) * (source.scale ?? 1) ** 2
303
+ if (count > bestCount) {
304
+ bestCount = count
305
+ fallback = source
306
+ }
307
+ }
308
+
309
+ return { src: fallback.uri, srcSet }
310
+ }
@@ -0,0 +1,26 @@
1
+ export { Image } from './Image.macos.tsrx'
2
+ export {
3
+ clearImageCaches,
4
+ evictImage,
5
+ initializeImageCache,
6
+ isImageCached,
7
+ prefetch,
8
+ } from './cache.macos'
9
+
10
+ export type {
11
+ ImageCacheConfig,
12
+ ImageCachePolicy,
13
+ ImageCacheQueryOptions,
14
+ ImageCacheState,
15
+ ImageContentFit,
16
+ ImageContentPosition,
17
+ ImageContentPositionObject,
18
+ ImageContentPositionString,
19
+ ImageContentPositionValue,
20
+ ImageDecoding,
21
+ ImageErrorEvent,
22
+ ImageLoadEvent,
23
+ ImageProps,
24
+ PrefetchOptions,
25
+ ImageSourceLike,
26
+ } from './props'
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Platform barrels — explicit .tsrx leaf imports (moduleSuffixes don't
2
+ // reach .tsrx; same convention as @octane-xplat/ui index.*.ts).
3
+ export { Image } from './Image.tsrx'
4
+ export {
5
+ clearImageCaches,
6
+ evictImage,
7
+ initializeImageCache,
8
+ isImageCached,
9
+ prefetch,
10
+ } from './cache'
11
+
12
+ export type {
13
+ ImageCacheConfig,
14
+ ImageCachePolicy,
15
+ ImageCacheQueryOptions,
16
+ ImageCacheState,
17
+ ImageContentFit,
18
+ ImageContentPosition,
19
+ ImageContentPositionObject,
20
+ ImageContentPositionString,
21
+ ImageContentPositionValue,
22
+ ImageDecoding,
23
+ ImageErrorEvent,
24
+ ImageLoadEvent,
25
+ ImageProps,
26
+ PrefetchOptions,
27
+ ImageSourceLike,
28
+ } from './props'
@@ -0,0 +1,28 @@
1
+ // Platform barrels — explicit .tsrx leaf imports (moduleSuffixes don't
2
+ // reach .tsrx; same convention as @octane-xplat/ui index.*.ts).
3
+ export { Image } from './Image.web.tsrx'
4
+ export {
5
+ clearImageCaches,
6
+ evictImage,
7
+ initializeImageCache,
8
+ isImageCached,
9
+ prefetch,
10
+ } from './cache.web'
11
+
12
+ export type {
13
+ ImageCacheConfig,
14
+ ImageCachePolicy,
15
+ ImageCacheQueryOptions,
16
+ ImageCacheState,
17
+ ImageContentFit,
18
+ ImageContentPosition,
19
+ ImageContentPositionObject,
20
+ ImageContentPositionString,
21
+ ImageContentPositionValue,
22
+ ImageDecoding,
23
+ ImageErrorEvent,
24
+ ImageLoadEvent,
25
+ ImageProps,
26
+ PrefetchOptions,
27
+ ImageSourceLike,
28
+ } from './props'
@@ -0,0 +1,8 @@
1
+ import { Img } from '@nativescript-community/ui-image'
2
+ import type { Attributes } from '@nativescript-community/octane/intrinsics'
3
+
4
+ declare module '@nativescript-community/octane/intrinsics' {
5
+ interface NativeScriptElements {
6
+ cachedimage: Attributes<typeof Img>
7
+ }
8
+ }
@@ -0,0 +1,50 @@
1
+ /** VENDORED — identical copy in packages/gif/src/prefetch-batch.ts; keep in
2
+ * sync.
3
+ *
4
+ * Shared batch driver for the image-engine leaves' `prefetch` contract:
5
+ * normalizes `string | string[]`, caps in-flight requests JS-side, and
6
+ * resolves `true` only when every URL warms — `false` on the first failure
7
+ * (in-flight requests settle on their own; the result is already decided). */
8
+
9
+ /** Matches the `maxRequests = 5` ceiling NS core's ImageCache used. */
10
+ export const DEFAULT_PREFETCH_CONCURRENCY = 5
11
+
12
+ export function prefetchBatch(
13
+ srcs: string | string[],
14
+ concurrency: number | undefined,
15
+ warm: (url: string) => Promise<boolean>,
16
+ ): Promise<boolean> {
17
+ const urls = Array.isArray(srcs) ? srcs : [srcs]
18
+ if (urls.length === 0) {
19
+ return Promise.resolve(true)
20
+ }
21
+
22
+ const requested = Math.floor(concurrency ?? DEFAULT_PREFETCH_CONCURRENCY)
23
+ const limit =
24
+ Number.isFinite(requested) && requested > 0 ? requested : DEFAULT_PREFETCH_CONCURRENCY
25
+
26
+ return new Promise<boolean>((resolve) => {
27
+ let next = 0
28
+ let active = Math.min(limit, urls.length)
29
+ for (let i = 0; i < active; i++) {
30
+ void (async () => {
31
+ while (next < urls.length) {
32
+ const url = urls[next++]
33
+ try {
34
+ if (!(await warm(url))) {
35
+ resolve(false)
36
+ return
37
+ }
38
+ } catch {
39
+ resolve(false)
40
+ return
41
+ }
42
+ }
43
+
44
+ if (--active === 0) {
45
+ resolve(true)
46
+ }
47
+ })()
48
+ }
49
+ })
50
+ }
@@ -0,0 +1,105 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { prefetch } from './cache.web'
3
+ import { prefetchBatch } from './prefetch-batch'
4
+
5
+ // Web prefetch contract: a throwaway <img> per URL, the promise awaited until
6
+ // load/error fires, false when any URL fails, and a JS-side concurrency cap —
7
+ // the same contract @octane-xplat/gif's prefetch carries.
8
+
9
+ class MockImage {
10
+ static instances: MockImage[] = []
11
+ onload: (() => void) | null = null
12
+ onerror: (() => void) | null = null
13
+ set src(_url: string) {
14
+ MockImage.instances.push(this)
15
+ }
16
+ load() {
17
+ this.onload?.()
18
+ }
19
+ fail() {
20
+ this.onerror?.()
21
+ }
22
+ }
23
+
24
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
25
+
26
+ async function drainPending(promise: Promise<boolean>) {
27
+ let result: boolean | undefined
28
+ void promise.then((value) => {
29
+ result = value
30
+ })
31
+
32
+ while (result === undefined) {
33
+ for (const image of MockImage.instances.splice(0)) {
34
+ image.load()
35
+ }
36
+
37
+ await flush()
38
+ }
39
+
40
+ return result
41
+ }
42
+
43
+ describe('prefetch (web)', () => {
44
+ afterEach(() => {
45
+ MockImage.instances = []
46
+ vi.unstubAllGlobals()
47
+ })
48
+
49
+ it('resolves true only after every <img> fires load', async () => {
50
+ vi.stubGlobal('Image', MockImage)
51
+ const promise = prefetch(['https://x/a.png', 'https://x/b.png'])
52
+ let settled = false
53
+ void promise.then(() => {
54
+ settled = true
55
+ })
56
+
57
+ await flush()
58
+ expect(settled).toBe(false) // bytes have not landed yet — honest await
59
+ expect(await drainPending(promise)).toBe(true)
60
+ })
61
+
62
+ it('resolves false when any URL fails', async () => {
63
+ vi.stubGlobal('Image', MockImage)
64
+ const promise = prefetch(['https://x/a.png', 'https://x/b.png'])
65
+ const [failed] = MockImage.instances.splice(0)
66
+ failed.fail()
67
+ expect(await promise).toBe(false)
68
+ })
69
+
70
+ it('accepts a single URL and resolves true for an empty batch', async () => {
71
+ vi.stubGlobal('Image', MockImage)
72
+ expect(await prefetch([])).toBe(true)
73
+ const promise = prefetch('https://x/a.png')
74
+ expect(MockImage.instances).toHaveLength(1)
75
+ expect(await drainPending(promise)).toBe(true)
76
+ })
77
+
78
+ it('caps in-flight loads at options.concurrency', async () => {
79
+ vi.stubGlobal('Image', MockImage)
80
+ const promise = prefetch(
81
+ Array.from({ length: 7 }, (_, i) => `https://x/${i}.png`),
82
+ { concurrency: 2, headers: { Authorization: 'Bearer x' } },
83
+ )
84
+
85
+ expect(MockImage.instances).toHaveLength(2)
86
+ MockImage.instances.splice(0).forEach((image) => image.load())
87
+ await flush()
88
+ await flush()
89
+ expect(MockImage.instances.length).toBeLessThanOrEqual(2)
90
+ expect(await drainPending(promise)).toBe(true)
91
+ })
92
+
93
+ it('defaults to five parallel loads', async () => {
94
+ vi.stubGlobal('Image', MockImage)
95
+ const promise = prefetch(Array.from({ length: 9 }, (_, i) => `https://x/${i}.png`))
96
+ expect(MockImage.instances).toHaveLength(5)
97
+ expect(await drainPending(promise)).toBe(true)
98
+ })
99
+ })
100
+
101
+ describe('prefetchBatch', () => {
102
+ it('reports false if the warm callback throws', async () => {
103
+ expect(await prefetchBatch(['a', 'b'], 5, () => Promise.reject(new Error('boom')))).toBe(false)
104
+ })
105
+ })
package/src/props.ts ADDED
@@ -0,0 +1,119 @@
1
+ import type { ImageProps as CoreImageProps } from '@octane-xplat/ui';
2
+
3
+ // The leaf's Image is a drop-in for `@octane-xplat/ui`'s Image — same name,
4
+ // same props. The core contract types come straight from ui so the leaf can
5
+ // never drift from them; swapping engines is an import-path change.
6
+ export type {
7
+ ImageContentFit,
8
+ ImageContentPosition,
9
+ ImageContentPositionObject,
10
+ ImageContentPositionString,
11
+ ImageContentPositionValue,
12
+ ImageDecoding,
13
+ ImageSourceLike,
14
+ } from '@octane-xplat/ui';
15
+
16
+ /** How the engine's caches participate in a load. The plugin exposes a single
17
+ * all-off switch (`noCache` → Glide `skipMemoryCache` + `DiskCacheStrategy.NONE`
18
+ * on Android, `SDWebImageOptions.FromLoaderOnly` on iOS), so the only honest
19
+ * non-default value is 'none'. 'memory'/'disk' splits are not representable
20
+ * through the plugin's request options today. Ignored on web — browsers own
21
+ * their HTTP cache. */
22
+ export type ImageCachePolicy = 'memory-disk' | 'none'
23
+
24
+ export interface ImageLoadEvent {
25
+ /** Decoded bitmap size in pixels. */
26
+ width: number
27
+ height: number
28
+ /** Where the engine served the image from. Absent on web — the browser
29
+ * does not report which cache (if any) satisfied an <img>. */
30
+ source?: 'network' | 'memory' | 'disk' | 'local'
31
+ }
32
+
33
+ export interface ImageErrorEvent {
34
+ error: Error
35
+ }
36
+
37
+ /** Cache sizing/eviction applied through `initializeImageCache`. Native only —
38
+ * the web engine (browser HTTP cache) offers no equivalent knobs. */
39
+ export interface ImageCacheConfig {
40
+ /** In-memory bitmap cache size in bytes (Android Glide). */
41
+ memoryCacheSize?: number
42
+ /** Size the memory cache as N fullscreen bitmaps (Android Glide). */
43
+ memoryCacheScreens?: number
44
+ /** Persist the cache-key registry across launches so evictions stay
45
+ * consistent (Android SharedPreferences store). */
46
+ persistentCacheKeys?: boolean
47
+ /** Bump to invalidate every cached entry — Glide `ObjectKey` signature. */
48
+ globalSignatureKey?: string
49
+ /** iOS: track transformed cache keys for eviction. Needed when decodeWidth/
50
+ * decodeHeight (or other transforms) are used alongside evictImage —
51
+ * transformed images key differently than the raw URL. */
52
+ iosComplexCacheEviction?: boolean
53
+ }
54
+
55
+ /** Result of `isImageCached`. 'none' also covers "unknown" — on web the
56
+ * browser cache is not queryable, so web always reports 'none'. */
57
+ export type ImageCacheState = 'memory' | 'disk' | 'none'
58
+
59
+ /** Options for `prefetch` — the same contract `@octane-xplat/gif`'s
60
+ * `prefetch` carries. */
61
+ export interface PrefetchOptions {
62
+ /** Request headers forwarded on Android and iOS (the framework patch
63
+ * restores iOS request modifiers). Ignored on web — a plain <img>
64
+ * cannot send custom headers. */
65
+ headers?: Record<string, string>
66
+ /** Max URLs fetched in parallel (JS-side cap). Default 5 — the same
67
+ * ceiling NS core's ImageCache used. */
68
+ concurrency?: number
69
+ }
70
+
71
+ /** Display-load context for `isImageCached`. On iOS the engine folds the
72
+ * transform a load used into the cache key — decode bounds are the common
73
+ * one, and the leaf always decodes to a size (explicit props or the view's
74
+ * measured size), so a bare probe misses entries a display load wrote.
75
+ * Repeat the same decode bounds here to probe the key a display load hits.
76
+ * Android keys probes on the URL alone and ignores these — but the plugin
77
+ * cannot see Glide's active resources there, so a bitmap still on screen
78
+ * may report 'none'. Web reports 'none' regardless. */
79
+ export interface ImageCacheQueryOptions {
80
+ /** Device-pixel decode bounds — same meaning as the `Image` props. */
81
+ decodeWidth?: number
82
+ decodeHeight?: number
83
+ }
84
+
85
+ /** Image — drop-in alternative to `@octane-xplat/ui`'s Image backed by a real
86
+ * image engine (Glide on Android, SDWebImage on iOS via
87
+ * `@nativescript-community/ui-image`): sized in-memory bitmap cache, disk
88
+ * cache, decode-to-view-size, placeholders and transitions. Web renders a
89
+ * plain <img>. Every `@octane-xplat/ui` Image prop is accepted with the same
90
+ * meaning — the members below are engine extensions with no core equivalent;
91
+ * they are ignored on web. SVG `src` is the one behavioral gap — keep core
92
+ * `Image` for those. One asymmetry: `decoding='sync'` is not honorably
93
+ * supported — Glide/SDWebImage have no synchronous decode mode (the plugin's
94
+ * `loadMode` prop is registered but never read), so 'sync' degrades to
95
+ * async on native with a console warning rather than a fake. */
96
+ export interface ImageProps extends CoreImageProps {
97
+ /** Native only — 'none' bypasses memory and disk caches for this image.
98
+ * Default 'memory-disk' leaves both caches active. */
99
+ cachePolicy?: ImageCachePolicy
100
+ /** Native only — image shown when `src` fails (`failureImageUri`). */
101
+ failureImage?: string
102
+ /** Native only — extra HTTP headers for the request (authenticated URLs). */
103
+ headers?: Record<string, string>
104
+ /** Native only — explicit decode bounds in device pixels. Default is the
105
+ * view's laid-out size (the src is held until first layout so the engine
106
+ * never decodes at full source resolution). */
107
+ decodeWidth?: number
108
+ decodeHeight?: number
109
+ /** Native only — progressive/interlaced rendering while bytes stream. */
110
+ progressive?: boolean
111
+ /** Native only — crossfade duration in ms when the image lands (0
112
+ * disables; engine default applies when unset). */
113
+ fadeDuration?: number
114
+ /** Fired when the final image is set. `source` reports which cache level
115
+ * served the bitmap — native only; web fires without `source`. */
116
+ onLoad?: (event: ImageLoadEvent) => void
117
+ /** Fired when the load fails. */
118
+ onError?: (event: ImageErrorEvent) => void
119
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": 1,
3
+ "files": [
4
+ "Image.d.ts",
5
+ "Image.web.d.ts",
6
+ "cache.d.ts",
7
+ "cache.web.d.ts",
8
+ "hash-image.d.ts",
9
+ "image-binding.d.ts",
10
+ "image-content.d.ts",
11
+ "index.d.ts",
12
+ "index.web.d.ts",
13
+ "prefetch-batch.d.ts",
14
+ "props.d.ts"
15
+ ]
16
+ }