Images
PNG and JPEG decoding is client-side and pure JS
(pngjs,
jpeg-js — no native modules). A
decoded Image is independent of any X connection; the first time it is
drawn it uploads to that server as a 32-bit ARGB pixmap and is cached, so
repeated draws cost a single server-side composite.
import { createClient, loadImage } from 'ntk';
const app = await createClient();
const wnd = app.createWindow({ width: 400, height: 300 });
const ctx = wnd.getContext('2d');
const img = await loadImage('photo.jpg'); // path, file URL or Buffer
ctx.drawImage(img, 0, 0); // natural size
ctx.drawImage(img, 0, 0, 200, 150); // scaled (server-side, bilinear)
ctx.drawImage(img, 10, 10, 80, 80, 220, 10, 160, 160); // source crop + scale
wnd.map();
API
loadImage(source, options?)→Promise<Image>—sourceis a file path, a fileURL, or aBuffer/Uint8Arrayof encoded PNG/JPEG bytesdecodeImage(buffer, options?)→Image— synchronous decode of in-memory bytes; the format is sniffed from magic bytesoptions.imageOrientation—'from-image'(the default) turns a JPEG the way its EXIF Orientation says, see Orientation;'none'keeps the pixels as stored. The names are canvas's (createImageBitmap) and CSS's (image-orientation)
exifOrientation(buffer)→1–8— a JPEG's EXIF Orientation,1when it has none (or the bytes are not a JPEG). What an app that decodes withimageOrientation: 'none'needs to apply the turn itselfnew Image(imagedata)— anImageDatafromctx.getImageData()is already the right shape, so reading pixels back and turning them into a reusable server-side image needs no conversionnew Image({ width, height, data })— wrap raw non-premultiplied RGBA pixels (width * height * 4bytes)
Image
image.width,image.height— pixel dimensionsimage.data— non-premultiplied RGBA bytes (Buffer)image.picture(app)→Picture— the cached server-side picture for that app's display (uploaded on first call).ctx.drawImageuses this internally; it is public for manual Render compositingimage.pixmap(app)→Pixmap— the drawable those pixels were uploaded to (uploading on first call, likepicture). It is what building a second picture over the same upload needs:ctx.createPatternmakes a repeating one there rather than changing howpicture(app)samples everywhere elseimage.destroy()/Symbol.dispose— free the server-side copies (safe: the image re-uploads if drawn again). Client-side pixel data stays usable; the server resources are also reclaimed by GC as a fallback (see resource management)
Orientation
A camera held upright still reads its sensor sideways, and says so in the
JPEG's EXIF Orientation tag rather than by turning the pixels. A browser
applies the tag (CSS image-orientation: from-image, the default since
2020), and so does decodeImage: a photo taken in portrait comes out
portrait, with width and height swapped from the stored ones for the
four orientations that turn a quarter.
| Value | Stored pixels are shown | Size |
|---|---|---|
| 1 | as stored | w × h |
| 2 | mirrored left to right | w × h |
| 3 | turned half way | w × h |
| 4 | mirrored top to bottom | w × h |
| 5 | transposed (mirrored, then turned a quarter anticlockwise) | h × w |
| 6 | turned a quarter clockwise | h × w |
| 7 | transversed (mirrored, then turned a quarter clockwise) | h × w |
| 8 | turned a quarter anticlockwise | h × w |
Only the tag is read — tag 0x0112 of IFD0 in the first APP1 segment that
carries Exif, in either byte order — and a value it cannot read leaves the
image as stored. The turn is one pass over the pixels, ~18ms at 12
megapixels against ~800ms for jpeg-js to decode them. PNG has no
orientation here: an eXIf chunk is not read.
To keep the stored pixels — an editor that shows the tag, or an app that
turns the image at draw time with a transform instead — decode with
{ imageOrientation: 'none' } and read the value with exifOrientation().
Notes
- Alpha is handled correctly: pixels are premultiplied at upload, so
translucent PNGs blend with what is underneath (
Overcomposition). - Uploads are chunked to respect the server's maximum request length, so large images work over the wire.
- Drawing the same
Imageinto several windows/pixmaps of one app reuses one upload; using it with several apps (rare) keeps one upload per app.