Images and assets
Images work the way they do in React Native. require() a file next to your code and pass it to Image:
import { View, Image, StyleSheet } from 'react-native';
export default function Logo() {
return (
<View style={styles.screen}>
<Image source={require('./logo.png')} resizeMode="contain" style={styles.logo} />
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: 'center', justifyContent: 'center' },
logo: { width: 96, height: 96 },
});
There are three kinds of source, and they differ in what they cost on the device:
| Source | Example | Where the pixels live | Boards |
|---|---|---|---|
| Bundled | require('./logo.png') | In flash, drawn in place. No RAM per pixel. | All |
| SD card | { uri: 'file:///sd/photo.png' } | Read from the card while drawing. | Waveshare 1.46B and AMOLED-2.06 (not yet tested on hardware) |
| Network | { uri: 'https://example.com/a.png' } | Downloaded and decoded into PSRAM. | Boards with PSRAM (the 1.46B and AMOLED-2.06) |
A bare string such as source="logo.png" is an error that points you to require() or { uri }. Kog supports one source at a time; React Native also accepts arrays of source candidates, which Kog does not support. Kog’s bundled require() returns an object instead of React Native’s opaque numeric reference.
Bundled images
When you build, Kog finds every image you require() (or import), decodes it, converts it to the display's pixel format, and packs all of them into one asset pack, dist/assets.bin, next to your app bundle dist/app.mjs. The pack is written to its own flash partition, and the device draws each image directly from flash. An image costs a few dozen bytes of RAM for its descriptor, no matter how large it is, so bundled images are the right choice for icons, logos and backgrounds on every board, including the CYD.
- Formats. PNG (every color type and bit depth, transparency, interlaced), JPEG (baseline and progressive; the EXIF orientation is not applied), GIF (the first frame) and BMP (uncompressed). The format is detected from the file's contents, not its extension. WebP is refused with a build error that suggests PNG or JPEG.
- Pixel format. An image whose pixels are all opaque is stored as 16-bit color, 2 bytes per pixel. An image with any transparency keeps an 8-bit alpha plane, 3 bytes per pixel.
- What
require()returns. An object{ width, height, scale, … }describing the image, wherewidthandheightare in logical pixels.import logo from './logo.png'gives the same object.Image.resolveAssetSource(logo)returns{ uri, width, height, scale }, as in React Native. - Sizing. An
Imagewith no size in its style takes the image's own logical size. That also holds withStyleSheet.absoluteFill: unlike React Native, a bundledImagekeeps its own size between the insets unless its style setswidthandheight('100%'fills).ImageBackgroundalways fills its box.resizeModedefaults to'cover', as in React Native, and also takes'contain','stretch','center'and'repeat'(tiled). - Tinting.
tintColorrecolors the image, which suits single-color icons. - What gets packed. Every
require()d image is packed, even if the result is never used, exactly as Metro does. Animportthat is never used is left out. Building the same project twice gives a byte-identical pack.kog buildalso writesdist/assets.json, which lists each image's id, source file, size and format.
@2x and @3x variants
Some boards have a HiDPI panel. The round Waveshare ESP32-S3-Touch-LCD-1.46B and the Waveshare ESP32-S3-Touch-AMOLED-2.06 draw two physical pixels per logical pixel, while the CYD draws one. Add scaled files beside the original, and Kog picks one for the board, using React Native's rule:
src/
logo.png 96 x 96 scale 1 (logo@1x.png works too)
logo@2x.png 192 x 192 scale 2, used on the 1.46B and AMOLED-2.06
logo@3x.png 288 x 288 scale 3
You always write require('./logo.png'). The build picks the smallest scale that is at least the board's scale, or the largest one available if none is. If only some variants exist, the bare name still resolves to the best of them, and require('./logo@2x.png') uses exactly that file. Only the chosen file goes into the pack.
The board comes from board: in your kog.config, or from kog build --board <name>. With neither, images are picked for scale 1 and the build says so.
Limits
The pack lives in its own partition, so it never competes with your JavaScript heap:
| Board | Asset partition | What fits |
|---|---|---|
| Waveshare ESP32-S3-Touch-LCD-1.46B, ESP32-S3-Touch-AMOLED-2.06 | 4 MB | Plenty for full-screen backgrounds at 2x. |
CYD (cyd, cyd-classic) | 512 KB | About three full-screen 320 x 240 opaque images (153,600 bytes each), or hundreds of small icons. The app code itself must still fit the CYD's 76 KB bundle limit. |
Each image can be at most 65,535 pixels on a side and 16 megapixels in total. When the pack doesn't fit the board, nothing is written and you get a clear error:
dist/assets.bin is <size> B but this board's assets region holds 524288 B — use fewer or smaller images.
Getting assets onto the device
| Command | What happens to the pack |
|---|---|
kog build | Writes dist/app.mjs, dist/assets.bin and dist/assets.json. A build with no images removes stale ones. |
kog simulate | Loads the pack into the simulator, so images show exactly as on the device. |
kog flash | Writes the runtime firmware, and the pack too when you have built one. On the 1.46B and AMOLED-2.06 the first boot after a full flash takes longer, because the board prepares its flash-resident runtime once. |
kog dev | Sends the pack before the code, and only when it changed. Editing an image triggers a rebuild and push, so ordinary code edits stay sub-second. |
kog deploy | Writes the pack when it changed, then installs the signed app bundle for the next boot. |
Hot reload of images works. Save a new logo.png and kog dev repacks it, pushes it, and reloads your app. Writing a pack stops the running app, because the images it draws live in the region being rewritten. The app comes back when the code push that always follows lands, so that reload takes a moment longer than a code-only change. If a pack push fails partway, images draw nothing until the next successful push.
A board flashed before asset packs existed can't show bundled images. kog dev warns and keeps running; kog deploy stops. Both tell you to reflash with kog flash. A 1.46B flashed by an older Kog release needs a full kog flash once, because its partition layout changed and a firmware-only update can't move partitions.
Bundled audio
Sounds bundle the same way, the way they do in an Expo app: require() a .wav or .mp3 next to
your code and hand it to a player from expo-audio.
import { Pressable, Text } from 'react-native';
import { useAudioPlayer } from 'expo-audio';
export default function Doorbell() {
const chime = useAudioPlayer(require('./assets/chime.wav'));
return (
<Pressable onPress={() => chime.play()}>
<Text>Ring</Text>
</Pressable>
);
}
require('./chime.wav') (or import chime from './chime.wav') evaluates to a number, the sound's
id in the asset pack, which is expo-audio's AudioSource. The build puts the file into the same
pack as your images, dist/assets.bin, exactly as you authored it: nothing is decoded or
re-encoded at build time, and the device decodes it straight from flash while it plays. A sound
costs no RAM for its bytes, and an MP3 stays MP3-sized, so a 3-minute song is about 3 MB of flash
rather than the ~30 MB its PCM would be. MP3 suits music; short effects are fine as either.
| Format | Bundled | From the SD card |
|---|---|---|
| WAV: PCM 8/16/24-bit, float 32, IMA-ADPCM; mono or stereo; 8–48 kHz | Yes | Yes |
| MP3: MPEG-1/2/2.5 Layer III | Yes | Yes |
.m4a/AAC, .ogg, .opus, .flac, .aiff, .caf, … | A build error that names the conversion | An AUDIO_FORMAT error status |
Kog reads the format from the file's bytes, not its name. A .m4a in your project fails the build
with:
src/assets/intro.m4a: Kog decodes WAV and MP3 on device; convert with `ffmpeg -i intro.m4a intro.mp3`
and a file named .wav whose bytes aren't RIFF/WAVE fails too, rather than playing noise. Passing an
image require() to a player (or a sound to Image) is an error, not garbage on the speaker or
screen.
Bundled sounds travel with your images on every path in the table above: kog build writes them
into dist/assets.bin (and lists them in dist/assets.json with kind: 'audio'), kog simulate
loads them, kog flash writes them, and kog deploy writes the pack when it changed. Hot reload
of sounds works like hot reload of images: save a new chime.wav and kog dev repacks it and
pushes the pack before the code, because the pack's checksum changed even though no image did.
Studio does the same for a linked board. A pack that contains sounds needs firmware with audio
support; a board flashed before it refuses the pack, and kog flash updates it.
Sounds on the SD card play from a file:///sd/… URI (useAudioPlayer('file:///sd/music/a.mp3')),
read ahead from the card while they play, and recordings land on the card too. See the
Audio API. Network audio isn't streamed yet: an https:// source throws
AUDIO_SOURCE telling you to download the file to the card with fetch() or bundle it.
Images from an SD card
A file:// URI under /sd/ reads from the SD card:
<Image source={{ uri: 'file:///sd/photos/cat.png' }} style={{ width: 120, height: 120 }} />
The image is read from the card as it draws instead of being bundled. In the simulator, /sd/ maps to a folder you choose with kog simulate --sd-root <dir>; the simulator never reads outside that folder.
On devices, SD cards work on the Waveshare 1.46B and AMOLED-2.06 only:
- Not yet tested on hardware. The SD path is verified in the emulator, where PNG and LVGL
.binfiles from a FAT card render pixel-exact. It has not run on a real board yet. - How the card is wired. The firmware drives the slot in SD 1-bit mode over the chip's SDMMC host, not SPI, so the SPI bus stays free for your own
busOpen('spi')devices. The SD pins are reserved for the card. - When the card is read. Booting never touches the card. The FAT filesystem is mounted at
/sdcardthe first time an image opens afile:///sd/path. If no card answers, that image stays empty, and the next attempt waits at least 5 seconds. You can pull, re-insert or swap the card while the app runs: a card that stops answering is unmounted, and the next image that needs it mounts the new one, with no reboot. - File types. PNG, baseline JPEG and LVGL
.binimage files. PNG and.binare verified in the emulator; JPEG from a card is not tested yet. - File names. Long names work, up to 255 characters, including spaces and non-ASCII characters. Case doesn't matter, extensions included:
file:///sd/Photos/Beach.JPGopensbeach.jpg, andfile:///sd/my photo.pngopensMy Photo.PNG. The path is URL-decoded, as in React Native, soMy%20Photo.pngandMy Photo.pngboth openMy Photo.png, andCaf%C3%A9.pngopensCafé.png. Write a literal%as%25. A%that isn't a valid escape is an error that says so, and an escape that decodes to/or a NUL character is refused. A?queryor#fragmentis ignored, as in any file URL, sofile:///sd/x.png?v=2opensx.png(with one warning); write%3For%23if the name really contains?or#. A..segment is an error (SD paths cannot climb above the SD card root);.segments and dots inside a name are fine. - In the simulator on Linux, whose file system is case-sensitive, give each file under
--sd-roota lower-case extension (Beach.jpg, notBeach.JPG). The rest of the name can keep its case. macOS doesn't have this problem.
The CYD has no SD card support. Its firmware has no SD driver, so a file:///sd/ image there calls onError with ASSET_MISSING:
this board has no SD card drive; bundle 'file:///sd/photos/cat.png' with require('./image.png') instead
An SD image reports its result once, as React Native does. On success, onLoadStart, onLoad and onLoadEnd fire in that order, and onLoad carries the image's real logical size in nativeEvent.source. On failure, onError gets one of these codes, and the image is cleared:
| Code | When |
|---|---|
ASSET_MISSING | The file isn't on the card, no card is mounted, the card isn't ready, or the board has no SD drive. The message says which. |
IMAGE_DECODE | The file can't be read as an image, for example a broken file or an extension no decoder takes. |
IMAGE_UNSUPPORTED | A PNG or JPEG on a board without decoders. Convert it to an LVGL .bin or bundle it with require(). |
If you change source several times in quick succession, only the last source reports.
Images from the network
<Image
source={{ uri: 'https://example.com/avatar.png' }}
style={{ width: 64, height: 64 }}
onLoad={() => setReady(true)}
onError={(e) => setError(e.nativeEvent.error)}
/>
A network image is downloaded with fetch and decoded on the device, which needs width x height x 2 bytes or more of free memory. That is far more than a board without PSRAM has to spare, so the rule is:
-
On a board with PSRAM (the Waveshare 1.46B and AMOLED-2.06), network images work. PNG and baseline JPEG are decoded. One decoded image may use the largest free block of PSRAM minus a 256 KB reserve, about 5.6 MB on a freshly booted 1.46B. The device checks that budget before it decodes, and calls
onErrorwithIMAGE_NO_MEMORYinstead of running out of memory. -
On a board without PSRAM (the CYD), network images are refused, because the CYD firmware has no image decoder at all. Nothing is downloaded:
Imagechecks the board first and callsonErrorwithIMAGE_UNSUPPORTED, which explains why:network images need PSRAM to decode; bundle 'https://…' with require('./image.png') insteadThe build also warns you ahead of time when your configured board has no PSRAM. The warning appears in the
kog buildoutput and in Studio's Problems panel:src/App.tsx:8: <Image> source "https://…": network images need PSRAM to decode; cyd has none — use require('./x.png')
onError receives { nativeEvent: { error, code } }, where error is a message and code is IMAGE_UNSUPPORTED, IMAGE_NO_MEMORY, IMAGE_DECODE or ASSET_MISSING. A bundled image on a device or simulator that has no asset pack loaded reports ASSET_MISSING. onLoadStart, onLoad and onLoadEnd fire as in React Native, and defaultSource shows while the image loads. The image stays empty on failure; your app keeps running. When an Image unmounts or its source changes, its decoded pixels are freed.
If an image is the same for every user, bundle it with require() instead. It draws instantly, costs no RAM, and works on every board.
Network images in Studio and documentation widgets
Both browser simulators download the original bytes and pass them to the same PNG and baseline JPEG decoders used by the firmware. Transparency, image dimensions, RGB565 conversion, and load/error callbacks follow the device path; the browser's own image decoder does not substitute for the firmware decoder. Progressive JPEG, GIF, BMP, SVG, and WebP are not network image formats, even if your browser can display them in an ordinary web page. Bundled images support the broader format list above because they are converted at build time.
Browser networking still follows browser security rules. Use an HTTPS URL when Studio is served over HTTPS. A server on another origin must return an appropriate Access-Control-Allow-Origin header, such as * for public images. A URL that opens in a browser tab is not necessarily readable by a simulator's fetch. Same-origin images need no CORS configuration. Devices do not enforce browser CORS, but still need network access and a valid HTTPS certificate.
The web simulator limits a fetched response to 1 MiB and reports oversized downloads as a network error. It runs with simulator memory rather than the selected board's physical PSRAM budget; a successful simulated decode does not guarantee that a large image fits on a device. onError also reports HTTP failures (HTTP) and transport failures (such as a blocked CORS request), and onLoadEnd runs after either success or failure.
Backgrounds
ImageBackground draws an image behind its children, as in React Native:
<ImageBackground source={require('./sky.png')} resizeMode="cover" style={{ flex: 1, padding: 16 }}>
<Text style={{ color: '#ffffff' }}>Good morning</Text>
</ImageBackground>
Limits at a glance
| Waveshare 1.46B, AMOLED-2.06 | CYD | |
|---|---|---|
Bundled images (require) | Yes, 4 MB pack | Yes, 512 KB pack |
| Variant used | @2x | @1x |
SD card (file:///sd/…) | Yes, SD 1-bit, mounted on first use (not yet tested on hardware) | No |
Network images (https://…) | Yes, decoded into PSRAM | Refused with onError |
In Kog Studio, add images and sounds with Add image or sound… in the Explorer or by dragging them onto the file tree. Each lands in src/assets/, opens in a preview with the require() line to paste, and is delivered to the simulator and to a connected board exactly as kog dev does.