@imgly/editor-react-native 1.40.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 (48) hide show
  1. package/IMGLYEditorModule.podspec +21 -0
  2. package/LICENSE.md +137 -0
  3. package/README.md +31 -0
  4. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  5. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  6. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  7. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  8. package/android/.gradle/8.9/gc.properties +0 -0
  9. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  10. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  11. package/android/.gradle/vcs-1/gc.properties +0 -0
  12. package/android/build.gradle +65 -0
  13. package/android/src/main/AndroidManifest.xml +8 -0
  14. package/android/src/main/assets/photo-ui-empty.png +0 -0
  15. package/android/src/main/kotlin/ly/img/editor/reactnative/module/IMGLYEditorModule.kt +252 -0
  16. package/android/src/main/kotlin/ly/img/editor/reactnative/module/IMGLYEditorPackage.kt +35 -0
  17. package/android/src/main/kotlin/ly/img/editor/reactnative/module/activity/EditorActivity.kt +112 -0
  18. package/android/src/main/kotlin/ly/img/editor/reactnative/module/builder/EditorBuilder.kt +478 -0
  19. package/android/src/main/kotlin/ly/img/editor/reactnative/module/helper/AssetHelper.kt +18 -0
  20. package/android/src/main/kotlin/ly/img/editor/reactnative/module/helper/IntentHelper.kt +61 -0
  21. package/android/src/main/kotlin/ly/img/editor/reactnative/module/model/EditorPreset.kt +47 -0
  22. package/android/src/main/kotlin/ly/img/editor/reactnative/module/model/EditorResult.kt +73 -0
  23. package/android/src/main/kotlin/ly/img/editor/reactnative/module/model/EditorSettings.kt +63 -0
  24. package/android/src/main/kotlin/ly/img/editor/reactnative/module/model/EditorSourceType.kt +39 -0
  25. package/android/src/main/kotlin/ly/img/editor/reactnative/module/model/Source.kt +44 -0
  26. package/android/src/newarch/IMGLYEditorModuleSpec.kt +7 -0
  27. package/android/src/oldarch/IMGLYEditorModuleSpec.kt +18 -0
  28. package/index.d.ts +3 -0
  29. package/index.js +3 -0
  30. package/ios/Constants/IMGLYConstants.h +13 -0
  31. package/ios/Constants/IMGLYConstants.m +13 -0
  32. package/ios/Extensions/OnCreate+.swift +75 -0
  33. package/ios/Extensions/OnExport+.swift +64 -0
  34. package/ios/Extensions/String+.swift +7 -0
  35. package/ios/Helper/ExportHelper.swift +67 -0
  36. package/ios/IMGLYEditorModule-Bridging-Header.h +3 -0
  37. package/ios/IMGLYEditorModule.h +11 -0
  38. package/ios/IMGLYEditorModule.mm +86 -0
  39. package/ios/IMGLYEditorModule.swift +110 -0
  40. package/ios/Model/EditorBuilder.swift +293 -0
  41. package/ios/Model/EditorPreset.swift +52 -0
  42. package/ios/Model/EditorResult.swift +27 -0
  43. package/ios/Model/EditorSettings.swift +33 -0
  44. package/ios/Model/EditorSourceType.swift +11 -0
  45. package/ios/Model/Source.swift +8 -0
  46. package/ios/View/ModalEditor.swift +55 -0
  47. package/package.json +46 -0
  48. package/src/NativeIMGLYEditor.ts +195 -0
@@ -0,0 +1,112 @@
1
+ package ly.img.editor.reactnative.module.activity
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.os.Build
6
+ import android.os.Bundle
7
+ import android.os.Parcelable
8
+ import androidx.activity.ComponentActivity
9
+ import androidx.activity.compose.setContent
10
+ import ly.img.editor.reactnative.module.IMGLYEditorModule
11
+ import ly.img.editor.reactnative.module.builder.Builder
12
+ import ly.img.editor.reactnative.module.builder.CustomBuilderScope
13
+ import ly.img.editor.reactnative.module.builder.EditorBuilder
14
+ import ly.img.editor.reactnative.module.helper.IntentHelper
15
+ import ly.img.editor.reactnative.module.model.EditorPreset
16
+ import ly.img.editor.reactnative.module.model.EditorSettings
17
+ import java.io.Serializable
18
+
19
+ /** The [Activity] used to display the editor. */
20
+ class EditorActivity : ComponentActivity() {
21
+ companion object {
22
+ /** The request code for this activity. */
23
+ var INTENT_REQUEST_CODE = 29055
24
+
25
+ /**
26
+ * The result code for this activity in
27
+ * case of an error.
28
+ */
29
+ var INTENT_RESULT_ERROR_CODE = 29056
30
+
31
+ /** The identifier to the config intent extra. */
32
+ const val INTENT_EXTRA_CONFIG = "intent_extra_config_cesdk"
33
+
34
+ /** The identifier to the preset intent extra. */
35
+ const val INTENT_EXTRA_PRESET = "intent_extra_preset_cesdk"
36
+
37
+ /** The identifier to the metadata intent extra. */
38
+ const val INTENT_EXTRA_METADATA = "intent_extra_metadata_cesdk"
39
+
40
+ /** The identifier to the result intent extra. */
41
+ const val INTENT_EXTRA_RESULT = "intent_extra_result_cesdk"
42
+ }
43
+
44
+ override fun onCreate(savedInstanceState: Bundle?) {
45
+ super.onCreate(savedInstanceState)
46
+
47
+ val config =
48
+ intent.extras?.getParcelableCompat<EditorSettings>(INTENT_EXTRA_CONFIG)
49
+ ?: return
50
+ val preset = intent.extras?.getParcelableCompat<EditorPreset>(INTENT_EXTRA_PRESET) ?: EditorPreset.DESIGN
51
+ val metadataBundle = intent.extras?.getParcelableCompat<Bundle>(INTENT_EXTRA_METADATA)
52
+ val metadata = IntentHelper.bundleToMap(metadataBundle)
53
+ val builder: Builder = IMGLYEditorModule.builderClosure?.invoke(preset, metadata) ?: builderForPreset(preset)
54
+
55
+ setContent {
56
+ builder(
57
+ CustomBuilderScope(config, preset, metadata, { result ->
58
+ val (resolvedResult, resultCode) =
59
+ result.fold(
60
+ onFailure = { error ->
61
+ Pair(error, INTENT_RESULT_ERROR_CODE)
62
+ },
63
+ onSuccess = { value ->
64
+ Pair(value, Activity.RESULT_OK)
65
+ },
66
+ )
67
+
68
+ val resultIntent =
69
+ Intent().apply {
70
+ when (resultCode) {
71
+ INTENT_RESULT_ERROR_CODE -> {
72
+ putExtra(INTENT_EXTRA_RESULT, resolvedResult as Serializable)
73
+ }
74
+
75
+ Activity.RESULT_OK -> {
76
+ putExtra(INTENT_EXTRA_RESULT, resolvedResult as Parcelable)
77
+ }
78
+ }
79
+ }
80
+
81
+ setResult(resultCode, resultIntent)
82
+ finish()
83
+ }, {
84
+ if (it != null) {
85
+ val resultIntent =
86
+ Intent().apply {
87
+ putExtra(INTENT_EXTRA_RESULT, it)
88
+ }
89
+ setResult(INTENT_RESULT_ERROR_CODE, resultIntent)
90
+ }
91
+ finish()
92
+ }),
93
+ )
94
+ }
95
+ }
96
+
97
+ private fun builderForPreset(preset: EditorPreset): Builder =
98
+ when (preset) {
99
+ EditorPreset.APPAREL -> EditorBuilder.apparel()
100
+ EditorPreset.POSTCARD -> EditorBuilder.postcard()
101
+ EditorPreset.PHOTO -> EditorBuilder.photo()
102
+ EditorPreset.DESIGN -> EditorBuilder.design()
103
+ EditorPreset.VIDEO -> EditorBuilder.video()
104
+ }
105
+
106
+ private inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String): T? =
107
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
108
+ getParcelable(key, T::class.java)
109
+ } else {
110
+ getParcelable(key)
111
+ }
112
+ }
@@ -0,0 +1,478 @@
1
+ package ly.img.editor.reactnative.module.builder
2
+
3
+ import android.net.Uri
4
+ import androidx.compose.runtime.Composable
5
+ import kotlinx.coroutines.Dispatchers
6
+ import kotlinx.coroutines.Job
7
+ import kotlinx.coroutines.coroutineScope
8
+ import kotlinx.coroutines.flow.toList
9
+ import kotlinx.coroutines.launch
10
+ import kotlinx.coroutines.withContext
11
+ import ly.img.editor.ApparelEditor
12
+ import ly.img.editor.DesignEditor
13
+ import ly.img.editor.DismissVideoExportEvent
14
+ import ly.img.editor.EditorDefaults
15
+ import ly.img.editor.EngineConfiguration
16
+ import ly.img.editor.HideLoading
17
+ import ly.img.editor.PhotoEditor
18
+ import ly.img.editor.PostcardEditor
19
+ import ly.img.editor.ShowLoading
20
+ import ly.img.editor.ShowVideoExportProgressEvent
21
+ import ly.img.editor.VideoEditor
22
+ import ly.img.editor.core.event.EditorEventHandler
23
+ import ly.img.editor.core.library.data.TextAssetSource
24
+ import ly.img.editor.core.library.data.TypefaceProvider
25
+ import ly.img.editor.reactnative.module.model.EditorPreset
26
+ import ly.img.editor.reactnative.module.model.EditorResult
27
+ import ly.img.editor.reactnative.module.model.EditorSettings
28
+ import ly.img.editor.reactnative.module.model.EditorSourceType
29
+ import ly.img.engine.DemoAssetSource
30
+ import ly.img.engine.DesignBlock
31
+ import ly.img.engine.DesignBlockType
32
+ import ly.img.engine.Engine
33
+ import ly.img.engine.MimeType
34
+ import ly.img.engine.addDefaultAssetSources
35
+ import ly.img.engine.addDemoAssetSources
36
+ import java.io.File
37
+ import java.util.UUID
38
+ import kotlin.coroutines.cancellation.CancellationException
39
+
40
+ /** A closure used to return a [Result] with type [EditorResult] from the editor. */
41
+ typealias EditorBuilderResult = (Result<EditorResult?>) -> Unit
42
+
43
+ /** Closure allowing to return a custom UI based on various input. */
44
+ typealias Builder = @Composable CustomBuilderScope.() -> Unit
45
+
46
+ /**
47
+ * @property settings The [EditorSettings] to configure the editor.
48
+ * @property preset The [EditorPreset] to open.
49
+ * @property metadata The custom metadata.
50
+ * @property result The [EditorBuilderResult] handling the result.
51
+ * @property onClose The closure to handle editor closing.
52
+ */
53
+ data class CustomBuilderScope(
54
+ val settings: EditorSettings,
55
+ val preset: EditorPreset,
56
+ val metadata: Map<String, Any>?,
57
+ val result: EditorBuilderResult,
58
+ val onClose: (Throwable?) -> Unit,
59
+ )
60
+
61
+ /** The [EditorBuilder] is used to build different editor configurations */
62
+ class EditorBuilder {
63
+ companion object {
64
+ /** The default design editor. */
65
+ fun design(): Builder =
66
+ {
67
+ ModalDesignEditor(settings, result, onClose)
68
+ }
69
+
70
+ /** The default postcard editor. */
71
+ fun postcard(): Builder =
72
+ {
73
+ ModalPostcardEditor(settings, result, onClose)
74
+ }
75
+
76
+ /** The default apparel editor. */
77
+ fun apparel(): Builder =
78
+ {
79
+ ModalApparelEditor(settings, result, onClose)
80
+ }
81
+
82
+ /** The default photo editor. */
83
+ fun photo(): Builder =
84
+ {
85
+ ModalPhotoEditor(settings, result, onClose)
86
+ }
87
+
88
+ /** The default video editor. */
89
+ fun video(): Builder =
90
+ {
91
+ ModalVideoEditor(settings, result, onClose)
92
+ }
93
+
94
+ /**
95
+ * A custom editor implementation.
96
+ * @param contentProvider A closure to provide a custom view.
97
+ * @return A [Builder] providing the custom view.
98
+ */
99
+ fun custom(contentProvider: @Composable CustomBuilderScope.() -> Unit): Builder =
100
+ {
101
+ val scope = CustomBuilderScope(settings, preset, metadata, result, onClose)
102
+ scope.contentProvider()
103
+ }
104
+
105
+ @Composable
106
+ private fun ModalPhotoEditor(
107
+ settings: EditorSettings,
108
+ result: EditorBuilderResult,
109
+ onClose: (Throwable?) -> Unit,
110
+ ) {
111
+ val engineConfiguration =
112
+ EngineConfiguration.remember(
113
+ license = settings.license,
114
+ baseUri = Uri.parse(settings.baseUri),
115
+ userId = settings.userId,
116
+ onCreate = {
117
+ EditorBuilderDefaults.onCreate(
118
+ engine = editorContext.engine,
119
+ eventHandler = editorContext.eventHandler,
120
+ settings = settings,
121
+ defaultScene = EditorBuilderDefaults.defaultPhotoUri,
122
+ sourceType = EditorSourceType.IMAGE,
123
+ )
124
+ },
125
+ onExport = {
126
+ val export =
127
+ EditorBuilderDefaults.onExport(
128
+ engine = editorContext.engine,
129
+ eventHandler = editorContext.eventHandler,
130
+ mimeType = MimeType.PNG,
131
+ )
132
+ result(Result.success(export))
133
+ },
134
+ )
135
+ PhotoEditor(engineConfiguration = engineConfiguration) {
136
+ onClose(it)
137
+ }
138
+ }
139
+
140
+ @Composable
141
+ private fun ModalApparelEditor(
142
+ settings: EditorSettings,
143
+ result: EditorBuilderResult,
144
+ onClose: (Throwable?) -> Unit,
145
+ ) {
146
+ val engineConfiguration =
147
+ EngineConfiguration.remember(
148
+ license = settings.license,
149
+ baseUri = Uri.parse(settings.baseUri),
150
+ userId = settings.userId,
151
+ onCreate = {
152
+ EditorBuilderDefaults.onCreate(
153
+ engine = editorContext.engine,
154
+ eventHandler = editorContext.eventHandler,
155
+ settings = settings,
156
+ defaultScene = EngineConfiguration.defaultApparelSceneUri,
157
+ )
158
+ },
159
+ onExport = {
160
+ val export =
161
+ EditorBuilderDefaults.onExport(
162
+ engine = editorContext.engine,
163
+ eventHandler = editorContext.eventHandler,
164
+ mimeType = MimeType.PDF,
165
+ )
166
+ result(Result.success(export))
167
+ },
168
+ )
169
+ ApparelEditor(engineConfiguration = engineConfiguration) {
170
+ onClose(it)
171
+ }
172
+ }
173
+
174
+ @Composable
175
+ private fun ModalDesignEditor(
176
+ settings: EditorSettings,
177
+ result: EditorBuilderResult,
178
+ onClose: (Throwable?) -> Unit,
179
+ ) {
180
+ val engineConfiguration =
181
+ EngineConfiguration.remember(
182
+ license = settings.license,
183
+ baseUri = Uri.parse(settings.baseUri),
184
+ userId = settings.userId,
185
+ onCreate = {
186
+ EditorBuilderDefaults.onCreate(
187
+ engine = editorContext.engine,
188
+ eventHandler = editorContext.eventHandler,
189
+ settings = settings,
190
+ defaultScene = EngineConfiguration.defaultDesignSceneUri,
191
+ )
192
+ },
193
+ onExport = {
194
+ val export =
195
+ EditorBuilderDefaults.onExport(
196
+ engine = editorContext.engine,
197
+ eventHandler = editorContext.eventHandler,
198
+ mimeType = MimeType.PDF,
199
+ )
200
+ result(Result.success(export))
201
+ },
202
+ )
203
+
204
+ DesignEditor(engineConfiguration = engineConfiguration) {
205
+ onClose(it)
206
+ }
207
+ }
208
+
209
+ @Composable
210
+ private fun ModalPostcardEditor(
211
+ settings: EditorSettings,
212
+ result: EditorBuilderResult,
213
+ onClose: (Throwable?) -> Unit,
214
+ ) {
215
+ val engineConfiguration =
216
+ EngineConfiguration.remember(
217
+ license = settings.license,
218
+ baseUri = Uri.parse(settings.baseUri),
219
+ userId = settings.userId,
220
+ onCreate = {
221
+ EditorBuilderDefaults.onCreate(
222
+ engine = editorContext.engine,
223
+ eventHandler = editorContext.eventHandler,
224
+ settings = settings,
225
+ defaultScene = EngineConfiguration.defaultPostcardSceneUri,
226
+ )
227
+ },
228
+ onExport = {
229
+ val export =
230
+ EditorBuilderDefaults.onExport(
231
+ engine = editorContext.engine,
232
+ eventHandler = editorContext.eventHandler,
233
+ mimeType = MimeType.PDF,
234
+ )
235
+ result(Result.success(export))
236
+ },
237
+ )
238
+
239
+ PostcardEditor(engineConfiguration = engineConfiguration) {
240
+ onClose(it)
241
+ }
242
+ }
243
+
244
+ @Composable
245
+ private fun ModalVideoEditor(
246
+ settings: EditorSettings,
247
+ result: EditorBuilderResult,
248
+ onClose: (Throwable?) -> Unit,
249
+ ) {
250
+ val engineConfiguration =
251
+ EngineConfiguration.remember(
252
+ license = settings.license,
253
+ baseUri = Uri.parse(settings.baseUri),
254
+ userId = settings.userId,
255
+ onCreate = {
256
+ EditorBuilderDefaults.onCreate(
257
+ engine = editorContext.engine,
258
+ eventHandler = editorContext.eventHandler,
259
+ settings = settings,
260
+ defaultScene = EngineConfiguration.defaultVideoSceneUri,
261
+ )
262
+ },
263
+ onExport = {
264
+ try {
265
+ val export =
266
+ EditorBuilderDefaults.onExportVideo(
267
+ engine = editorContext.engine,
268
+ eventHandler = editorContext.eventHandler,
269
+ mimeType = MimeType.MP4,
270
+ )
271
+ result(Result.success(export))
272
+ } catch (e: Exception) {
273
+ if (e !is CancellationException) {
274
+ result(Result.failure(e))
275
+ } else {
276
+ editorContext.eventHandler.send(DismissVideoExportEvent)
277
+ }
278
+ }
279
+ },
280
+ )
281
+ VideoEditor(engineConfiguration = engineConfiguration) {
282
+ onClose(it)
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ /** Default implementations to ease the editor configuration process. */
289
+ object EditorBuilderDefaults {
290
+ /** The default image uri for the photo editor. */
291
+ val defaultPhotoUri: Uri = Uri.parse("file:///android_asset/photo-ui-empty.png")
292
+
293
+ /**
294
+ * Default plugin implementation for the *onCreate* callback.
295
+ * @param engine The [Engine] that should be used.
296
+ * @param eventHandler The [EditorEventHandler] that should be used.
297
+ * @param settings The [EditorSettings] containing the relevant editor information.
298
+ * @param defaultScene The [Uri] of the defaultScene if no [EditorSettings.source] is specified.
299
+ * @param sourceType The [EditorSourceType] defining of which type the source is.
300
+ */
301
+ suspend fun onCreate(
302
+ engine: Engine,
303
+ eventHandler: EditorEventHandler,
304
+ settings: EditorSettings,
305
+ defaultScene: Uri,
306
+ sourceType: EditorSourceType = EditorSourceType.SCENE,
307
+ ) = coroutineScope {
308
+ fun isValidUri(uri: Uri): Boolean =
309
+ try {
310
+ uri.scheme != null && uri.path != null
311
+ } catch (e: Exception) {
312
+ false
313
+ }
314
+
315
+ val uri =
316
+ if (settings.source != null) {
317
+ val sourceUri = Uri.parse(settings.source?.source)
318
+ val isValid = isValidUri(sourceUri)
319
+ if (!isValid) {
320
+ throw IllegalArgumentException("The specified source is not a valid Uri.")
321
+ }
322
+ sourceUri
323
+ } else {
324
+ defaultScene
325
+ }
326
+
327
+ when (settings.source?.type ?: sourceType) {
328
+ EditorSourceType.IMAGE -> {
329
+ engine.scene.createFromImage(uri)
330
+ val graphicBlocks = engine.block.findByType(DesignBlockType.Graphic)
331
+ require(graphicBlocks.size == 1) { "No image found." }
332
+ val graphicBlock = graphicBlocks[0]
333
+ val pages = engine.scene.getPages()
334
+ require(pages.size == 1) { "No image found." }
335
+ val page = pages[0]
336
+ engine.block.setFill(page, engine.block.getFill(graphicBlock))
337
+ engine.block.destroy(graphicBlock)
338
+ }
339
+ EditorSourceType.VIDEO -> {
340
+ engine.scene.createFromVideo(uri)
341
+ }
342
+
343
+ else -> {
344
+ engine.scene.load(uri)
345
+ }
346
+ }
347
+
348
+ launch {
349
+ val assetBaseUri = settings.assetBaseUri ?: "https://cdn.img.ly/assets/v3"
350
+ val isValid = isValidUri(Uri.parse(assetBaseUri))
351
+ val baseUri = Uri.parse(if (isValid) assetBaseUri else "https://cdn.img.ly/assets/v3")
352
+ engine.addDefaultAssetSources(baseUri = baseUri)
353
+ val excluded = setOf(DemoAssetSource.IMAGE, DemoAssetSource.VIDEO, DemoAssetSource.AUDIO)
354
+ engine.addDemoAssetSources(sceneMode = engine.scene.getMode(), withUploadAssetSources = true, exclude = excluded)
355
+ val defaultTypeface = TypefaceProvider().provideTypeface(engine, "Roboto")
356
+ requireNotNull(defaultTypeface)
357
+ engine.asset.addSource(TextAssetSource(engine, defaultTypeface))
358
+ }
359
+ coroutineContext[Job]?.invokeOnCompletion {
360
+ eventHandler.send(HideLoading)
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Default plugin implementation for the *onExport* callback.
366
+ * @param engine The [Engine] that should be used.
367
+ * @param eventHandler The [EditorEventHandler] that should be used.
368
+ * @param mimeType The [MimeType] of the exported artifact.
369
+ * @param thumbnailHeight The height of the generated thumbnail.
370
+ */
371
+ suspend fun onExport(
372
+ engine: Engine,
373
+ eventHandler: EditorEventHandler,
374
+ mimeType: MimeType,
375
+ thumbnailHeight: Int = 100,
376
+ ): EditorResult {
377
+ EditorBuilderDefaults.run {
378
+ eventHandler.send(ShowLoading)
379
+ val blob =
380
+ engine.block.export(
381
+ block = requireNotNull(engine.scene.get()),
382
+ mimeType = mimeType,
383
+ ) {
384
+ scene.getPages().forEach {
385
+ block.setScopeEnabled(it, key = "layer/visibility", enabled = true)
386
+ block.setVisible(it, visible = true)
387
+ }
388
+ }
389
+ val tempFile = EditorDefaults.writeToTempFile(blob, mimeType)
390
+ val scene = engine.scene.get()
391
+ val sceneString =
392
+ scene?.let {
393
+ engine.scene.saveToString(it)
394
+ }
395
+ val sceneUri = sceneString?.let { saveScene(it) }
396
+ val firstPage = engine.scene.getPages().first()
397
+ val thumbnail = saveThumbnail(firstPage, engine, thumbnailHeight)
398
+ eventHandler.send(HideLoading)
399
+ return EditorResult(
400
+ scene = sceneUri.toString(),
401
+ artifact = Uri.fromFile(tempFile).toString(),
402
+ thumbnail = thumbnail.toString(),
403
+ )
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Default plugin implementation for the *onExport* callback for the video editor.
409
+ * @param engine The [Engine] that should be used.
410
+ * @param eventHandler The [EditorEventHandler] that should be used.
411
+ * @param mimeType The [MimeType] of the exported artifact.
412
+ * @param thumbnailHeight The height of the generated thumbnail.
413
+ */
414
+ suspend fun onExportVideo(
415
+ engine: Engine,
416
+ eventHandler: EditorEventHandler,
417
+ mimeType: MimeType,
418
+ thumbnailHeight: Int = 100,
419
+ ): EditorResult {
420
+ EditorBuilderDefaults.run {
421
+ eventHandler.send(ShowVideoExportProgressEvent(0f))
422
+
423
+ // First create the thumbnail and save the scene
424
+ // in order to finish before the video export sheet is done.
425
+ val page = engine.scene.getCurrentPage() ?: engine.scene.getPages().first()
426
+ val thumbnail = saveThumbnail(page, engine, thumbnailHeight)
427
+ val scene = engine.scene.get()
428
+ val sceneString =
429
+ scene?.let {
430
+ engine.scene.saveToString(it)
431
+ }
432
+
433
+ val buffer =
434
+ engine.block.exportVideo(
435
+ block = page,
436
+ timeOffset = 0.0,
437
+ duration = engine.block.getDuration(page),
438
+ mimeType = mimeType,
439
+ progressCallback = { progress ->
440
+ eventHandler.send(
441
+ ShowVideoExportProgressEvent(progress.encodedFrames.toFloat() / progress.totalFrames),
442
+ )
443
+ },
444
+ )
445
+
446
+ val tempFile = EditorDefaults.writeToTempFile(buffer, mimeType)
447
+ eventHandler.send(DismissVideoExportEvent)
448
+
449
+ return EditorResult(
450
+ scene = sceneString,
451
+ artifact = Uri.fromFile(tempFile).toString(),
452
+ thumbnail = thumbnail.toString(),
453
+ )
454
+ }
455
+ }
456
+
457
+ private suspend fun saveScene(scene: String): Uri =
458
+ withContext(Dispatchers.IO) {
459
+ val file =
460
+ File
461
+ .createTempFile("cesdk_export_scene_" + UUID.randomUUID().toString(), ".scene")
462
+ file.writeText(scene)
463
+ Uri.fromFile(file)
464
+ }
465
+
466
+ private suspend fun saveThumbnail(
467
+ id: DesignBlock,
468
+ engine: Engine,
469
+ height: Int,
470
+ ): Uri {
471
+ val frames =
472
+ engine.block.generateVideoThumbnailSequence(id, height, timeBegin = 0.0, timeEnd = 0.1, numberOfFrames = 1).toList()
473
+ val firstFrame = frames.first()
474
+ val buffer = firstFrame.imageData
475
+ val tempFile = EditorDefaults.writeToTempFile(buffer, MimeType.PNG)
476
+ return Uri.fromFile(tempFile)
477
+ }
478
+ }
@@ -0,0 +1,18 @@
1
+ package ly.img.editor.reactnative.module.helper
2
+
3
+ import android.net.Uri
4
+
5
+ /** Helper to use resolve assets. */
6
+ class AssetHelper {
7
+ companion object {
8
+ /**
9
+ * Validates the source of a given asset and returns the Uri if succeeded.
10
+ * @param source The source of the asset.
11
+ * @return The resolved [Uri] as a [String].
12
+ */
13
+ fun validateAssetSource(source: String): String? {
14
+ fun uriOrNull(uri: Uri?): String? = uri.takeIf { uri?.scheme != null && uri.path != null }?.toString()
15
+ return uriOrNull(Uri.parse(source))
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,61 @@
1
+ package ly.img.editor.reactnative.module.helper
2
+
3
+ import android.os.Bundle
4
+ import android.os.Parcelable
5
+ import java.io.Serializable
6
+
7
+ /** A helper to parse Maps for intents */
8
+ class IntentHelper {
9
+ companion object {
10
+ /**
11
+ * Converts a [Map] to a [Bundle].
12
+ * @param map The map to convert.
13
+ * @return The bundle.
14
+ */
15
+ fun mapToBundle(map: Map<*, *>?): Bundle {
16
+ val bundle = Bundle()
17
+ if (map != null) {
18
+ for ((key, value) in map) {
19
+ if (key is String) {
20
+ when (value) {
21
+ is Int -> bundle.putInt(key, value)
22
+ is Long -> bundle.putLong(key, value)
23
+ is String -> bundle.putString(key, value)
24
+ is Float -> bundle.putFloat(key, value)
25
+ is Double -> bundle.putDouble(key, value)
26
+ is Boolean -> bundle.putBoolean(key, value)
27
+ is Bundle -> bundle.putBundle(key, value)
28
+ is Map<*, *> -> bundle.putBundle(key, mapToBundle(value))
29
+ is Parcelable -> bundle.putParcelable(key, value)
30
+ is Serializable -> bundle.putSerializable(key, value)
31
+ // Add other types as needed
32
+ else -> throw IllegalArgumentException("Unsupported value type: ${value?.javaClass}")
33
+ }
34
+ } else {
35
+ throw IllegalArgumentException("Unsupported key type: ${key?.javaClass}")
36
+ }
37
+ }
38
+ }
39
+ return bundle
40
+ }
41
+
42
+ /**
43
+ * Converts a [Bundle] to a *Map<String, Any>*
44
+ * @param bundle The [Bundle] to convert.
45
+ * @return The map.
46
+ */
47
+ fun bundleToMap(bundle: Bundle?): Map<String, Any>? {
48
+ bundle ?: return null
49
+ val map = mutableMapOf<String, Any>()
50
+ for (key in bundle.keySet()) {
51
+ val value = bundle.get(key)
52
+ if (value is Bundle) {
53
+ map[key] = bundleToMap(value)!!
54
+ } else if (value != null) {
55
+ map[key] = value
56
+ }
57
+ }
58
+ return map
59
+ }
60
+ }
61
+ }