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.

Guides · std.image Referenz

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