diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/BasicWebViewSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/BasicWebViewSample.kt index d9f3a214..9d0001a6 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/BasicWebViewSample.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/BasicWebViewSample.kt @@ -1,12 +1,15 @@ package com.kevinnzou.sample import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.Button import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -14,6 +17,7 @@ import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack @@ -24,13 +28,18 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.LightGray import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.navigation.NavHostController import co.touchlab.kermit.Logger import com.multiplatform.webview.cookie.Cookie @@ -39,8 +48,10 @@ import com.multiplatform.webview.web.LoadingState import com.multiplatform.webview.web.WebView import com.multiplatform.webview.web.WebViewState import com.multiplatform.webview.web.rememberWebViewNavigator +import com.multiplatform.webview.web.rememberWebViewSnapshot import com.multiplatform.webview.web.rememberWebViewState import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch /** * Created By Kevin Zou On 2023/9/8 @@ -65,6 +76,10 @@ internal fun BasicWebViewSample(navHostController: NavHostController? = null) { onDispose { } } val navigator = rememberWebViewNavigator() + val webViewSnapshot = rememberWebViewSnapshot() + val coroutineScope = rememberCoroutineScope() + var webViewSnapshotResult: ImageBitmap? by remember { mutableStateOf(null) } + var textFieldValue by remember(state.lastLoadedUrl) { mutableStateOf(state.lastLoadedUrl) } @@ -86,6 +101,22 @@ internal fun BasicWebViewSample(navHostController: NavHostController? = null) { ) } }, + actions = { + TextButton(onClick = { + coroutineScope.launch { + webViewSnapshotResult = webViewSnapshot.takeSnapshot( + Rect( + top = 50f, + left = 50f, + right = 1000f, + bottom = 2000f + ) + ) + } + }) { + Text("Snapshot", color = Color.White) + } + } ) Row { @@ -96,9 +127,9 @@ internal fun BasicWebViewSample(navHostController: NavHostController? = null) { contentDescription = "Error", colorFilter = ColorFilter.tint(Color.Red), modifier = - Modifier - .align(Alignment.CenterEnd) - .padding(8.dp), + Modifier + .align(Alignment.CenterEnd) + .padding(8.dp), ) } @@ -128,14 +159,45 @@ internal fun BasicWebViewSample(navHostController: NavHostController? = null) { modifier = Modifier.fillMaxWidth(), ) } - - WebView( - state = state, - modifier = + Box { + WebView( + state = state, + modifier = Modifier .fillMaxSize(), - navigator = navigator, - ) + navigator = navigator, + webViewSnapshot = webViewSnapshot + ) + + // When WebView's Bitmap image is captured, show snapshot in dialog + webViewSnapshotResult?.let { bitmap -> + Dialog(onDismissRequest = { }) { + Box { + Column( + modifier = Modifier + .background(LightGray) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Preview of WebView snapshot") + Spacer(Modifier.size(16.dp)) + Image( + bitmap = bitmap, + contentDescription = "Preview of WebViewSnapshot" + ) + Spacer(Modifier.size(4.dp)) + } + + Button( + onClick = { webViewSnapshotResult = null }, + modifier = Modifier.align(Alignment.BottomCenter) + ) { + Text("Close Snapshot Preview") + } + } + } + } + } } } } diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/HtmlWebViewSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/HtmlWebViewSample.kt index 37f629de..bb74fd98 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/HtmlWebViewSample.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/HtmlWebViewSample.kt @@ -1,9 +1,14 @@ package com.kevinnzou.sample +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.Button import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -16,10 +21,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color.Companion.LightGray +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.navigation.NavHostController import co.touchlab.kermit.Logger import com.kevinnzou.sample.eventbus.FlowEventBus @@ -32,8 +42,10 @@ import com.multiplatform.webview.util.KLogSeverity import com.multiplatform.webview.web.WebView import com.multiplatform.webview.web.WebViewState import com.multiplatform.webview.web.rememberWebViewNavigator +import com.multiplatform.webview.web.rememberWebViewSnapshot import com.multiplatform.webview.web.rememberWebViewStateWithHTMLFile import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch /** * Created By Kevin Zou On 2023/9/8 @@ -55,6 +67,11 @@ internal fun BasicWebViewWithHTMLSample(navHostController: NavHostController? = val webViewNavigator = rememberWebViewNavigator() val jsBridge = rememberWebViewJsBridge(webViewNavigator) var jsRes by mutableStateOf("Evaluate JavaScript") + val webViewSnapshot = rememberWebViewSnapshot() + + val coroutineScope = rememberCoroutineScope() + var webViewSnapshotResult: ImageBitmap? by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { initWebView(webViewState) initJsBridge(jsBridge) @@ -82,11 +99,16 @@ internal fun BasicWebViewWithHTMLSample(navHostController: NavHostController? = captureBackPresses = false, navigator = webViewNavigator, webViewJsBridge = jsBridge, + webViewSnapshot = webViewSnapshot ) - Button( - onClick = { - webViewNavigator.evaluateJavaScript( - """ + Column( + Modifier.align(Alignment.BottomCenter).padding(bottom = 30.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = { + webViewNavigator.evaluateJavaScript( + """ document.getElementById("subtitle").innerText = "Hello from KMM!"; window.kmpJsBridge.callNative("Greet",JSON.stringify({message: "Hello"}), function (data) { @@ -96,13 +118,50 @@ internal fun BasicWebViewWithHTMLSample(navHostController: NavHostController? = ); callJS(); """.trimIndent(), + ) { + jsRes = it + } + }, + modifier = Modifier, + ) { + Text(jsRes) + } + + Spacer(Modifier.height(12.dp)) + + Button( + onClick = { + coroutineScope.launch { + webViewSnapshotResult = webViewSnapshot.takeSnapshot(null) + } + }, + modifier = Modifier, + ) { + Text("Take Snapshot") + } + } + + // When WebView's Bitmap image is captured, show snapshot in dialog + webViewSnapshotResult?.let { bitmap -> + Dialog(onDismissRequest = { }) { + Column( + modifier = Modifier + .background(LightGray) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - jsRes = it + Text("Preview of WebView snapshot") + Spacer(Modifier.size(16.dp)) + Image( + bitmap = bitmap, + contentDescription = "Preview of WebViewSnapshot" + ) + Spacer(Modifier.size(4.dp)) + Button(onClick = { webViewSnapshotResult = null }) { + Text("Close Snapshot Preview") + } } - }, - modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 50.dp), - ) { - Text(jsRes) + } } } } diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/util/WebViewSnapshotUtil.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/util/WebViewSnapshotUtil.kt new file mode 100644 index 00000000..4c93b764 --- /dev/null +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/util/WebViewSnapshotUtil.kt @@ -0,0 +1,70 @@ +package com.multiplatform.webview.util + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Matrix +import android.webkit.WebView +import androidx.compose.ui.geometry.Rect + +internal fun WebView.takeSnapshot(rect: Rect?): Bitmap? { + // Create a Rect object representing the area to capture. + // If the provided rect is null, default to the full dimensions of the WebView. + val snapshotRect = Rect( + left = rect?.left ?: 0f, + top = rect?.top ?: 0f, + right = rect?.right ?: width.toFloat(), + bottom = rect?.bottom ?: height.toFloat() + ) + + // Calculate the scale factor and dimensions + val (scaledWidth, scaledHeight, scaleFactor) = WebViewSnapshotUtil.calculateScaleFactorAndDimensions( + snapshotRect.width.toInt(), + snapshotRect.height.toInt() + ) + + // Create a bitmap with the target dimensions + val bm = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888) + + // Create a canvas to draw the WebView content + val scaledCanvas = Canvas(bm) + + // Apply the scaling transformation to the canvas and adjust for scroll position + val matrix = Matrix().apply { + setScale(scaleFactor, scaleFactor) + postTranslate( + -snapshotRect.left * scaleFactor - scrollX * scaleFactor, + -snapshotRect.top * scaleFactor - scrollY * scaleFactor + ) + } + scaledCanvas.setMatrix(matrix) + + // Draw the WebView onto the scaled canvas + draw(scaledCanvas) + return bm +} + +object WebViewSnapshotUtil { + + fun calculateScaleFactorAndDimensions( + width: Int, + height: Int + ): Triple { + val targetWidth = minOf(MAX_SNAPSHOT_WIDTH, width) + val targetHeight = minOf(MAX_SNAPSHOT_HEIGHT, height) + + // Determine the aspect ratio preserving scale factor + val scaleFactor = minOf( + targetWidth.toFloat() / width, + targetHeight.toFloat() / height + ) + + // Calculate the scaled dimensions + val scaledWidth = (width * scaleFactor).toInt() + val scaledHeight = (height * scaleFactor).toInt() + + return Triple(scaledWidth, scaledHeight, scaleFactor) + } + + private const val MAX_SNAPSHOT_WIDTH = 2500 + private const val MAX_SNAPSHOT_HEIGHT = 2500 +} \ No newline at end of file diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt index f2d0f240..10eaeb95 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt @@ -62,6 +62,7 @@ fun AccompanistWebView( captureBackPresses: Boolean = true, navigator: WebViewNavigator = rememberWebViewNavigator(), webViewJsBridge: WebViewJsBridge? = null, + webViewSnapshot: WebViewSnapshot? = null, onCreated: (WebView) -> Unit = {}, onDispose: (WebView) -> Unit = {}, client: AccompanistWebViewClient = remember { AccompanistWebViewClient() }, @@ -98,6 +99,7 @@ fun AccompanistWebView( captureBackPresses, navigator, webViewJsBridge, + webViewSnapshot, onCreated, onDispose, client, @@ -140,6 +142,7 @@ fun AccompanistWebView( captureBackPresses: Boolean = true, navigator: WebViewNavigator = rememberWebViewNavigator(), webViewJsBridge: WebViewJsBridge? = null, + webViewSnapshot: WebViewSnapshot? = null, onCreated: (WebView) -> Unit = {}, onDispose: (WebView) -> Unit = {}, client: AccompanistWebViewClient = remember { AccompanistWebViewClient() }, @@ -230,6 +233,7 @@ fun AccompanistWebView( val androidWebView = AndroidWebView(it, scope, webViewJsBridge) state.webView = androidWebView webViewJsBridge?.webView = androidWebView + webViewSnapshot?.webView = androidWebView } }, modifier = modifier, diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt index f95e5d13..463c612f 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt @@ -1,10 +1,22 @@ package com.multiplatform.webview.web +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Paint +import android.util.Log +import android.view.View.MeasureSpec import android.webkit.JavascriptInterface import android.webkit.WebView +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.view.drawToBitmap import com.multiplatform.webview.jsbridge.JsMessage import com.multiplatform.webview.jsbridge.WebViewJsBridge import com.multiplatform.webview.util.KLogger +import com.multiplatform.webview.util.WebViewSnapshotUtil +import com.multiplatform.webview.util.takeSnapshot import kotlinx.coroutines.CoroutineScope import kotlinx.serialization.json.Json @@ -139,4 +151,15 @@ class AndroidWebView( null } } + + override fun takeSnapshot(rect: Rect?, completionHandler: (ImageBitmap?) -> Unit) { + webView.post { + try { + val bm = webView.takeSnapshot(rect) + completionHandler.invoke(bm?.asImageBitmap()) + } catch (e: Exception) { + completionHandler.invoke(null) + } + } + } } diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt index f20b4c68..9bf9dcf3 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt @@ -15,6 +15,7 @@ actual fun ActualWebView( captureBackPresses: Boolean, navigator: WebViewNavigator, webViewJsBridge: WebViewJsBridge?, + webViewSnapshot: WebViewSnapshot?, onCreated: (NativeWebView) -> Unit, onDispose: (NativeWebView) -> Unit, factory: (WebViewFactoryParam) -> NativeWebView, @@ -25,6 +26,7 @@ actual fun ActualWebView( captureBackPresses, navigator, webViewJsBridge, + webViewSnapshot, onCreated = onCreated, onDispose = onDispose, factory = { factory(WebViewFactoryParam(it)) }, diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt index 840e8ce9..42ce53ff 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt @@ -1,5 +1,7 @@ package com.multiplatform.webview.web +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.ImageBitmap import com.multiplatform.webview.jsbridge.WebViewJsBridge import com.multiplatform.webview.util.KLogger import compose_webview_multiplatform.webview.generated.resources.Res @@ -223,4 +225,9 @@ interface IWebView { * Get the scroll offset of the WebView. */ fun scrollOffset(): Pair + + /** + * Takes a snapshot of the WebView + */ + fun takeSnapshot(rect: Rect?, completionHandler: (ImageBitmap?) -> Unit) } diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt index aa7cfade..77f1580e 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt @@ -37,6 +37,7 @@ fun WebView( captureBackPresses: Boolean = true, navigator: WebViewNavigator = rememberWebViewNavigator(), webViewJsBridge: WebViewJsBridge? = null, + webViewSnapshot: WebViewSnapshot? = null, onCreated: () -> Unit = {}, onDispose: () -> Unit = {}, ) { @@ -46,6 +47,7 @@ fun WebView( captureBackPresses = captureBackPresses, navigator = navigator, webViewJsBridge = webViewJsBridge, + webViewSnapshot = webViewSnapshot, onCreated = { _ -> onCreated() }, onDispose = { _ -> onDispose() }, ) @@ -72,6 +74,7 @@ fun WebView( captureBackPresses: Boolean = true, navigator: WebViewNavigator = rememberWebViewNavigator(), webViewJsBridge: WebViewJsBridge? = null, + webViewSnapshot: WebViewSnapshot? = null, onCreated: (NativeWebView) -> Unit = {}, onDispose: (NativeWebView) -> Unit = {}, factory: ((WebViewFactoryParam) -> NativeWebView)? = null, @@ -152,6 +155,7 @@ fun WebView( captureBackPresses = captureBackPresses, navigator = navigator, webViewJsBridge = webViewJsBridge, + webViewSnapshot = webViewSnapshot, onCreated = onCreated, onDispose = onDispose, factory = factory ?: ::defaultWebViewFactory, @@ -195,6 +199,7 @@ expect fun ActualWebView( captureBackPresses: Boolean = true, navigator: WebViewNavigator = rememberWebViewNavigator(), webViewJsBridge: WebViewJsBridge? = null, + webViewSnapshot: WebViewSnapshot? = null, onCreated: (NativeWebView) -> Unit = {}, onDispose: (NativeWebView) -> Unit = {}, factory: (WebViewFactoryParam) -> NativeWebView = ::defaultWebViewFactory, diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewSnapshot.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewSnapshot.kt new file mode 100644 index 00000000..6c35e798 --- /dev/null +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewSnapshot.kt @@ -0,0 +1,48 @@ +package com.multiplatform.webview.web + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.ImageBitmap +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +/** + * Created By Murod 2024/07/20 + */ + +/** + * Allows capturing a snapshot of the webview. + * + * @see [rememberWebViewSnapshot] + */ +class WebViewSnapshot { + internal var webView: IWebView? = null + + /** + * Captures a snapshot of the `WebView` within the specified rectangle. + * + * @param rect An optional `Rect` specifying the area to capture. If `null`, captures the entire `WebView`. + * @return An `ImageBitmap` containing the snapshot, or `null` if the capture fails. + */ + suspend fun takeSnapshot(rect: Rect? = null): ImageBitmap? { + return suspendCancellableCoroutine { + webView?.takeSnapshot( + rect = rect, + completionHandler = { img -> + it.resume(img) + } + ) + } + } +} + +/** + * Creates and remembers a [WebViewSnapshot] + * + * @return An instance of `WebViewSnapshot`. + */ +@Composable +fun rememberWebViewSnapshot(): WebViewSnapshot { + return remember { WebViewSnapshot() } +} \ No newline at end of file diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt index dfc982f4..6ce8cc47 100644 --- a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt +++ b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt @@ -22,6 +22,7 @@ actual fun ActualWebView( captureBackPresses: Boolean, navigator: WebViewNavigator, webViewJsBridge: WebViewJsBridge?, + webViewSnapshot: WebViewSnapshot?, onCreated: (NativeWebView) -> Unit, onDispose: (NativeWebView) -> Unit, factory: (WebViewFactoryParam) -> NativeWebView, @@ -44,12 +45,13 @@ actual class WebViewFactoryParam( val fileContent: String, ) { inline val webSettings get() = state.webSettings - inline val rendering: CefRendering get() = - if (webSettings.desktopWebSettings.offScreenRendering) { - CefRendering.OFFSCREEN - } else { - CefRendering.DEFAULT - } + inline val rendering: CefRendering + get() = + if (webSettings.desktopWebSettings.offScreenRendering) { + CefRendering.OFFSCREEN + } else { + CefRendering.DEFAULT + } inline val transparent: Boolean get() = webSettings.desktopWebSettings.transparent val requestContext: CefRequestContext get() = createModifiedRequestContext(webSettings) } @@ -64,6 +66,7 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView = param.transparent, param.requestContext, ) + is WebContent.Data -> param.client.createBrowserWithHtml( content.data, @@ -71,6 +74,7 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView = param.rendering, param.transparent, ) + is WebContent.File -> param.client.createBrowserWithHtml( param.fileContent, @@ -78,6 +82,7 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView = param.rendering, param.transparent, ) + else -> param.client.createBrowser( KCEFBrowser.BLANK_URI, diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/util/ImageBitmap.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/util/ImageBitmap.kt new file mode 100644 index 00000000..6d022b9d --- /dev/null +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/util/ImageBitmap.kt @@ -0,0 +1,26 @@ +package com.multiplatform.webview.util + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import org.jetbrains.skia.Image +import platform.UIKit.UIImage +import platform.UIKit.UIImagePNGRepresentation +import platform.posix.memcpy + +/** + * Converts a `UIImage` to an `ImageBitmap` for use in Compose. + */ +@OptIn(ExperimentalForeignApi::class) +fun UIImage.toImageBitmap(): ImageBitmap? { + val pngRepresentation = UIImagePNGRepresentation(this) + ?: return null // Return null if PNG representation is not available + val byteArray = ByteArray(pngRepresentation.length.toInt()).apply { + usePinned { + memcpy(it.addressOf(0), pngRepresentation.bytes, pngRepresentation.length) + } + } + return Image.makeFromEncoded(byteArray).toComposeImageBitmap() +} \ No newline at end of file diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/util/Rect.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/util/Rect.kt new file mode 100644 index 00000000..ab7f7260 --- /dev/null +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/util/Rect.kt @@ -0,0 +1,41 @@ +package com.multiplatform.webview.util + +import androidx.compose.ui.geometry.Rect +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ExperimentalForeignApi +import platform.CoreGraphics.CGFloat +import platform.CoreGraphics.CGRect +import platform.CoreGraphics.CGRectMake +import platform.UIKit.UIScreen + +/** + * Created By Murod 2024/07/20 + */ + +/** + * Converts a Kotlin `Rect` to a native `CGRect` for iOS. + * + * This function transforms a `Rect` (using pixels) into a `CGRect` (using points) for use in iOS + * development. It adjusts the rectangle’s coordinates and dimensions accordingly. + * + * @return A `CValue` representing the rectangle in iOS points. + */ +@OptIn(ExperimentalForeignApi::class) +fun Rect.toCGRect(): CValue { + return CGRectMake( + x = left.toPints(),// X coordinate in points + y = top.toPints(), // Y coordinate in points + width = width.toPints(), // Width in points + height = height.toPints() // Height in points + ) +} + +/** + * On iOS, UI elements are measured in points, which are resolution-independent units. + * The actual number of pixels per point depends on the device's screen scale factor. + * + * Function to convert pixels to points + */ +fun Float.toPints(): CGFloat { + return this / UIScreen.mainScreen.scale +} \ No newline at end of file diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt index c2be7477..59269156 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt @@ -1,15 +1,27 @@ package com.multiplatform.webview.web +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toSkiaRect import com.multiplatform.webview.jsbridge.WKJsMessageHandler import com.multiplatform.webview.jsbridge.WebViewJsBridge import com.multiplatform.webview.util.KLogger import com.multiplatform.webview.util.getPlatformVersionDouble +import com.multiplatform.webview.util.toCGRect +import com.multiplatform.webview.util.toImageBitmap import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.CValue import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.allocArrayOf import kotlinx.cinterop.memScoped +import kotlinx.cinterop.objcPtr +import kotlinx.cinterop.readValue import kotlinx.cinterop.useContents import kotlinx.coroutines.CoroutineScope +import platform.CoreGraphics.CGFloat +import platform.CoreGraphics.CGRect +import platform.CoreGraphics.CGRectGetHeight +import platform.CoreGraphics.CGRectMake import platform.Foundation.HTTPBody import platform.Foundation.HTTPMethod import platform.Foundation.NSBundle @@ -18,6 +30,8 @@ import platform.Foundation.NSMutableURLRequest import platform.Foundation.NSURL import platform.Foundation.create import platform.Foundation.setValue +import platform.UIKit.UIScreen +import platform.WebKit.WKSnapshotConfiguration import platform.WebKit.WKWebView import platform.darwin.NSObject import platform.darwin.NSObjectMeta @@ -181,6 +195,26 @@ class IOSWebView( } } + @OptIn(ExperimentalForeignApi::class) + override fun takeSnapshot(rect: Rect?, completionHandler: (ImageBitmap?) -> Unit) { + val snapshotConfig = WKSnapshotConfiguration() + rect?.let { + snapshotConfig.rect = it.toCGRect() + } + webView.takeSnapshotWithConfiguration( + snapshotConfiguration = snapshotConfig, + completionHandler = { image, error -> + if (error != null) { + KLogger.e("takeSnapshot error: ${error}") + completionHandler.invoke(null) + } else { + KLogger.e("takeSnapshot success") + completionHandler.invoke(image?.toImageBitmap()) + } + } + ) + } + private class BundleMarker : NSObject() { companion object : NSObjectMeta() } diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt index f47ceb65..5c0ab961 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/WebView.ios.kt @@ -25,6 +25,7 @@ actual fun ActualWebView( captureBackPresses: Boolean, navigator: WebViewNavigator, webViewJsBridge: WebViewJsBridge?, + webViewSnapshot: WebViewSnapshot?, onCreated: (NativeWebView) -> Unit, onDispose: (NativeWebView) -> Unit, factory: (WebViewFactoryParam) -> NativeWebView, @@ -35,6 +36,7 @@ actual fun ActualWebView( captureBackPresses = captureBackPresses, navigator = navigator, webViewJsBridge = webViewJsBridge, + webViewSnapshot = webViewSnapshot, onCreated = onCreated, onDispose = onDispose, factory = factory, @@ -46,7 +48,8 @@ actual data class WebViewFactoryParam(val config: WKWebViewConfiguration) /** Default WebView factory for iOS. */ @OptIn(ExperimentalForeignApi::class) -actual fun defaultWebViewFactory(param: WebViewFactoryParam) = WKWebView(frame = CGRectZero.readValue(), configuration = param.config) +actual fun defaultWebViewFactory(param: WebViewFactoryParam) = + WKWebView(frame = CGRectZero.readValue(), configuration = param.config) /** * iOS WebView implementation. @@ -59,6 +62,7 @@ fun IOSWebView( captureBackPresses: Boolean, navigator: WebViewNavigator, webViewJsBridge: WebViewJsBridge?, + webViewSnapshot: WebViewSnapshot?, onCreated: (NativeWebView) -> Unit, onDispose: (NativeWebView) -> Unit, factory: (WebViewFactoryParam) -> NativeWebView, @@ -109,9 +113,9 @@ fun IOSWebView( (it.iOSWebSettings.backgroundColor ?: it.backgroundColor).toUIColor() val scrollViewColor = ( - it.iOSWebSettings.underPageBackgroundColor - ?: it.backgroundColor - ).toUIColor() + it.iOSWebSettings.underPageBackgroundColor + ?: it.backgroundColor + ).toUIColor() setOpaque(it.iOSWebSettings.opaque) if (!it.iOSWebSettings.opaque) { setBackgroundColor(backgroundColor) @@ -131,6 +135,7 @@ fun IOSWebView( val iosWebView = IOSWebView(it, scope, webViewJsBridge) state.webView = iosWebView webViewJsBridge?.webView = iosWebView + webViewSnapshot?.webView = iosWebView } }, modifier = modifier,