@@ -2,14 +2,8 @@ package com.kdroid.composetray.utils
22
33import androidx.compose.runtime.Composable
44import androidx.compose.ui.ImageComposeScene
5- import androidx.compose.ui.use
65import kotlinx.coroutines.Dispatchers
7- import org.jetbrains.skia.Bitmap
8- import org.jetbrains.skia.EncodedImageFormat
9- import org.jetbrains.skia.FilterMipmap
10- import org.jetbrains.skia.FilterMode
11- import org.jetbrains.skia.Image
12- import org.jetbrains.skia.MipmapMode
6+ import org.jetbrains.skia.*
137import java.io.File
148import java.util.zip.CRC32
159
@@ -18,76 +12,222 @@ import java.util.zip.CRC32
1812 */
1913object ComposableIconUtils {
2014
15+ private var currentRenderApi: RenderApi = RenderApi .AUTO
16+
17+ enum class RenderApi {
18+ AUTO ,
19+ DIRECTX12 ,
20+ OPENGL ,
21+ SOFTWARE
22+ }
23+
24+ init {
25+ // Start with automatic detection, will fallback if needed
26+ configureRenderingApi(RenderApi .AUTO )
27+ }
28+
29+ /* *
30+ * Configure Skiko rendering API with fallback mechanism
31+ */
32+ private fun configureRenderingApi (api : RenderApi ) {
33+ try {
34+ when (api) {
35+ RenderApi .AUTO -> {
36+ // Let Skiko choose automatically (usually tries DirectX12 first on Windows)
37+ System .clearProperty(" skiko.renderApi" )
38+ debugln { " [ComposableIconUtils] Using automatic rendering API selection" }
39+ }
40+ RenderApi .DIRECTX12 -> {
41+ System .setProperty(" skiko.renderApi" , " DIRECT3D" )
42+ debugln { " [ComposableIconUtils] Configured DirectX12 rendering" }
43+ }
44+ RenderApi .OPENGL -> {
45+ System .setProperty(" skiko.renderApi" , " OPENGL" )
46+ System .setProperty(" skiko.rendering.api" , " OPENGL" )
47+ debugln { " [ComposableIconUtils] Configured OpenGL rendering" }
48+ }
49+ RenderApi .SOFTWARE -> {
50+ System .setProperty(" skiko.renderApi" , " SOFTWARE" )
51+ System .setProperty(" skiko.softwareRendering" , " true" )
52+ System .setProperty(" skiko.rendering.api" , " SOFTWARE" )
53+ System .setProperty(" skiko.rendering.useGpu" , " false" )
54+ System .setProperty(" skiko.directx.disabled" , " true" )
55+ debugln { " [ComposableIconUtils] Configured software rendering" }
56+ }
57+ }
58+ currentRenderApi = api
59+ } catch (e: Exception ) {
60+ errorln { " [ComposableIconUtils] Failed to configure rendering API: ${e.message} " }
61+ }
62+ }
63+
64+ /* *
65+ * Try the next rendering API in the fallback chain
66+ */
67+ private fun tryNextRenderApi (): Boolean {
68+ return when (currentRenderApi) {
69+ RenderApi .AUTO , RenderApi .DIRECTX12 -> {
70+ debugln { " [ComposableIconUtils] Falling back from $currentRenderApi to OpenGL" }
71+ configureRenderingApi(RenderApi .OPENGL )
72+ true
73+ }
74+ RenderApi .OPENGL -> {
75+ debugln { " [ComposableIconUtils] Falling back from OpenGL to Software rendering" }
76+ configureRenderingApi(RenderApi .SOFTWARE )
77+ true
78+ }
79+ RenderApi .SOFTWARE -> {
80+ errorln { " [ComposableIconUtils] Already using software rendering, no more fallbacks available" }
81+ false
82+ }
83+ }
84+ }
85+
2186 /* *
2287 * Renders a Composable to a PNG file and returns the path to the file.
2388 *
2489 * @param iconRenderProperties Properties for rendering the icon
2590 * @param content The Composable content to render
2691 * @return Path to the generated PNG file
92+ * @throws Exception if rendering fails after all fallback attempts
2793 */
2894 fun renderComposableToPngFile (
2995 iconRenderProperties : IconRenderProperties ,
3096 content : @Composable () -> Unit
3197 ): String {
3298 val tempFile = createTempFile(suffix = " .png" )
33- val pngData = renderComposableToPngBytes (iconRenderProperties, content)
99+ val pngData = renderComposableToPngBytesWithFallback (iconRenderProperties, content)
34100 tempFile.writeBytes(pngData)
35101 return tempFile.absolutePath
36102 }
37103
104+ /* *
105+ * Renders a Composable to PNG bytes with automatic fallback between rendering APIs
106+ */
107+ private fun renderComposableToPngBytesWithFallback (
108+ iconRenderProperties : IconRenderProperties ,
109+ content : @Composable () -> Unit
110+ ): ByteArray {
111+ var attempts = 0
112+ val maxAttempts = 3 // Try DirectX/Auto, then OpenGL, then Software
113+ var lastException: Exception ? = null
114+
115+ while (attempts < maxAttempts) {
116+ try {
117+ debugln { " [ComposableIconUtils] Render attempt ${attempts + 1 } with $currentRenderApi " }
118+ return renderComposableToPngBytes(iconRenderProperties, content)
119+ } catch (e: Exception ) {
120+ lastException = e
121+ val errorMessage = e.message ? : " "
122+ val errorClassName = e.javaClass.simpleName
123+
124+ errorln { " [ComposableIconUtils] $errorClassName with ${currentRenderApi} : $errorMessage " }
125+
126+ // Check if the error is related to DirectX, OpenGL, or rendering in general
127+ val isDirectXError = errorMessage.contains(" DirectX12" , ignoreCase = true ) ||
128+ errorMessage.contains(" D3D12" , ignoreCase = true ) ||
129+ errorMessage.contains(" Direct3D" , ignoreCase = true ) ||
130+ errorMessage.contains(" choose DirectX12 adapter" , ignoreCase = true ) ||
131+ errorClassName.contains(" RenderException" , ignoreCase = true )
132+
133+ val isOpenGLError = errorMessage.contains(" OpenGL" , ignoreCase = true ) ||
134+ errorMessage.contains(" GL" , ignoreCase = true )
135+
136+ val isRenderingError = errorClassName.contains(" Render" , ignoreCase = true ) ||
137+ errorMessage.contains(" render" , ignoreCase = true )
138+
139+ // If it's a rendering-related error, try the next API
140+ if (isDirectXError || isOpenGLError || isRenderingError) {
141+ if (tryNextRenderApi()) {
142+ attempts++
143+ continue // Try again with the next API
144+ }
145+ }
146+
147+ // If we can't identify the error or no more fallbacks, throw the exception
148+ break
149+ }
150+ }
151+
152+ // If all attempts failed, throw the last exception
153+ throw lastException ? : Exception (" Failed to render composable after $attempts attempts" )
154+ }
155+
38156 /* *
39157 * Renders a Composable to a PNG image and returns the result as a byte array.
40158 *
41- * This function creates an [ImageComposeScene] based on the provided [IconRenderProperties],
42- * renders the Composable content, and encodes the output into PNG format.
43- * If scaling is required based on the [IconRenderProperties], the rendered content is scaled before encoding.
44- *
45159 * @param iconRenderProperties Properties for rendering the icon
46160 * @param content The Composable content to render
47161 * @return A byte array containing the rendered PNG image data.
162+ * @throws Exception if rendering fails
48163 */
164+ @Throws(Exception ::class )
49165 fun renderComposableToPngBytes (
50166 iconRenderProperties : IconRenderProperties ,
51167 content : @Composable () -> Unit
52168 ): ByteArray {
53- val scene = ImageComposeScene (
54- width = iconRenderProperties.sceneWidth,
55- height = iconRenderProperties.sceneHeight,
56- density = iconRenderProperties.sceneDensity,
57- coroutineContext = Dispatchers .Unconfined
58- ) {
59- content()
60- }
61-
62- val renderedIcon = scene.use { it.render() }
169+ var scene: ImageComposeScene ? = null
170+ var renderedIcon: Image ? = null
171+ var scaledBitmap: Bitmap ? = null
172+ var scaledImage: Image ? = null
63173
64- val iconData = if (iconRenderProperties.requiresScaling) {
65- val scaledIcon = Bitmap ().apply {
66- allocN32Pixels(iconRenderProperties.targetWidth, iconRenderProperties.targetHeight)
67- }
68- renderedIcon.use {
69- it.scalePixels(scaledIcon.peekPixels()!! , FilterMipmap (FilterMode .LINEAR , MipmapMode .LINEAR ), true )
174+ try {
175+ // Create the scene - this is where DirectX/OpenGL errors typically occur
176+ scene = ImageComposeScene (
177+ width = iconRenderProperties.sceneWidth,
178+ height = iconRenderProperties.sceneHeight,
179+ density = iconRenderProperties.sceneDensity,
180+ coroutineContext = Dispatchers .Unconfined
181+ ) {
182+ content()
70183 }
71- scaledIcon.use { bitmap ->
72- Image .makeFromBitmap(bitmap).use { image ->
73- image.encodeToData(EncodedImageFormat .PNG )!!
184+
185+ // Render the scene - this may also trigger rendering API errors
186+ renderedIcon = scene.render()
187+
188+ val iconData = if (iconRenderProperties.requiresScaling) {
189+ scaledBitmap = Bitmap ().apply {
190+ allocN32Pixels(iconRenderProperties.targetWidth, iconRenderProperties.targetHeight)
74191 }
192+
193+ renderedIcon.scalePixels(
194+ scaledBitmap.peekPixels()!! ,
195+ FilterMipmap (FilterMode .LINEAR , MipmapMode .LINEAR ),
196+ true
197+ )
198+
199+ scaledImage = Image .makeFromBitmap(scaledBitmap)
200+ scaledImage.encodeToData(EncodedImageFormat .PNG )
201+ ? : throw Exception (" Failed to encode scaled image to PNG" )
202+ } else {
203+ renderedIcon.encodeToData(EncodedImageFormat .PNG )
204+ ? : throw Exception (" Failed to encode image to PNG" )
75205 }
76- } else {
77- renderedIcon.use { image ->
78- image.encodeToData(EncodedImageFormat .PNG )!!
206+
207+ return iconData.bytes
208+ } catch (e: Exception ) {
209+ // Re-throw to be handled by the fallback wrapper
210+ throw e
211+ } finally {
212+ // Ensure proper cleanup
213+ try {
214+ scaledImage?.close()
215+ scaledBitmap?.close()
216+ renderedIcon?.close()
217+ scene?.close()
218+ } catch (e: Exception ) {
219+ debugln { " [ComposableIconUtils] Error during cleanup: ${e.message} " }
79220 }
80221 }
81-
82- return iconData.bytes
83222 }
84223
85224 /* *
86225 * Renders a Composable to an ICO file and returns the path to the file.
87226 *
88227 * @param iconRenderProperties Properties for rendering the icon
89228 * @param content The Composable content to render
90- * @return Path to the generated PNG file
229+ * @return Path to the generated ICO file
230+ * @throws Exception if rendering fails after all fallback attempts
91231 */
92232 fun renderComposableToIcoFile (
93233 iconRenderProperties : IconRenderProperties ,
@@ -107,13 +247,14 @@ object ComposableIconUtils {
107247 * @param iconRenderProperties Properties for rendering the icon
108248 * @param content The Composable content to render
109249 * @return Byte array containing the ICO data
250+ * @throws Exception if rendering fails after all fallback attempts
110251 */
111252 fun renderComposableToIcoBytes (
112253 iconRenderProperties : IconRenderProperties ,
113254 content : @Composable () -> Unit
114255 ): ByteArray {
115- // First render to PNG format (which is supported )
116- val pngBytes = renderComposableToPngBytes (iconRenderProperties, content)
256+ // First render to PNG format (with fallback support )
257+ val pngBytes = renderComposableToPngBytesWithFallback (iconRenderProperties, content)
117258
118259 // Create a simple ICO format wrapper around the PNG data
119260 // ICO header (6 bytes) + ICO directory entry (16 bytes) + PNG data
@@ -159,6 +300,20 @@ object ComposableIconUtils {
159300 return icoData
160301 }
161302
303+ /* *
304+ * Resets the rendering API to try again from the beginning of the fallback chain.
305+ * Useful when creating new instances or after configuration changes.
306+ */
307+ fun resetRenderingApi () {
308+ debugln { " [ComposableIconUtils] Resetting rendering API to AUTO" }
309+ configureRenderingApi(RenderApi .AUTO )
310+ }
311+
312+ /* *
313+ * Gets the current rendering API being used
314+ */
315+ fun getCurrentRenderApi (): String = currentRenderApi.name
316+
162317 /* *
163318 * Creates a temporary file that will be deleted when the JVM exits.
164319 */
@@ -181,12 +336,18 @@ object ComposableIconUtils {
181336 iconRenderProperties : IconRenderProperties ,
182337 content : @Composable () -> Unit
183338 ): Long {
184- // Render the composable to PNG bytes
185- val pngBytes = renderComposableToPngBytes(iconRenderProperties, content)
339+ return try {
340+ // Render the composable to PNG bytes (with fallback support)
341+ val pngBytes = renderComposableToPngBytesWithFallback(iconRenderProperties, content)
186342
187- // Calculate CRC32 hash of the PNG bytes
188- val crc = CRC32 ()
189- crc.update(pngBytes)
190- return crc.value
343+ // Calculate CRC32 hash of the PNG bytes
344+ val crc = CRC32 ()
345+ crc.update(pngBytes)
346+ crc.value
347+ } catch (e: Exception ) {
348+ errorln { " [ComposableIconUtils] Failed to calculate content hash: ${e.message} " }
349+ // Return a time-based hash as fallback
350+ System .currentTimeMillis()
351+ }
191352 }
192- }
353+ }
0 commit comments