Images
Back on Text, you met Doodle's other drawing primitive with an async loading step. Images play by the same rules — load first, then draw — and they're our last stop in the Rendering section.
Images draw directly to the canvas like any other primitive — transforms and clipping treat them no differently than shapes or text. You load one with ImageLoader
Include the ImageModule (Web, Desktop) in your application to use ImageLoader.
package rendering
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.Modules.Companion.ImageModule
import io.nacular.doodle.application.application
import io.nacular.doodle.core.Display
import io.nacular.doodle.image.ImageLoader
import org.kodein.di.instance
class ImageLoaderApp(display: Display, images: ImageLoader): Application {
override fun shutdown() {}
}
fun imageLoader() {
//sampleStart
application(modules = listOf(ImageModule)) {
ImageLoaderApp(display = instance(), images = instance())
}
//sampleEnd
}ImageLoader is happy to load from URLs, file paths, or LocalFiles (from drag-and-drop or a FileSelector):
package rendering
import io.nacular.doodle.image.ImageLoader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
fun CoroutineScope.loadImage(images: ImageLoader) {
//sampleStart
launch {
val image = images.load("some_image_path")
// won't get here until load resolves
image?.let {
// ...
}
}
//sampleEnd
}
Loading can hang — see Handling Timeouts on the Text page for a pattern you can borrow here too.
Once it's loaded, actually drawing it is a single call away. You get to choose which rectangular region of the image gets used (for cropping) and where it lands on the canvas (for scaling) — exactly how you'd zoom, crop, or scale an image as you draw it
Pass a source rectangle, and it crops to that region before drawing; pair that with the image's own ImageLoader and you can build an "aspect-fill" crop that always covers its destination without distorting. Both tricks are the same image call wearing different arguments — see the live demos below for the full worked versions.
- Image
- Cropped
- Aspect Cropped
package io.nacular.doodle.docs.apps
import io.nacular.doodle.application.Application
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.view
import io.nacular.doodle.docs.utils.controlBackgroundColor
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.image.ImageLoader
import io.nacular.doodle.image.aspectRatio
import io.nacular.doodle.layout.constraints.constrain
import io.nacular.doodle.utils.Resizer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class ImageApp(display: Display, images: ImageLoader, appScope: CoroutineScope): Application {
init {
appScope.launch {
var defaultLayoutPending = true
//sampleStart
val image = images.load("/doodle/images/photo.jpg") ?: return@launch
display += view {
render = {
image(image, destination = bounds.atOrigin)
}
Resizer(this)
}
//sampleEnd
display.layout = constrain(display.first()) {
if (defaultLayoutPending && !parent.size.value.empty) {
defaultLayoutPending = false
it.width eq it.height * image.aspectRatio
it.height eq parent.height - 80
it.center eq parent.center
} else {
it.top prefers it.top.value
it.left prefers it.left.value
it.width prefers it.width.value
it.height prefers it.height.value
}
}
display.fill(controlBackgroundColor.paint)
}
}
override fun shutdown() {}
}package io.nacular.doodle.docs.apps
import io.nacular.doodle.application.Application
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.view
import io.nacular.doodle.docs.utils.controlBackgroundColor
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.div
import io.nacular.doodle.image.ImageLoader
import io.nacular.doodle.layout.constraints.Strength.Companion.Required
import io.nacular.doodle.layout.constraints.constrain
import io.nacular.doodle.layout.constraints.minus
import io.nacular.doodle.utils.Resizer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class CroppedImageApp(display: Display, images: ImageLoader, appScope: CoroutineScope): Application {
init {
appScope.launch {
var defaultLayoutPending = true
//sampleStart
val image = images.load("/doodle/images/photo.jpg") ?: return@launch
display += view {
render = {
image(image, source = Rectangle(image.size / 2), destination = bounds.atOrigin)
}
Resizer(this)
}
//sampleEnd
display.layout = constrain(display.first()) {
if (defaultLayoutPending && !parent.size.value.empty) {
defaultLayoutPending = false
it.width eq it.height
it.height eq parent.height - 80 strength Required - 1
it.center eq parent.center
} else {
it.top prefers it.top.value
it.left prefers it.left.value
it.width prefers it.width.value
it.height prefers it.height.value
}
}
display.fill(controlBackgroundColor.paint)
}
}
override fun shutdown() {}
}package io.nacular.doodle.docs.apps
import io.nacular.doodle.application.Application
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Color
import io.nacular.doodle.drawing.Color.Companion.White
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.div
import io.nacular.doodle.image.ImageLoader
import io.nacular.doodle.image.aspectRatio
import io.nacular.doodle.layout.constraints.constrain
import io.nacular.doodle.utils.Resizer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.min
class AspectCroppedImageApp(display: Display, images: ImageLoader, appScope: CoroutineScope): Application {
init {
appScope.launch {
var defaultLayoutPending = true
//sampleStart
val image = images.load("/doodle/images/photo.jpg") ?: return@launch
display += view {
render = {
val w = min(height * image.aspectRatio, width)
val h = w / image.aspectRatio
rect(bounds.atOrigin, fill = White.paint)
image(
image = image,
source = Rectangle(image.size / 2),
destination = Rectangle((width - w) / 2, (height - h) / 2, w, h)
)
}
Resizer(this)
}
//sampleEnd
display.layout = constrain(display.first()) {
if (defaultLayoutPending && !parent.size.value.empty) {
defaultLayoutPending = false
it.width eq it.height * image.aspectRatio
it.height eq parent.height - 80
it.center eq parent.center
} else {
it.top prefers it.top.value
it.left prefers it.left.value
it.width prefers it.width.value
it.height prefers it.height.value
}
}
display.fill(Color(0xf1f2f5u).paint)
}
}
override fun shutdown() {}
}image also accepts a corner radius and opacity.
Recap
ImageLoaderneedsImageModule(Web, Desktop) — load an image before you can draw it.imagedraws a loadedImage, withsource/destinationcontrolling cropping and scaling.- Loading can hang, same as font loading — reuse the timeout pattern from the Text page.
That's a wrap on Rendering — Canvas, Paint & Stroke, Shapes & Paths, Text, and Images, all behind you. Next: on to Layout to see how Views get sized and positioned in the first place, before they ever reach render.