{"id":16320276,"url":"https://github.com/hellonico/origami-compose-article","last_synced_at":"2025-10-09T22:46:13.314Z","repository":{"id":66707092,"uuid":"371841660","full_name":"hellonico/origami-compose-article","owner":"hellonico","description":null,"archived":false,"fork":false,"pushed_at":"2024-06-18T05:20:36.000Z","size":17052,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-08T00:48:46.529Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hellonico.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-05-28T23:45:34.000Z","updated_at":"2024-06-18T05:20:39.000Z","dependencies_parsed_at":"2024-05-01T02:49:21.415Z","dependency_job_id":"08b26759-3032-44b9-a47b-9e0eb9037cfc","html_url":"https://github.com/hellonico/origami-compose-article","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hellonico/origami-compose-article","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hellonico%2Forigami-compose-article","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hellonico%2Forigami-compose-article/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hellonico%2Forigami-compose-article/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hellonico%2Forigami-compose-article/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hellonico","download_url":"https://codeload.github.com/hellonico/origami-compose-article/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hellonico%2Forigami-compose-article/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279002113,"owners_count":26083307,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-09T02:00:07.460Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-10T22:43:48.230Z","updated_at":"2025-10-09T22:46:13.291Z","avatar_url":"https://github.com/hellonico.png","language":"Kotlin","readme":"# Write your own imaging processing UI in 15 minutes, using Kotlin and Compose for the Desktop\n\nCompose used to be just a smooth way of writing Android Applications in Kotlin. Now JetBrains has ported Compose to the Desktop, and it's now easier than ever to protype UI in real-time. \n\nOf course, you can write a web app based UI, or a single page application, but sometimes, especially in the IoT world, and a small cluster of Raspberry Pis, the easiest way is to have actually a small Desktop UI.\n\nA few months back, for a fast paced PoC, I had to set up a lot of AWS EC2 instances, lambdas, docker instances and had to monitor them, I actually reverted back to write a quick Desktop UI in compose just for that. And. it. works. great.\n\nToday's article is about plugging in Compose with my de-facto Imaging/AI library, Origami, the only OpenCV wrapper for the JVM. The article will focus on the ease of use of Compose, and will leave apart any advanced thing that can be with Origami, so we will focus on writing a UI with a drag and drop area to accept an image, and when the image is shown, two sliders will accept a range of values for threadshold 1 and threadshold 2 of the opencv canny function.\n\nThe end result looks like this:\n\n![image-20210528171524826](typora-user-images/image-20210528171524826.png)\n\nAnd if you have IntelliJ installed, we are targeting to take you there in less than 15 minutes. \n\n## Project Setup\n\nLet's start by creating a new Compose/Desktop project in IntelliJ.\n\n![image-20210528172846669](typora-user-images/image-20210528172846669.png)\n\nAny recent JVM will do, but let's keep a stable JVM version 11.\n\nSettings for the New Project are straight forward, and we are keeping the proposed settings as is and clicking Finish.\n\n![image-20210528172503125](typora-user-images/image-20210528172503125.png)\n\nAfter you create the project, and open the main.kt file the setup should look like the screenshot below:\n\n![image-20210528172937690](typora-user-images/image-20210528172937690.png)\n\nIf you start the program by running the main function with:\n\n![image-20210528173007896](typora-user-images/image-20210528173007896.png)\n\n\n\nThe original program simply displays a button displaying \"Hello, World!\" that reacts on a onClick event.\n\n![image-20210528173058712](typora-user-images/image-20210528173058712.png)\n\n## Loading an image with Origami\n\nTo use Origami in your project, edit the build.gradle.ks file, and add the new repository\n\n![image-20210528173339197](typora-user-images/image-20210528173339197.png)\n\nAnd the dependencies:\n\n![image-20210528173611408](typora-user-images/image-20210528173611408.png)\n\nOrigami Core and the filters are separated, so we add those two to the project.\n\nYou'll be asked to reload the gradle project settings, and this can be done by clicking the icon below:\n\n![image-20210528173645407](typora-user-images/image-20210528173645407.png)\n\nYou can now import the Origami library\n\n![image-20210528173716311](typora-user-images/image-20210528173716311.png)\n\nAnd your main.kt file should now look like this\n\n![image-20210528173735587](typora-user-images/image-20210528173735587.png)\n\nAn image in Kotlin/Compose is quite easy to add with the Composable Image.\n\n![image-20210528174009229](typora-user-images/image-20210528174009229.png)\n\nImage expects a bitmap, and Origami just like OpenCV works with Mat. So we will write a small function to convert a Mat to the expected bitmap.\n\n```kotlin\nfun asImageAsset(image: Mat): ImageBitmap {\n    val bytes = MatOfByte()\n    Imgcodecs.imencode(\".jpg\", image, bytes)\n    val byteArray = ByteArray((image.total() * image.channels()).toInt())\n    bytes.get(0, 0, byteArray)\n    return org.jetbrains.skija.Image.makeFromEncoded(byteArray).asImageBitmap()\n}\n```\n\nWe encode the OpenCV mat object into bytes, representing the JPG version of the image, and then use that to load into an ImageBitmap using *makeFromEncoded*.\n\nThen, we can just read the image using the usual OpenCV imread and convert to bitmap.\n\n```kotlin\nasImageAsset(imread(name))\n```\n\nYour main.kt file should now look like this:\n\n![image-20210528174358532](typora-user-images/image-20210528174358532.png)\n\nAnd if you run the kotlin code:\n\n![image-20210528174330510](typora-user-images/image-20210528174330510.png)\n\n## Canny Effect\n\nBefore using the asImageAsset function, you're in the land of Origami, and so you can apply any filter you want. Replacing the bitmap parameter of the image with:\n\n```kotlin\nbitmap = asImageAsset(Canny().apply(Imgcodecs.imread(\"andy.jpg\"))),\n```\n\nWill nicely give you:\n\n![image-20210528174551926](typora-user-images/image-20210528174551926.png)\n\n\n\n## Drag and drop\n\nDrag and Drop is not natively supported yet by Kotlin / Compose *but* we can make it work with a bit of glue. Here we plug in into the underlying terrifying Java AWT Framework. Once the window receives a file, we change the value of the mutable *name* variable. \n\n```kotlin\n    val name = remember { mutableStateOf(\"\") }\n    val target = object : DropTarget() {\n        @Synchronized\n        override fun drop(evt: DropTargetDropEvent) {\n            evt.acceptDrop(DnDConstants.ACTION_REFERENCE)\n            val droppedFiles = evt.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List\u003c*\u003e\n            droppedFiles.first()?.let {\n                name.value = (it as File).absolutePath\n            }\n        }\n    }\n    AppManager.windows.first().window.contentPane.dropTarget = target\n```\n\nAfter that, our application will show a text field if no image has been dropped yet, and the image if it can. No error detection done here, so better be an image !\n\n```kotlin\nMaterialTheme {\n        if (name.value == \"\") {\n            Text(\"Drop a file . . .\")\n        } else {\n            Image(\n                bitmap = asImageAsset(Canny().apply(Imgcodecs.imread(name.value))),\n                contentDescription = \"Icon\",\n                modifier = Modifier.fillMaxSize()\n            )\n        }\n}\n```\n\nRunning the application will give:\n\n![image-20210528175450601](typora-user-images/image-20210528175450601.png)\n\nAnd once we have dropped the image file on the window, Andy and his banana are back.\n\n![image-20210528175532109](typora-user-images/image-20210528175532109.png)\n\n## Complete with sliders\n\nNow we wrap the rest of the code with the sliders, by creating our own CustomComponent, *MyCustomOrigamiComponent*. Here we are simply taking values from two sliders, and using the values as threshold1 and threshold2 for the Canny filter. \n\nThis component will use the MutableState value from the drag and drop settings.\n\n```kotlin\n\n@Composable\nfun MyCustomOrigamiComponent(name:MutableState\u003cString\u003e) {\n\n    if (name.value == \"\") {\n        Text(\"Drop a file . . .\")\n    } else {\n\n        val value = remember { mutableStateOf(10.0F) }\n        val value2 = remember { mutableStateOf(10.0F) }\n        val filter = Canny()\n        filter.threshold1 = value.value.toInt()\n        filter.threshold2 = value2.value.toInt()\n\n        Column {\n            Text(value.value.toString())\n            Slider(steps = 100, valueRange = 1f..250f, value = value.value, onValueChange = {\n                value.value = it\n            })\n            Text(value2.value.toString())\n            Slider(steps = 100, valueRange = 1f..250f, value = value2.value, onValueChange = {\n                value2.value = it\n            })\n\n            Image(\n                bitmap = asImageAsset2(filter.apply(imread(name.value))),\n                contentDescription = \"Icon\",\n                modifier = Modifier.fillMaxSize())\n        }\n    }\n}\n```\n\nAnd now the core application code is just calling that CustomComponent:\n\n```\nMaterialTheme {\n        MyCustomOrigamiComponent(name)\n}\n```\n\nNow, by playing with the two sliders, you can see instant update of the Image.\n\n![image-20210528180036769](typora-user-images/image-20210528180036769.png)\n\n Et voila.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhellonico%2Forigami-compose-article","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhellonico%2Forigami-compose-article","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhellonico%2Forigami-compose-article/lists"}