@@ -2,8 +2,14 @@ package com.kdroid.composetray.utils
22
33import androidx.compose.runtime.Composable
44import androidx.compose.ui.ImageComposeScene
5+ import androidx.compose.ui.use
56import kotlinx.coroutines.Dispatchers
6- import org.jetbrains.skia.*
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
713import java.io.File
814import java.util.zip.CRC32
915
@@ -12,156 +18,35 @@ import java.util.zip.CRC32
1218 */
1319object ComposableIconUtils {
1420
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-
8621 /* *
8722 * Renders a Composable to a PNG file and returns the path to the file.
8823 *
8924 * @param iconRenderProperties Properties for rendering the icon
9025 * @param content The Composable content to render
9126 * @return Path to the generated PNG file
92- * @throws Exception if rendering fails after all fallback attempts
27+ * @throws Exception if rendering fails completely
9328 */
9429 fun renderComposableToPngFile (
9530 iconRenderProperties : IconRenderProperties ,
9631 content : @Composable () -> Unit
9732 ): String {
9833 val tempFile = createTempFile(suffix = " .png" )
99- val pngData = renderComposableToPngBytesWithFallback (iconRenderProperties, content)
34+ val pngData = renderComposableToPngBytes (iconRenderProperties, content)
10035 tempFile.writeBytes(pngData)
10136 return tempFile.absolutePath
10237 }
10338
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-
15639 /* *
15740 * Renders a Composable to a PNG image and returns the result as a byte array.
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.
15844 *
15945 * @param iconRenderProperties Properties for rendering the icon
16046 * @param content The Composable content to render
16147 * @return A byte array containing the rendered PNG image data.
16248 * @throws Exception if rendering fails
16349 */
164- @Throws(Exception ::class )
16550 fun renderComposableToPngBytes (
16651 iconRenderProperties : IconRenderProperties ,
16752 content : @Composable () -> Unit
@@ -172,18 +57,32 @@ object ComposableIconUtils {
17257 var scaledImage: Image ? = null
17358
17459 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()
183- }
60+ // Try to create and render the scene
61+ try {
62+ scene = ImageComposeScene (
63+ width = iconRenderProperties.sceneWidth,
64+ height = iconRenderProperties.sceneHeight,
65+ density = iconRenderProperties.sceneDensity,
66+ coroutineContext = Dispatchers .Unconfined
67+ ) {
68+ content()
69+ }
70+
71+ renderedIcon = scene.render()
72+ } catch (e: Exception ) {
73+ // Log the error but don't modify any system properties
74+ val errorMessage = e.message ? : " Unknown error"
75+ errorln { " [ComposableIconUtils] Failed to render scene: $errorMessage " }
76+
77+ // Check if it's a DirectX error on Windows
78+ if (errorMessage.contains(" DirectX12" , ignoreCase = true ) ||
79+ errorMessage.contains(" Failed to choose DirectX12 adapter" , ignoreCase = true )) {
80+ errorln { " [ComposableIconUtils] DirectX12 not available on this system. Scene rendering failed." }
81+ }
18482
185- // Render the scene - this may also trigger rendering API errors
186- renderedIcon = scene.render()
83+ // Re-throw the exception - let the caller handle it
84+ throw e
85+ }
18786
18887 val iconData = if (iconRenderProperties.requiresScaling) {
18988 scaledBitmap = Bitmap ().apply {
@@ -205,9 +104,6 @@ object ComposableIconUtils {
205104 }
206105
207106 return iconData.bytes
208- } catch (e: Exception ) {
209- // Re-throw to be handled by the fallback wrapper
210- throw e
211107 } finally {
212108 // Ensure proper cleanup
213109 try {
@@ -227,7 +123,7 @@ object ComposableIconUtils {
227123 * @param iconRenderProperties Properties for rendering the icon
228124 * @param content The Composable content to render
229125 * @return Path to the generated ICO file
230- * @throws Exception if rendering fails after all fallback attempts
126+ * @throws Exception if rendering fails
231127 */
232128 fun renderComposableToIcoFile (
233129 iconRenderProperties : IconRenderProperties ,
@@ -247,14 +143,14 @@ object ComposableIconUtils {
247143 * @param iconRenderProperties Properties for rendering the icon
248144 * @param content The Composable content to render
249145 * @return Byte array containing the ICO data
250- * @throws Exception if rendering fails after all fallback attempts
146+ * @throws Exception if rendering fails
251147 */
252148 fun renderComposableToIcoBytes (
253149 iconRenderProperties : IconRenderProperties ,
254150 content : @Composable () -> Unit
255151 ): ByteArray {
256- // First render to PNG format (with fallback support )
257- val pngBytes = renderComposableToPngBytesWithFallback (iconRenderProperties, content)
152+ // First render to PNG format (which is supported )
153+ val pngBytes = renderComposableToPngBytes (iconRenderProperties, content)
258154
259155 // Create a simple ICO format wrapper around the PNG data
260156 // ICO header (6 bytes) + ICO directory entry (16 bytes) + PNG data
@@ -300,20 +196,6 @@ object ComposableIconUtils {
300196 return icoData
301197 }
302198
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-
317199 /* *
318200 * Creates a temporary file that will be deleted when the JVM exits.
319201 */
@@ -337,8 +219,8 @@ object ComposableIconUtils {
337219 content : @Composable () -> Unit
338220 ): Long {
339221 return try {
340- // Render the composable to PNG bytes (with fallback support)
341- val pngBytes = renderComposableToPngBytesWithFallback (iconRenderProperties, content)
222+ // Render the composable to PNG bytes
223+ val pngBytes = renderComposableToPngBytes (iconRenderProperties, content)
342224
343225 // Calculate CRC32 hash of the PNG bytes
344226 val crc = CRC32 ()
0 commit comments