Bildverarbeitung mit Lyx
std.image ist eine reine Decoder-Bibliothek: Sie lädt Bilder in ein einheitliches RGBA-Format, bietet Pixel-Lese- und Schreibzugriff, aber keine Encoder. Es gibt kein „Bild als PNG speichern“ — nur lesen, analysieren und programmatisch erzeugen.
Alle sieben Formate (BMP, PNG, JPEG, GIF, TIFF, WebP, AVIF) werden auf dasselbe Zielformat normalisiert: 4 Kanäle, 8 Bit/Kanal, RGBA, zeilenweise top-down.
import std.image.image; // Unified Facade: reicht für 90 % der Fälle
import std.image.core; // Struct-Direktzugriff, GrfImageAlloc, Pixel-Ops
Architektur
std/image/
├── core.lyx ← GrfImage-Struct, Allokation, Pixel-Ops, Format-Erkennung
├── image.lyx ← Unified Facade: ImageDecode, ImageDecodeFromMem, Accessoren
├── bmp.lyx ← BmpDecode
├── png.lyx ← PngDecode
├── jpeg.lyx ← JpegDecode
├── gif.lyx ← GifDecode
├── tiff.lyx ← TiffDecode
├── webp.lyx ← WebpDecode
└── avif.lyx ← AvifDecode
std.image.image importiert alle Format-Units automatisch. Wer nur JPEG braucht, kann direkt std.image.jpeg importieren — spart Link-Zeit und vermeidet unnötige Abhängigkeiten.
Minimales Beispiel
import std.image.image;
import std.image.core;
import std.alloc;
import std.io;
fn main(): int64 {
var img: int64 := GrfImageAlloc(0, 0); // leeres Struct anlegen
if (img == 0) { return 1; }
if (ImageDecode("foto.jpg"c as int64, 0, img) == 0) {
PrintLn("Laden fehlgeschlagen");
GrfImageFree(img);
return 1;
}
var w: int64 := ImageGetWidth(img);
var h: int64 := ImageGetHeight(img);
PrintLn(IntToStr(w) + "x" + IntToStr(h));
// Mittelpixel lesen — Format: 0xRRGGBBAA
var px: int64 := ImagePixelAt(img, w / 2, h / 2);
var r: int64 := (px >> 24) & 255;
var g: int64 := (px >> 16) & 255;
var b: int64 := (px >> 8) & 255;
PrintLn("R=" + IntToStr(r) + " G=" + IntToStr(g) + " B=" + IntToStr(b));
ImageFree(img);
return 0;
}
Workflow 1: Bild aus Datei laden
import std.image.image;
import std.image.core;
import std.alloc;
fn LoadImage(path: int64): int64 {
var img: int64 := GrfImageAlloc(0, 0);
if (img == 0) { return 0; }
// plen=0: Dateigröße wird nicht zurückgegeben (nicht benötigt)
if (ImageDecode(path, 0, img) == 0) {
GrfImageFree(img);
return 0;
}
return img; // Aufrufer muss ImageFree(img) aufrufen
}
ImageDecode liest die Datei intern in einen Puffer, erkennt das Format via Magic-Bytes und ruft den passenden Decoder auf. Kein manuelles Puffer-Management nötig.
Workflow 2: Bild aus Speicher-Puffer laden
Nützlich wenn das Bild bereits im RAM liegt — z.B. aus einem HTTP-Response, einem ZIP-Archiv oder einer eingebetteten Ressource:
import std.image.image;
import std.image.core;
fn DecodeFromBuffer(buf: int64, len: int64): int64 {
var img: int64 := GrfImageAlloc(0, 0);
if (img == 0) { return 0; }
if (ImageDecodeFromMem(buf, len, img) == 0) {
GrfImageFree(img);
return 0;
}
return img;
}
ImageDecodeFromMem benötigt mindestens 4 Bytes für die Format-Erkennung (WebP/AVIF brauchen 12 Bytes).
Workflow 3: Format-spezifischer Decoder
Wenn das Format bereits bekannt ist (z.B. eine Datei mit garantiert `.png`-Extension), spart der direkte Aufruf die Format-Erkennung:
import std.image.core;
import std.image.png;
import std.image.jpeg;
fn DecodeKnownFormat(buf: int64, len: int64, isPng: int64): int64 {
var img: int64 := GrfImageAlloc(0, 0);
if (img == 0) { return 0; }
var ok: int64 := 0;
if (isPng != 0) {
ok := PngDecode(buf, len, img);
} else {
ok := JpegDecode(buf, len, img);
}
if (ok == 0) { GrfImageFree(img); return 0; }
return img;
}
Oder mit GrfDetectFormat manuell prüfen:
import std.image.core;
import std.image.jpeg;
import std.image.png;
import std.image.bmp;
fn DecodeWithDetect(buf: int64, len: int64): int64 {
var fmt: int64 := GrfDetectFormat(buf, len);
var img: int64 := GrfImageAlloc(0, 0);
var ok: int64 := 0;
if (fmt == GRF_FMT_JPEG) { ok := JpegDecode(buf, len, img); }
if (fmt == GRF_FMT_PNG) { ok := PngDecode(buf, len, img); }
if (fmt == GRF_FMT_BMP) { ok := BmpDecode(buf, len, img); }
// Unbekannte Formate: ok bleibt 0
if (ok == 0) { GrfImageFree(img); return 0; }
return img;
}
Workflow 4: Durchschnittsfarbe berechnen
Ein typisches Analyse-Beispiel — alle Pixel iterieren und Mittelwert von R/G/B berechnen:
import std.image.image;
import std.image.core;
import std.alloc;
import std.io;
fn AverageColor(img: int64): void {
var w: int64 := ImageGetWidth(img);
var h: int64 := ImageGetHeight(img);
var sumR: int64 := 0;
var sumG: int64 := 0;
var sumB: int64 := 0;
var y: int64 := 0;
while (y < h) {
var x: int64 := 0;
while (x < w) {
var px: int64 := ImagePixelAt(img, x, y);
sumR := sumR + ((px >> 24) & 255);
sumG := sumG + ((px >> 16) & 255);
sumB := sumB + ((px >> 8) & 255);
x := x + 1;
}
y := y + 1;
}
var total: int64 := w * h;
PrintLn("Avg R=" + IntToStr(sumR / total) +
" G=" + IntToStr(sumG / total) +
" B=" + IntToStr(sumB / total));
}
Für Performance: bei großen Bildern jedes 4. Pixel samplen (x := x + 4) — Ergebnis bleibt repräsentativ, Laufzeit sinkt um Faktor 16.
Workflow 5: Bild programmatisch erzeugen
GrfImageAlloc mit echten Dimensionen anlegen und Pixel schreiben:
import std.image.core;
import std.alloc;
fn CreateCheckerboard(w: int64, h: int64, size: int64): int64 {
var img: int64 := GrfImageAlloc(w, h);
if (img == 0) { return 0; }
var y: int64 := 0;
while (y < h) {
var x: int64 := 0;
while (x < w) {
var check: int64 := ((x / size) + (y / size)) & 1;
var rgba: int64 := 0;
if (check == 0) {
rgba := 0xFFFFFFFF; // weiß, volle Deckkraft
} else {
rgba := 0x000000FF; // schwarz, volle Deckkraft
}
GrfImageSetPixel(img, x, y, rgba);
x := x + 1;
}
y := y + 1;
}
return img;
}
Zum Füllen mit einer Farbe die schnellere GrfImageClear-Funktion verwenden:
var img: int64 := GrfImageAlloc(800, 600);
GrfImageClear(img, 0xFF1A1A2EFF); // alle Pixel navy-blau, volle Deckkraft
Workflow 6: MJPEG-Frame aus AVI extrahieren
std.video.avi und std.image arbeiten direkt zusammen — AviExtractMjpegFrame dekodiert ein MJPEG-Frame aus dem AVI-Datenstrom in ein GrfImage:
import std.video.core;
import std.video.avi;
import std.image.core;
import std.image.jpeg;
import std.alloc;
import std.fs;
import std.io;
fn ExtractAviFrame(aviPath: int64, frameIdx: int64): int64 {
// AVI in Puffer laden
var fsize: int64 := FileSize(aviPath);
if (fsize <= 0) { return 0; }
var buf: int64 := alloc(fsize);
ReadFile(aviPath, buf, fsize);
// Metadaten prüfen — nur bei MJPEG sinnvoll
var vid: int64 := alloc(VID_SIZE);
if (AviMeta(buf, fsize, vid) == 0) {
free(buf, fsize); free(vid, VID_SIZE); return 0;
}
if (peek64(vid + VID_OFF_CODEC) != VID_CODEC_MJPEG) {
PrintLn("Kein MJPEG-Codec");
free(buf, fsize); free(vid, VID_SIZE); return 0;
}
// Frame dekodieren
var img: int64 := GrfImageAlloc(0, 0);
var ok: int64 := AviExtractMjpegFrame(buf, fsize, frameIdx, img);
free(buf, fsize);
free(vid, VID_SIZE);
if (ok == 0) { GrfImageFree(img); return 0; }
return img; // GrfImage mit RGBA-Pixeln; Aufrufer gibt frei
}
Workflow 7: RGBA → BGRA für Vega
Der Vega-Compositor (bsys_vega.lyx) arbeitet mit 32-Bit BGRA (Byte-Reihenfolge im Speicher: B, G, R, A). Beim Kopieren eines geladenen Bilds in einen Vega-Framebuffer müssen Kanal R und B getauscht werden:
import std.image.core;
// Kopiert img-Pixel in einen BGRA-Puffer (z.B. Vega-Fenster-FB)
fn CopyRgbaToBgra(img: int64, dst: int64): void {
var w: int64 := peek64(img + GRF_OFF_WIDTH);
var h: int64 := peek64(img + GRF_OFF_HEIGHT);
var pixels: int64 := peek64(img + GRF_OFF_PIXELS);
var pixLen: int64 := w * h * 4;
var i: int64 := 0;
while (i < pixLen) {
var r: int64 := peek8(pixels + i); // RGBA: Byte 0 = R
var g: int64 := peek8(pixels + i + 1);
var b: int64 := peek8(pixels + i + 2);
var a: int64 := peek8(pixels + i + 3);
// BGRA: Byte 0 = B, Byte 1 = G, Byte 2 = R, Byte 3 = A
poke8(dst + i, b);
poke8(dst + i + 1, g);
poke8(dst + i + 2, r);
poke8(dst + i + 3, a);
i := i + 4;
}
}
In Vega dann DisplayBlit oder BlitWindow mit dem konvertierten Puffer aufrufen.
Direktzugriff auf den Pixel-Buffer
Für hochperformante Operationen kann der interne Pixel-Buffer direkt über GRF_OFF_PIXELS angesprochen werden:
import std.image.core;
fn FastScan(img: int64): void {
var w: int64 := peek64(img + GRF_OFF_WIDTH);
var h: int64 := peek64(img + GRF_OFF_HEIGHT);
var pixels: int64 := peek64(img + GRF_OFF_PIXELS);
var pixLen: int64 := peek64(img + GRF_OFF_PIXLEN); // = w*h*4
// Zeiger-Arithmetik direkt auf den Puffer — kein Funktions-Overhead
var i: int64 := 0;
while (i < pixLen) {
// Byte-Reihenfolge im Puffer: R(i), G(i+1), B(i+2), A(i+3)
var r: int64 := peek8(pixels + i);
i := i + 4;
}
}
Der Puffer liegt zusammenhängend im Heap (alloziert von GrfImageAlloc). Stride = w × 4 Bytes, kein Padding.
Speicherverwaltung
GrfImageAlloc alloziert zwei Blöcke:
- Das Kontroll-Struct (56 Bytes, GRF_SIZE)
- Den Pixel-Buffer (w × h × 4 Bytes)
Beide werden von GrfImageFree / ImageFree freigegeben. Nie nur den Struct-Pointer freigeben — das leakt den Pixel-Buffer.
var img: int64 := GrfImageAlloc(800, 600);
// ... arbeiten ...
GrfImageFree(img); // gibt Struct UND Pixel-Buffer frei
// alternativ: ImageFree(img) aus std.image.image
Beim Fehlerfall in einem Decoder: Der Decoder gibt kein partiell alloziertes GrfImage zurück. Wenn ImageDecode 0 zurückgibt, ist der img-Puffer noch in dem Zustand wie vor dem Aufruf — bei GrfImageAlloc(0,0) hat GRF_OFF_PIXELS den Wert 0, GrfImageFree ist trotzdem sicher.
Einschränkungen
| Was | Einschränkung |
|---|---|
| Encoder | Nicht vorhanden — kein PNG-/JPEG-Schreiben |
| Resize / Transform | Nicht vorhanden — Pixel-Loop selbst implementieren |
| Farbräume | Alle Decoder liefern immer RGBA; kein YCbCr, kein CMYK als Ausgabe |
| GIF-Animationen | Nur der erste Frame wird dekodiert |
| Alpha-Kanal | Immer vorhanden (Kanal 4); bei JPEG und BMP ist a=255 |
| Maximale Bildgröße | Durch verfügbaren Heap begrenzt; 4K (3840×2160) = ~31 MB Pixel-Buffer |
| Thread-Safety | Kein Locking — ein GrfImage pro Thread oder extern synchronisieren |
Entscheidungsguide
| Ich will … | Import |
|---|---|
| Bild aus Datei laden (beliebiges Format) | std.image.image |
| Bild aus Speicher laden (HTTP-Antwort, ZIP) | std.image.image |
| Nur JPEG verarbeiten (weniger Abhängigkeiten) | std.image.jpeg + std.image.core |
| Nur PNG verarbeiten | std.image.png + std.image.core |
| Format per Magic-Bytes prüfen | std.image.core (GrfDetectFormat) |
| Bild programmatisch erzeugen | std.image.core (GrfImageAlloc + GrfImageSetPixel) |
| MJPEG-Frame aus AVI holen | std.video.avi + std.image.core + std.image.jpeg |
| Bild für Vega-Compositor vorbereiten | RGBA→BGRA-Konvertierung (Workflow 7) |
Letzte Aktualisierung: 2026-06-15
