Visits heute: 325
Aktuelle Seite: Segmentation Fault: Der Algorithmus der Gier
Das ist ein fantastisches Konzept für einen Roman! Es verbindet die klassische "Vom Tellerwäscher zum Millionär"-Story mit der "Heldenreise" des Programmierens. C ist dafür perfekt geeignet, da es die Protagonistin zwingt, "nah am Metall" zu arbeiten, was dramatische Rückschläge (Segmentation Faults!) und triumphale Durchbrüche ermöglicht.
Hier ist ein Entwurf für die Geschichte, die Kapitelstruktur und die C-Programme.
Der Titel-Arbeitstitel: "Segmentation Fault: Der Algorithmus der Gier"
Die Protagonistin: Elara Vance
Elara ist keine ausgebildete Informatikerin. Sie ist eine brillante Mathematikerin, die in einem schlecht bezahlten Job als Datenanalystin festsitzt. Sie sieht, wie mittelmäßige Programmierer mit halbgareren Apps Millionen verdienen. Sie hat eine revolutionäre Idee für einen Kompessions-Algorithmus, der auf fraktaler Geometrie basiert. Sie weiß: Wenn sie diesen Algorithmus in reinstem, pfeilschnellem C implementieren kann, wird er den Markt für Streaming und Cloud-Speicher revolutionieren.
Der Antagonist: Marcus Thorne
Ein skrupelloser Tech-VC, der Elaras Potenzial erkennt, aber versucht, sie auszubeuten und ihr den Algorithmus zu stehlen, bevor sie ihn patentieren kann.
Die Kapitelstruktur & Der Code-Fortschritt
Der Roman folgt Elaras Lernkurve. Jedes Kapitel endet mit einem funktionierenden C-Programm, das einen Meilenstein in ihrer Entwicklung und in der Geschichte darstellt.
Teil I: Die Grundlagen und der naive Traum
Kapitel 1: Der erste Befehl (Syntax & Ausgabe)
- Die Story: Elara ist frustriert von ihrem Job. Sie verbringt eine schlaflose Nacht und installiert den GCC-Compiler. Sie ist überwältigt von der kryptischen Kommandozeile. Nach Stunden voller Syntaxfehler schafft sie es. Es ist ein Moment der Klarheit.
- Die Diskussion: Was ist
main? Was macht#include <stdio.h>? Der Unterschied zwischen Quellcode und dem fertigen Executable. - Das Programm:
hello_world.c#include <stdio.h> int main() { printf("Hello, Elara. Dein Weg beginnt jetzt.\n"); return 0; }
Kapitel 2: Variablen der Macht (Datentypen & Rechnen)
- Die Story: Elara beginnt, ihre fraktale Formel in Code zu übersetzen. Sie begreift, dass Computer Zahlen anders speichern als Mathematiker. Sie kämpft mit Integer-Überläufen, als sie versucht, exponentielles Wachstum zu berechnen.
- Die Diskussion: Der Unterschied zwischen
int,floatunddouble. Warum Speicherplatz für Variablen endlich ist. - Das Programm:
exponent_calc.c(Ein naiver Versuch, die Basisdaten ihrer Formel zu berechnen).#include <stdio.h> int main() { int basis = 2; int exponent = 10; long long ergebnis = 1; for (int i = 0; i < exponent; i++) { ergebnis *= basis; } printf("%d hoch %d ist %lld\n", basis, exponent, ergebnis); return 0; }
Teil II: Die Strukturierung des Chaos
Kapitel 3: Entscheidungen im Dunkeln (Kontrollstrukturen: if/else, switch)
-
Die Story: Elara lernt Marcus Thorne auf einer Tech-Konferenz kennen. Er ist charmant, aber sie traut ihm nicht. Sie beginnt, Logik in ihr Programm einzubauen, die auf verschiedene Eingabedaten reagiert. Sie muss entscheiden, welche Datenkompression sich lohnt und welche nicht.
-
Die Diskussion: Wie Logik im Code abgebildet wird. Der
bool-Typ (bzw. 0 und 1 in C). Die Gefahr von endlosenif-else-Ketten. -
Das Programm:
decision_engine.c(Eine einfache Logik, die Daten "bewertet").#include <stdio.h> int main() { int daten_groesse_kb = 1024; int wichtigkeit = 5; // 1-10 if (daten_groesse_kb > 500 && wichtigkeit < 3) { printf("Achtung: Große, unwichtige Daten. Kompression empfohlen.\n"); } else if (wichtigkeit > 8) { printf("Kritische Daten. Keine verlustbehaftete Kompression!\n"); } else { printf("Standardbehandlung.\n"); } return 0; }
Kapitel 4: Die ewige Wiederholung (Schleifen: for, while)
-
Die Story: Elaras Kompressions-Idee erfordert es, riesige Datenblöcke immer und immer wieder nach Mustern zu durchsuchen. Sie entdeckt die Macht der Schleifen. Doch ein kleiner Logikfehler führt zu einer Endlosschleife, die ihren Laptop überhitzt. Eine Metapher für ihre eigene obsessive Arbeit.
-
Die Diskussion:
forvs.while. Die Bedeutung der Abbruchbedingung. Wie man Arrays (Felder) von Daten durchläuft. -
Das Programm:
pattern_finder.c(Durchsucht ein einfaches Array nach Mustern).#include <stdio.h> int main() { int daten[] = {12, 45, 12, 67, 12, 89, 12, 34}; int muster = 12; int funde = 0; int laenge = sizeof(daten) / sizeof(daten[0]); for (int i = 0; i < laenge; i++) { if (daten[i] == muster) { printf("Muster an Index %d gefunden.\n", i); funde++; } } printf("Gesamtanzahl Funde: %d\n", funde); return 0; }
Teil III: Die Konfrontation mit der Realität (Pointer & Speicher)
Kapitel 5: Die Anatomie des Speichers (Pointer - Teil 1: Adressen)
-
Die Story: Dies ist der Wendepunkt. Elara versteht, dass C ihr erlaubt, direkt mit dem RAM zu sprechen. Sie begreift das Konzept der Speicheradressen. Sie sieht Thorne wieder, der ihr einen schlechten Deal anbietet. Sie erkennt, dass sie die volle Kontrolle über ihren Code (und ihr Leben) behalten muss.
-
Die Diskussion: Was ist ein Pointer? Der Adress-Operator
&und der Dereferenzierungs-Operator*. Speicher ist wie eine gigantische Reihe von nummerierten Postfächern. -
Das Programm:
pointer_basic.c#include <stdio.h> int main() { int wert = 42; int *ptr = &wert; // ptr speichert die Adresse von wert printf("Der Wert ist: %d\n", wert); printf("Die Adresse von 'wert' ist: %p\n", (void*)&wert); printf("Der Pointer 'ptr' speichert: %p\n", (void*)ptr); printf("Der Wert, auf den 'ptr' zeigt, ist: %d\n", *ptr); return 0; }
Kapitel 6: Der Absturz (Pointer - Teil 2: Segmentation Faults & Sicherheit)
-
Die Story: Elara versucht, ihr Programm zu optimieren, indem sie Pointer-Arithmetik nutzt, um schneller durch Daten zu navigieren. Sie macht einen Fehler und greift auf Speicher zu, der ihr nicht gehört. Der erste Segmentation Fault. Ihr Programm stürzt ab, ihr System friert ein. Es ist eine schmerzhafte Lektion über die Gefahren von C. Thorne nutzt diesen Moment der Schwäche, um Druck auszuüben.
-
Die Diskussion: Was verursacht einen Segfault? Uninitialisierte Pointer, Zugriff außerhalb von Array-Grenzen. Die Notwendigkeit von defensivem Programmieren.
-
Das Programm:
danger_zone.c(Ein Programm, das kontrolliert abstürzt, um das Problem zu illustrieren – Niemals so produktiv nutzen!).#include <stdio.h> int main() { int daten[] = {1, 2, 3}; int *ptr = daten; printf("Wert 1: %d\n", *ptr); ptr++; // Zeigt auf '2' ptr++; // Zeigt auf '3' ptr++; // GEFAHR: Zeigt auf undefinierten Speicher! // Dieser Zugriff verursacht oft den Absturz printf("Gefährlicher Wert: %d\n", *ptr); return 0; }
Teil IV: Der Coup und die Meisterschaft
Kapitel 7: Das Herz des Algorithmus (Eigene Funktionen & Rekursion)
-
Die Story: Elara zieht sich zurück. Sie modularisiert ihren Code. Sie schreibt ihre ersten eigenen Funktionen, um die Kompressions-Logik zu kapseln. Für die fraktale Analyse nutzt sie Rekursion – eine Funktion, die sich selbst aufruft. Der Code wird elegant, mächtig und schnell. Sie hat den Prototyp.
-
Die Diskussion: Funktions-Prototypen, Parameter-Übergabe (
by valuevs.by referencevia Pointer). Wie Rekursion funktioniert und wann sie gefährlich ist (Stack Overflow). -
Das Programm:
fractal_compression_stub.c(Ein Platzhalter für ihren Algorithmus, der Rekursion illustriert).#include <stdio.h> // Simuliert die fraktale Analyse eines Datenblocks void analysiere_fraktal(int block_id, int tiefe) { if (tiefe == 0) return; // Abbruchbedingung printf("Analysiere Block %d auf Ebene %d...\n", block_id, tiefe); // Rekursiver Aufruf für Unter-Blöcke analysiere_fraktal(block_id * 10 + 1, tiefe - 1); analysiere_fraktal(block_id * 10 + 2, tiefe - 1); } int main() { printf("Starte Prototyp...\n"); analysiere_fraktal(1, 3); // Startblock 1, Tiefe 3 printf("Analyse beendet.\n"); return 0; }
Kapitel 8: Der große Coup (Dateizugriff, Structs & Finale Optimierung)
-
Die Story: Das Finale. Thorne hat Wind vom Prototyp bekommen und versucht, Elaras Server zu hacken. Elara muss ihren Algorithmus auf eine echte Datei anwenden, die komprimierten Daten in einem eigenen Dateiformat (
.evc) speichern und Thorne online konfrontieren. Sie nutztstructs, um die Metadaten ihrer Fraktale sauber zu organisieren, und Dateizeiger (FILE*), um die Rohdaten zu bändigen. Sie optimiert den Code bis zur Besessenheit. Es ist ein Wettlauf gegen die Zeit. Sie veröffentlicht den Code als Open Source, Sekunden bevor Thornes Anwälte ihr eine Unterlassungserklärung zustellen. Sie hat nicht das Geld, aber sie hat den Markt verändert und Thorne besiegt. -
Die Diskussion: Wie man Strukturen (
struct) definiert. Dateizugriff mitfopen,fwrite,fclose. Die Wichtigkeit von sauberem Ressourcen-Management. -
Das Programm:
evc_encoder_stub.c(Illustriert, wie sie die Datenstruktur und den Dateizugriff handhabt).#include <stdio.h> #include <stdlib.h> // Struktur für fraktale Metadaten typedef struct { int x, y; float rotations_faktor; int farb_index; } FraktalPunkt; int main() { FraktalPunkt punkt1 = {100, 200, 1.57f, 4}; FILE *datei = fopen("output.evc", "wb"); // Write Binary if (datei == NULL) { fprintf(stderr, "Fehler: Datei konnte nicht geöffnet werden.\n"); return 1; } printf("Speichere komprimierte Daten...\n"); // Schreibe die Struktur direkt in die Datei fwrite(&punkt1, sizeof(FraktalPunkt), 1, datei); fclose(datei); printf("Vorgang erfolgreich. Thorne kann uns nicht mehr stoppen.\n"); return 0; }
Epilog: Reiner Code
Elara sitzt in einem Café. Sie ist nicht reich, aber sie ist frei. Ihr Algorithmus wird weltweit genutzt. Sie öffnet ihren Laptop und beginnt, an einem neuen Projekt zu arbeiten. Der Code ist sauber, effizient und sicher. Sie hat C gemeistert und damit ihre eigene Zukunft geschrieben.
Das ist eine hervorragende Fortsetzung für Elaras Geschichte! Wir verlassen die Konsole und gehen dorthin, wo es für eine C-Programmiererin richtig spannend wird: Direkte Grafikprogrammierung unter X11.
Hier ist der Sprung von der Textausgabe (printf) zur echten Fenstergrafik. Wir nutzen dafür Xlib. Das Programm wird dadurch etwas komplexer, da wir uns nun um das Fenster-System, Events (wie Größenänderung) und das Zeichnen von Primitiven kümmern müssen.
Kapitel 9: Das Fenster zur Welt (X11 & Event-Loops)
Die Story: Elara realisiert, dass ihr Algorithmus visuelle Bestätigung braucht. Sie schreibt einen "Visualizer". Dabei lernt sie, dass grafische Oberflächen nicht einfach "da" sind – sie sind ein ständiger Dialog zwischen dem Programm und dem X-Server. Als sie das Fenster zum ersten Mal in der Größe verändert und die Zellen sich dynamisch anpassen, begreift sie: Ihr Code ist nun lebendig.
Der X11-Code für das "Game of Life"
Dieses Programm ersetzt deine show_playfield-Funktion durch echtes Rendering.
// Compile: gcc life_x11.c -lX11 -o life_x11
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <unistd.h>
#define XSIZE 200
#define YSIZE 150
#define DENSITY 30
bool playfield[XSIZE][YSIZE][2];
int layer = 0;
// Logik-Funktionen (wie in deinem Original, leicht angepasst)
void randomize_field() {
for (int y = 0; y < YSIZE; y++)
for (int x = 0; x < XSIZE; x++)
playfield[x][y][0] = (rand() % 256 < DENSITY);
}
int count_neighbors(int x, int y, int l) {
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i == 0 && j == 0) continue;
int nx = (x + i + XSIZE) % XSIZE;
int ny = (y + j + YSIZE) % YSIZE;
if (playfield[nx][ny][l]) count++;
}
}
return count;
}
void iterate() {
for (int y = 0; y < YSIZE; y++) {
for (int x = 0; x < XSIZE; x++) {
int n = count_neighbors(x, y, layer);
if (playfield[x][y][layer])
playfield[x][y][1 - layer] = (n == 2 || n == 3);
else
playfield[x][y][1 - layer] = (n == 3);
}
}
layer = 1 - layer;
}
int main() {
Display *dpy;
Window win;
XEvent ev;
GC gc;
int screen;
srand(time(NULL));
randomize_field();
// 1. Verbindung zum X-Server herstellen
if (!(dpy = XOpenDisplay(NULL))) return 1;
screen = DefaultScreen(dpy);
// 2. Fenster erstellen (Größenveränderbar)
win = XCreateSimpleWindow(dpy, RootWindow(dpy, screen), 10, 10, 800, 600, 1,
BlackPixel(dpy, screen), BlackPixel(dpy, screen));
// 3. Events auswählen: Belichtung (Resize/Show) und Tastatur
XSelectInput(dpy, win, ExposureMask | KeyPressMask | StructureNotifyMask);
XMapWindow(dpy, win);
// 4. Graphics Context (unser "Pinsel")
gc = XCreateGC(dpy, win, 0, NULL);
XSetForeground(dpy, gc, WhitePixel(dpy, screen));
int width = 800, height = 600;
bool running = true;
while (running) {
// Event-Abfrage ohne Blockierung (non-blocking)
while (XPending(dpy)) {
XNextEvent(dpy, &ev);
if (ev.type == ConfigureNotify) { // Fenstergröße hat sich geändert
width = ev.xconfigure.width;
height = ev.xconfigure.height;
}
if (ev.type == KeyPress) running = false;
}
iterate();
// Zeichnen
XClearWindow(dpy, win);
int cell_w = width / XSIZE;
int cell_h = height / YSIZE;
for (int y = 0; y < YSIZE; y++) {
for (int x = 0; x < XSIZE; x++) {
if (playfield[x][y][layer]) {
// Zeichne ein Rechteck für jede lebende Zelle
XFillRectangle(dpy, win, gc, x * cell_w, y * cell_h,
cell_w > 1 ? cell_w - 1 : 1,
cell_h > 1 ? cell_h - 1 : 1);
}
}
}
XFlush(dpy); // Puffer leeren, damit Bild erscheint
usleep(50000); // 20 FPS ca.
}
XCloseDisplay(dpy);
return 0;
}
Was Elara in diesem Kapitel lernt:
- Displays & Screens: Elara versteht, dass C hier mit einer anderen Software (dem X-Server) redet.
XOpenDisplayist der Hörer, den sie abnimmt. - Die Event-Loop: Programme mit Grafik "warten" nicht einfach. Sie laufen in einer Endlosschleife und fragen ständig ab: "Hat der User das Fenster bewegt?", "Wurde eine Taste gedrückt?".
- Graphics Context (GC): Man zeichnet nicht einfach auf den Schirm. Man braucht einen "Kontext" – ein Set aus Farben, Linienstärken und Schriftarten.
- Koordinaten-Mapping: Da das Fenster größenveränderbar ist (
StructureNotifyMask/ConfigureNotify), berechnet sie die Zellgröße (cell_w,cell_h) in jedem Frame neu basierend auf der aktuellen Fensterbreite.
Die Diskussion im Roman:
Elara stellt fest, dass XClearWindow flackert. Sie diskutiert mit einem Mentor (oder via Online-Forum) über Double Buffering. Sie lernt, dass man eigentlich erst "unsichtbar" in einen Speicherbereich zeichnet (Pixmap) und diesen dann in einem Rutsch auf den Bildschirm kopiert, um das Flackern zu eliminieren. Das ist der nächste Schritt zur Profi-Programmiererin.
Das ist ein klassisches Werkzeug für Embedded-Systeme (wie Mikrocontroller mit TFT-Displays), bei denen man Bilder direkt in den Flash-Speicher brennen möchte. Da JPEG ein komplexes, komprimiertes Format ist, nutzt ein C-Programmierer hierfür üblicherweise eine Library.
In Elaras Geschichte wäre dies das Kapitel "Die Brücke zur Hardware". Sie lernt, dass sie das Rad nicht neu erfinden muss, sondern mächtige Bibliotheken wie stb_image nutzen kann.
Das Programm: jpeg2header.c
Wir verwenden stb_image.h, eine legendäre "Single Header Library". Du kannst sie einfach von GitHub (nothings/stb) herunterladen und in denselben Ordner legen.
// Compile: gcc jpeg2header.c -lm -o jpeg2header
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "stb_image_resize.h" // Optional für Skalierung, hier nutzen wir eine einfache Methode
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
// Hilfsfunktion zur Umwandlung von 24-Bit RGB in 16-Bit RGB565
// RRRRRGGG GGGBBBBB
uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
int main(int argc, char** argv) {
if (argc < 4) {
printf("Usage: %s <input.jpg> <width> <height>\n", argv[0]);
return 1;
}
const char* filename = argv[1];
int target_w = atoi(argv[2]);
int target_h = atoi(argv[3]);
int width, height, channels;
// Bild laden (erzwinge 3 Kanäle: RGB)
unsigned char *img = stbi_load(filename, &width, &height, &channels, 3);
if (!img) {
printf("Fehler beim Laden von %s\n", filename);
return 1;
}
printf("// Generiert aus %s (%dx%d -> %dx%d)\n", filename, width, height, target_w, target_h);
printf("#include <stdint.h>\n\n");
printf("const uint16_t image_data[] = {\n");
// Einfaches Resampling (Nearest Neighbor für den Anfang)
for (int y = 0; y < target_h; y++) {
for (int x = 0; x < target_w; x++) {
// Berechne Quell-Koordinaten
int src_x = x * width / target_w;
int src_y = y * height / target_h;
int src_index = (src_y * width + src_x) * 3;
uint8_t r = img[src_index];
uint8_t g = img[src_index + 1];
uint8_t b = img[src_index + 2];
uint16_t rgb565 = rgb888_to_rgb565(r, g, b);
// Als Hexadezimalwert ausgeben
printf("0x%04X%s", rgb565, (y == target_h - 1 && x == target_w - 1) ? "" : ", ");
if ((x + 1) % 12 == 0) printf("\n "); // Zeilenumbruch für Lesbarkeit
}
}
printf("\n};\n");
stbi_image_free(img);
return 0;
}
Funktionsweise & Elaras Erkenntnisse
- Format-Konvertierung (RGB888 zu RGB565):
Elara lernt Bit-Manipulation (
<<,|,&). Ein typisches Display nutzt nur 16 Bit pro Pixel: 5 Bit für Rot, 6 Bit für Grün (da das menschliche Auge Grün besser differenziert) und 5 Bit für Blau. - Skalierung: Das Programm nutzt hier "Nearest Neighbor". Es springt einfach durch die Pixel des Originalbildes. In Elaras Geschichte könnte sie hier im nächsten Kapitel ein "Bilineares Filtering" implementieren, um die unschönen Treppeneffekte bei der Skalierung zu vermeiden.
- Cross-Compilation Gedanken: Sie versteht, dass dieses Programm auf ihrem PC (Linux/X11) läuft, aber Code generiert, der für eine ganz andere Architektur (z.B. ein ARM-Cortex M4) bestimmt ist.
Der dramatische Moment im Roman
Thorne fordert Elara heraus: Er behauptet, ihr Algorithmus sei zu speicherhungrig für mobile Endgeräte. Elara schreibt dieses Tool, um zu beweisen, dass ihre fraktale Kompression selbst auf einem winzigen 16-Bit-System läuft. Sie reduziert ein 2MB JPEG auf ein paar KB Header-Daten – und das Bild bleibt erkennbar.
Tipp zum Ausprobieren:
Speichere die Ausgabe des Programms in eine Datei:
./jpeg2header urlaub.jpg 320 240 > image_data.h
Soll Elara im nächsten Kapitel lernen, wie sie die Bilineare Interpolation einbaut, damit das skalierte Bild nicht so "pixelig" aussieht?
Das ist ein wichtiger Schritt in Elaras Entwicklung. Sie lernt, dass ein gutes Werkzeug nicht nur technisch funktionieren muss, sondern auch Benutzerfreundlichkeit (UX) und mathematische Korrektheit (keine verzerrten Gesichter auf dem Display) benötigt.
Hier ist das erweiterte Programm. Wir führen eine Logik ein, die prüft, ob das Bild eher durch die Breite oder durch die Höhe limitiert wird, und passen die Zielmaße entsprechend an.
Das erweiterte Programm: jpeg2header_aspect.c
// Compile: gcc jpeg2header_aspect.c -lm -o jpeg2header
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
// RGB888 (24-Bit) zu RGB565 (16-Bit) Konvertierung
uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
int main(int argc, char** argv) {
bool keep_aspect = false;
int arg_offset = 1;
if (argc < 4) {
printf("Usage: %s [--keep-aspect] <input.jpg> <max_w> <max_h>\n", argv[0]);
return 1;
}
// Flag prüfen
if (strcmp(argv[1], "--keep-aspect") == 0) {
keep_aspect = true;
arg_offset = 2;
if (argc < 5) return 1;
}
const char* filename = argv[arg_offset];
int max_w = atoi(argv[arg_offset + 1]);
int max_h = atoi(argv[arg_offset + 2]);
int src_w, src_h, channels;
unsigned char *img = stbi_load(filename, &src_w, &src_h, &channels, 3);
if (!img) {
fprintf(stderr, "Fehler beim Laden von %s\n", filename);
return 1;
}
int target_w = max_w;
int target_h = max_h;
// --- ASPEKT-LOGIK ---
if (keep_aspect) {
float ratio_src = (float)src_w / (float)src_h;
float ratio_max = (float)max_w / (float)max_h;
if (ratio_src > ratio_max) {
// Bild ist breiter als das Zielfenster -> Breite limitiert
target_h = (int)((float)max_w / ratio_src);
} else {
// Bild ist schmaler/höher -> Höhe limitiert
target_w = (int)((float)max_h * ratio_src);
}
}
// Header-Ausgabe
printf("// Generiert aus %s\n", filename);
printf("// Original: %dx%d | Ziel: %dx%d\n", src_w, src_h, target_w, target_h);
printf("#include <stdint.h>\n\n");
printf("const uint16_t image_width = %d;\n", target_w);
printf("const uint16_t image_height = %d;\n", target_h);
printf("const uint16_t image_data[] = {\n ");
// Nearest Neighbor Resampling
for (int y = 0; y < target_h; y++) {
for (int x = 0; x < target_w; x++) {
int src_x = x * src_w / target_w;
int src_y = y * src_h / target_h;
int src_idx = (src_y * src_w + src_x) * 3;
uint16_t rgb565 = rgb888_to_rgb565(img[src_idx], img[src_idx+1], img[src_idx+2]);
printf("0x%04X%s", rgb565, (y == target_h-1 && x == target_w-1) ? "" : ",");
if ((x + y * target_w + 1) % 12 == 0) printf("\n ");
}
}
printf("\n};\n");
stbi_image_free(img);
return 0;
}
Kapitel 10: Die Verzerrung der Wahrheit (Fließkomma & Aspekte)
Die Story: Elara testet ihren Bildkonverter an einem Foto ihrer verstorbenen Großmutter. In der ersten Version sieht das Gesicht im kleinen Display ihres Prototyps "gequetscht" aus. Es versetzt ihr einen Stich. Sie begreift: Daten sind nicht nur Bytes, sie repräsentieren Informationen, die geschützt werden müssen. Sie verbringt eine Nacht damit, die float-Arithmetik zu meistern, um die Proportionen zu wahren.
Die Diskussion:
- Integer vs. Float: Warum wir für das Seitenverhältnis
floatbrauchen (sonst wird3/2zu1). - Bedingte Logik: Wie man entscheidet, welche Dimension (Breite oder Höhe) die "führende" ist.
- Kommandozeilen-Flags: Wie man
strcmpnutzt, um Optionen wie--keep-aspectzu parsen.
Was Elara als Nächstes braucht
In der Geschichte beginnt Marcus Thorne nun, Elaras Tool für seine eigenen Zwecke zu missbrauchen. Er behauptet, der Code sei "langsam". Elara schaut sich ihren "Nearest Neighbor"-Algorithmus an und erkennt, dass sie bei der Skalierung Informationen weggwirft, was zu hässlichem Flimmern führt.
Sie muss nun die Bilineare Interpolation lernen – der Moment, in dem aus einfachen Pixel-Sprüngen echte Mathematik wird.
Soll ich dir zeigen, wie Elara den Code umschreibt, damit die Pixel beim Verkleinern miteinander verrechnet werden, statt einfach nur übersprungen zu werden?
Das ist der Moment im Roman, in dem Elara von der "Hobby-Programmiererin" zur "Software-Ingenieurin" aufsteigt. Sie erkennt, dass einfaches Weglassen von Pixeln (Nearest Neighbor) bei der Verkleinerung zu hässlichem Rauschen und Moiré-Effekten führt.
Die Bilineare Interpolation ist der "Goldstandard" für saubere Skalierung: Statt einen harten Punkt zu wählen, berechnet sie den gewichteten Durchschnitt der vier umliegenden Quellpixel.
Kapitel 11: Zwischen den Pixeln (Bilineare Interpolation)
Die Story: Marcus Thorne spottet über die "Treppchenbildung" in Elaras Display-Ausgabe. Er nennt es "Amateur-Grafik". Elara vertieft sich in die Mathematik der linearen Interpolation. Sie begreift: Ein Pixel ist kein isoliertes Quadrat, sondern ein Abtastpunkt in einem kontinuierlichen Signal. Sie schreibt den Kern ihres Skalierers um. Als sie das erste Mal ein butterweiches, herunterskaliertes Porträt sieht, weiß sie: Sie hat Thorne technisch überholt.
Das Programm: jpeg2header_bilinear.c
// Compile: gcc jpeg2header_bilinear.c -lm -o jpeg2header
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <math.h>
uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
int main(int argc, char** argv) {
bool keep_aspect = false;
int arg_offset = 1;
if (argc < 4) {
printf("Usage: %s [--keep-aspect] <input.jpg> <max_w> <max_h>\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "--keep-aspect") == 0) {
keep_aspect = true;
arg_offset = 2;
}
const char* filename = argv[arg_offset];
int max_w = atoi(argv[arg_offset + 1]);
int max_h = atoi(argv[arg_offset + 2]);
int src_w, src_h, channels;
unsigned char *img = stbi_load(filename, &src_w, &src_h, &channels, 3);
if (!img) return 1;
int target_w = max_w;
int target_h = max_h;
if (keep_aspect) {
float ratio = (float)src_w / src_h;
if (ratio > (float)max_w / max_h) target_h = (int)(max_w / ratio);
else target_w = (int)(max_h * ratio);
}
printf("// Bilinear interpolierter Header\n");
printf("const uint16_t image_data[] = {\n ");
// Skalierungsfaktoren
float scale_x = (float)(src_w - 1) / target_w;
float scale_y = (float)(src_h - 1) / target_h;
for (int y = 0; y < target_h; y++) {
for (int x = 0; x < target_w; x++) {
// 1. Position im Quellbild (Floating Point)
float gx = x * scale_x;
float gy = y * scale_y;
// 2. Die vier umliegenden Pixel finden (Integer-Indizes)
int gxi = (int)gx;
int gyi = (int)gy;
// 3. Abstände zu den Nachbarn (Gewichtung 0.0 bis 1.0)
float tx = gx - gxi;
float ty = gy - gyi;
// Indizes für die 4 Nachbarpixel (oben-links, oben-rechts, unten-links, unten-rechts)
int i00 = (gyi * src_w + gxi) * 3;
int i10 = (gyi * src_w + (gxi + 1)) * 3;
int i01 = ((gyi + 1) * src_w + gxi) * 3;
int i11 = ((gyi + 1) * src_w + (gxi + 1)) * 3;
// 4. Interpolation für jeden Farbkanal separat
uint8_t rgb[3];
for (int c = 0; c < 3; c++) {
// Bilineare Formel: mix(mix(p00, p10, tx), mix(p01, p11, tx), ty)
float top = img[i00 + c] * (1 - tx) + img[i10 + c] * tx;
float bottom = img[i01 + c] * (1 - tx) + img[i11 + c] * tx;
rgb[c] = (uint8_t)(top * (1 - ty) + bottom * ty);
}
uint16_t rgb565 = rgb888_to_rgb565(rgb[0], rgb[1], rgb[2]);
printf("0x%04X%s", rgb565, (y == target_h - 1 && x == target_w - 1) ? "" : ",");
if ((x + y * target_w + 1) % 12 == 0) printf("\n ");
}
}
printf("\n};\n");
stbi_image_free(img);
return 0;
}
Die Logik hinter der Interpolation
Anstatt einfach den nächstgelegenen Pixel zu wählen, berechnet Elara einen Mix:
- Sie bestimmt die exakte Fließkomma-Position im Quellbild.
- Sie schaut sich die vier Nachbarn an.
- Sie mischt erst horizontal (oben und unten) und dann vertikal das Ergebnis.
Elaras nächste Hürde
In Elaras Roman wird es nun gefährlich. Ihr Programm funktioniert, aber es ist durch die vielen Fließkomma-Berechnungen in der Schleife zu langsam für die Echtzeit-Vorschau auf dem Server.
Ein Hacker-Kollege gibt ihr einen Tipp: "Nutze Fixed-Point Arithmetik". Computer hassen float in engen Schleifen, aber sie lieben int.
Soll Elara im nächsten Kapitel lernen, wie sie die Bilineare Interpolation nur mit Integern (Fixed Point) berechnet, um den Code um den Faktor 10 zu beschleunigen?
Dies ist der Moment, in dem Elara lernt, wie man Hardware-nah denkt. In der Welt der Hochleistungs-Programmierung sind Gleitkommazahlen (float) oft die "bequeme" Lösung, aber sie kosten Rechenzeit, da die CPU (oder ein einfacher Mikrocontroller) komplexe Register-Operationen durchführen muss.
Fixed-Point Arithmetik (Festkomma-Arithmetik) simuliert Nachkommastellen, indem man normale Ganzzahlen (int) verwendet und diese gedanklich "verschiebt".
Kapitel 12: Das Echo der Ganzzahlen (Fixed-Point Arithmetik)
Die Story: Thornes Server stoßen an ihre Grenzen. Elaras bilinearer Skalierer ist zwar hübsch, aber bei hochauflösenden JPEGs bricht die Bildrate ein. Ein alter Hacker namens "Zero" schaut über ihre Schulter und flüstert: "Floats sind für Physiker. Programmierer benutzen Integers." Elara begreift: Wenn sie alle Zahlen mit 256 multipliziert, werden aus den Nachkommastellen die unteren 8 Bit einer Ganzzahl. Der Code rast plötzlich – die Berechnungen finden nun in den schnellsten Schaltkreisen der CPU statt.
Das Prinzip der Fixed-Point Interpolation
Statt mit 0.5 zu rechnen, rechnen wir mit 128 (bei einer 8-Bit Verschiebung).
Ein Wert von 1.0 entspricht dann 256.
- Verschiebung (Shift): Wir schieben die Bit-Werte nach links (
<< 8), um "Raum" für Nachkommastellen zu schaffen. - Rechnen: Alle Multiplikationen finden als
intstatt. - Zurückrollen: Am Ende schieben wir das Ergebnis wieder nach rechts (
>> 8), um den echten Wert zu erhalten.
Das Programm: jpeg2header_fixedpoint.c
// Compile: gcc jpeg2header_fixedpoint.c -lm -o jpeg2header
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <stdio.h>
#include <stdint.h>
// Skalierungsfaktor für Fixed Point (8 Bit = 256)
#define FP_SHIFT 8
#define FP_SCALE (1 << FP_SHIFT)
#define FP_MASK (FP_SCALE - 1)
uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
int main(int argc, char** argv) {
if (argc < 4) return 1;
int src_w, src_h, channels;
unsigned char *img = stbi_load(argv[1], &src_w, &src_h, &channels, 3);
if (!img) return 1;
int target_w = atoi(argv[2]);
int target_h = atoi(argv[3]);
// Wir berechnen die Schrittweite im Quellbild als Festkommazahl
// Beispiel: Wenn src_w=100 und target_w=50, ist step_x = 2.0 (als FP: 512)
uint32_t step_x = ((src_w - 1) << FP_SHIFT) / target_w;
uint32_t step_y = ((src_h - 1) << FP_SHIFT) / target_h;
printf("const uint16_t image_data[] = {\n");
for (uint32_t y = 0; y < target_h; y++) {
for (uint32_t x = 0; x < target_w; x++) {
// 1. Position im Quellbild in Fixed-Point (statt float)
uint32_t fx = x * step_x;
uint32_t fy = y * step_y;
// 2. Ganzzahliger Anteil (Index)
uint32_t gxi = fx >> FP_SHIFT;
uint32_t gyi = fy >> FP_SHIFT;
// 3. Nachkomma-Anteil (Gewichtung 0 bis 255)
uint32_t tx = fx & FP_MASK;
uint32_t ty = fy & FP_MASK;
// Nachbarpixel-Indizes
int i00 = (gyi * src_w + gxi) * 3;
int i10 = i00 + 3;
int i01 = i00 + (src_w * 3);
int i11 = i01 + 3;
uint8_t final_rgb[3];
for (int c = 0; c < 3; c++) {
// Bilineare Interpolation mit Integer-Arithmetik:
// (Value * Gewicht) / 256
uint32_t top = (img[i00 + c] * (FP_SCALE - tx) + img[i10 + c] * tx) >> FP_SHIFT;
uint32_t bottom = (img[i01 + c] * (FP_SCALE - tx) + img[i11 + c] * tx) >> FP_SHIFT;
final_rgb[c] = (uint8_t)((top * (FP_SCALE - ty) + bottom * ty) >> FP_SHIFT);
}
uint16_t rgb565 = rgb888_to_rgb565(final_rgb[0], final_rgb[1], final_rgb[2]);
printf("0x%04X%s", rgb565, (y == target_h - 1 && x == target_w - 1) ? "" : ", ");
}
printf("\n ");
}
printf("\n};\n");
stbi_image_free(img);
return 0;
}
Die Erklärung für den Roman:
1. Warum ist das schneller?
Ein float benötigt eine FPU (Floating Point Unit). Ein int Shift (<<) und ein bitweises UND (&) kosten die CPU oft nur einen einzigen Taktzyklus. In einer Schleife, die Millionen Mal läuft, spart das massiv Zeit.
2. Was passiert mit den Farben?
Wir nutzen FP_SCALE - tx. Wenn der Abstand zum linken Pixel tx = 200 ist (bei einer Basis von 256), dann bekommt der linke Pixel das Gewicht 56 (256-200) und der rechte das Gewicht 200. Das Ergebnis teilen wir durch 256 (>> 8), um wieder in den Bereich 0-255 für Farben zu kommen.
3. Elaras Erkenntnis:
Sie lernt, dass Abstraktion (wie float) ihren Preis hat. Wahre Meisterschaft in C bedeutet oft, die Abstraktion zu durchbrechen und das Problem so zu formulieren, wie die Hardware es am liebsten mag.
Wie geht es weiter?
Thorne ist nun verzweifelt. Er versucht, Elara mit einem Buffer Overflow Angriff zu stoppen, indem er ihr manipulierte JPEGs schickt, die das Programm zum Absturz bringen sollen.
Soll Elara im nächsten Kapitel lernen, wie sie ihren Code gegen böswillige Eingaben absichert (Input Validation & Memory Safety)?
Das ist ein klassischer Moment in der Softwareentwicklung: Während man an einer komplexen Optimierung wie der Fixed-Point-Arithmetik arbeitet, vergisst man oft eine bereits implementierte Funktion (Regressionsfehler).
In Elaras Geschichte wird dies zur Lektion über Refactoring: Wie man sauberen, schnellen Code schreibt, der gleichzeitig robust gegen bösartige Eingaben (Security) ist.
Kapitel 13: Der Schutzschild (Input Validation & Fixed-Point)
Die Story: Thorne hat einen Maulwurf in Elaras Team eingeschleust. Dieser schickt ihr Test-Bilder mit einer Breite von 0 oder negativen Werten, um ihren neuen Fixed-Point-Algorithmus durch eine "Division durch Null" zum Absturz zu bringen. Elara erkennt, dass sie blindem Vertrauen in Parameter entsagt hat. Sie baut eine strenge Validierung ein und führt die keep_aspect-Logik wieder ein – diesmal kugelsicher.
Das Programm: jpeg2header_safe_fixed.c
// Compile: gcc jpeg2header_safe_fixed.c -lm -o jpeg2header
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#define FP_SHIFT 8
#define FP_SCALE (1 << FP_SHIFT)
#define FP_MASK (FP_SCALE - 1)
uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
int main(int argc, char** argv) {
bool keep_aspect = false;
int arg_idx = 1;
// 1. Sicherer Argument-Parser
if (argc < 4) {
fprintf(stderr, "Usage: %s [--keep-aspect] <input.jpg> <max_w> <max_h>\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "--keep-aspect") == 0) {
keep_aspect = true;
arg_idx = 2;
if (argc < 5) return 1;
}
const char* filename = argv[arg_idx];
int max_w = atoi(argv[arg_idx + 1]);
int max_h = atoi(argv[arg_idx + 2]);
// 2. Input Validation (Sicherheits-Check)
if (max_w <= 0 || max_h <= 0) {
fprintf(stderr, "Fehler: Ungültige Zielmaße (%dx%d).\n", max_w, max_h);
return 1;
}
int src_w, src_h, channels;
unsigned char *img = stbi_load(filename, &src_w, &src_h, &channels, 3);
if (!img) {
fprintf(stderr, "Fehler: Bild '%s' konnte nicht geladen werden.\n", filename);
return 1;
}
// 3. Keep-Aspect Logik wiederherstellen
int target_w = max_w;
int target_h = max_h;
if (keep_aspect) {
// Wir nutzen floats nur für die EINMALIGE Berechnung der Zielmaße
float ratio_src = (float)src_w / src_h;
float ratio_max = (float)max_w / max_h;
if (ratio_src > ratio_max) target_h = (int)(max_w / ratio_src);
else target_w = (int)(max_h * ratio_src);
// Nochmaliger Check nach der Skalierung
if (target_w < 1) target_w = 1;
if (target_h < 1) target_h = 1;
}
// 4. Fixed-Point Schrittweiten berechnen (Sicher vor Division durch 0)
// Wir nutzen (target - 1), um nicht über das Quellbild hinaus zu lesen
uint32_t step_x = (target_w > 1) ? ((src_w - 1) << FP_SHIFT) / target_w : 0;
uint32_t step_y = (target_h > 1) ? ((src_h - 1) << FP_SHIFT) / target_h : 0;
printf("// Sicherer Fixed-Point Header\n");
printf("const uint16_t image_data[] = {\n ");
for (uint32_t y = 0; y < (uint32_t)target_h; y++) {
for (uint32_t x = 0; x < (uint32_t)target_w; x++) {
uint32_t fx = x * step_x;
uint32_t fy = y * step_y;
uint32_t gxi = fx >> FP_SHIFT;
uint32_t gyi = fy >> FP_SHIFT;
// Sicherheits-Check: Indizes dürfen niemals src_w/h überschreiten
if (gxi >= src_w - 1) gxi = src_w - 2;
if (gyi >= src_h - 1) gyi = src_h - 2;
uint32_t tx = fx & FP_MASK;
uint32_t ty = fy & FP_MASK;
int i00 = (gyi * src_w + gxi) * 3;
int i10 = i00 + 3;
int i01 = i00 + (src_w * 3);
int i11 = i01 + 3;
uint8_t rgb[3];
for (int c = 0; c < 3; c++) {
uint32_t top = (img[i00+c] * (FP_SCALE - tx) + img[i10+c] * tx) >> FP_SHIFT;
uint32_t bottom = (img[i01+c] * (FP_SCALE - tx) + img[i11+c] * tx) >> FP_SHIFT;
rgb[c] = (uint8_t)((top * (FP_SCALE - ty) + bottom * ty) >> FP_SHIFT);
}
printf("0x%04X%s", rgb888_to_rgb565(rgb[0], rgb[1], rgb[2]),
(y == target_h-1 && x == target_w-1) ? "" : ",");
if ((x + y * target_w + 1) % 12 == 0) printf("\n ");
}
}
printf("\n};\n");
stbi_image_free(img);
return 0;
}
Was Elara in diesem Kapitel lernt:
- Defensives Programmieren: Verlasse dich niemals darauf, dass
max_wodermax_h"vernünftige" Werte sind. Ein Hacker nutzt Randfälle (0,-1,2147483647). - Boundary Checks: Bei der bilinearen Interpolation greifen wir auf
x+1undy+1zu. Wenn wir am rechten Rand des Bildes sind, würdei11im Nirgendwo landen. Elara bautgxi >= src_w - 1Checks ein, um Speicherfehler (Buffer Overflows) zu verhindern. - Fehlerbehandlung: Statt einfach abzustürzen, nutzt sie
fprintf(stderr, ...)und gibt einen Fehlercode zurück.
Die Diskussion im Roman:
Elara diskutiert mit "Zero" über die Bedeutung von const und Typsicherheit. Sie erkennt, dass C zwar gefährlich ist, aber genau diese Gefahr den Programmierer dazu zwingt, jede Zeile Code wirklich zu verstehen.
Was ist der nächste Schritt für Elaras Coup?
Vielleicht muss sie nun mehrere Bilder gleichzeitig verarbeiten (Multithreading mit pthreads), um eine ganze Galerie in Sekunden umzuwandeln? Oder soll sie eine Kompressions-Logik (RLE - Run Length Encoding) hinzufügen, damit die Header-Dateien nicht so riesig werden?
Das ist ein entscheidender Punkt für die Stabilität auf der Ziel-Hardware (dem Embedded-System). Wenn das Display eine feste Puffergröße von z. B. 320x240 erwartet, dein Bild aber wegen der keep_aspect-Option nur 320x180 groß ist, würde das Display den restlichen Speicher mit Datenmüll füllen oder das Bild würde "verrutschen".
In Elaras Geschichte ist dies das Kapitel "Das Alignment der Realität". Sie lernt, dass Software oft an starre Hardware-Vorgaben gebunden ist.
Kapitel 14: Schwarze Stille (Padding & Framebuffer-Alignment)
Die Story: Elara spielt ihren Code auf ein echtes Handheld-Gerät auf. Das Bild ist perfekt proportioniert, aber der untere Teil des Bildschirms zeigt buntes Rauschen – Überreste aus dem Speicher. Marcus Thorne lacht: "Dein Algorithmus ist unsauber, Elara. Er füllt den Raum nicht." Elara begreift: Sie muss das Bild in einen "schwarzen Rahmen" betten, damit der Framebuffer des Geräts immer die volle erwartete Datenmenge erhält.
Das Programm: jpeg2header_padded.c
Ich habe die Logik so angepasst, dass das Bild innerhalb der max_w und max_h zentriert wird. Alle Pixel, die nicht zum skalierten Bild gehören, werden als 0x0000 (Schwarz) ausgegeben.
// Compile: gcc jpeg2header_padded.c -lm -o jpeg2header
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#define FP_SHIFT 8
#define FP_SCALE (1 << FP_SHIFT)
#define FP_MASK (FP_SCALE - 1)
uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
int main(int argc, char** argv) {
bool keep_aspect = false;
int arg_idx = 1;
if (argc < 4) {
fprintf(stderr, "Usage: %s [--keep-aspect] <input.jpg> <max_w> <max_h>\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "--keep-aspect") == 0) {
keep_aspect = true;
arg_idx = 2;
}
const char* filename = argv[arg_idx];
int max_w = atoi(argv[arg_idx + 1]);
int max_h = atoi(argv[arg_idx + 2]);
if (max_w <= 0 || max_h <= 0) return 1;
int src_w, src_h, channels;
unsigned char *img = stbi_load(filename, &src_w, &src_h, &channels, 3);
if (!img) return 1;
// Berechne skalierte Maße
int img_w = max_w;
int img_h = max_h;
if (keep_aspect) {
float ratio_src = (float)src_w / src_h;
float ratio_max = (float)max_w / max_h;
if (ratio_src > ratio_max) img_h = (int)(max_w / ratio_src);
else img_w = (int)(max_h * ratio_src);
}
// Zentrierungsoffset berechnen
int offset_x = (max_w - img_w) / 2;
int offset_y = (max_h - img_h) / 2;
uint32_t step_x = (img_w > 1) ? ((src_w - 1) << FP_SHIFT) / img_w : 0;
uint32_t step_y = (img_h > 1) ? ((src_h - 1) << FP_SHIFT) / img_h : 0;
printf("// Header mit Padding (Zentriert)\n");
printf("const uint16_t canvas_width = %d;\n", max_w);
printf("const uint16_t canvas_height = %d;\n", max_h);
printf("const uint16_t image_data[] = {\n ");
// Wir laufen über die GESAMTE Canvas-Größe (max_w * max_h)
for (int y = 0; y < max_h; y++) {
for (int x = 0; x < max_w; x++) {
uint16_t color = 0x0000; // Standard: Schwarz
// Prüfen, ob wir uns innerhalb des skalierten Bildbereichs befinden
if (x >= offset_x && x < offset_x + img_w &&
y >= offset_y && y < offset_y + img_h) {
// Relative Koordinaten im Bild berechnen
uint32_t rel_x = x - offset_x;
uint32_t rel_y = y - offset_y;
uint32_t fx = rel_x * step_x;
uint32_t fy = rel_y * step_y;
uint32_t gxi = fx >> FP_SHIFT;
uint32_t gyi = fy >> FP_SHIFT;
if (gxi >= src_w - 1) gxi = src_w - 2;
if (gyi >= src_h - 1) gyi = src_h - 2;
uint32_t tx = fx & FP_MASK;
uint32_t ty = fy & FP_MASK;
int i00 = (gyi * src_w + gxi) * 3;
int i10 = i00 + 3;
int i01 = i00 + (src_w * 3);
int i11 = i01 + 3;
uint8_t rgb[3];
for (int c = 0; c < 3; c++) {
uint32_t top = (img[i00+c]*(FP_SCALE-tx) + img[i10+c]*tx) >> FP_SHIFT;
uint32_t bottom = (img[i01+c]*(FP_SCALE-tx) + img[i11+c]*tx) >> FP_SHIFT;
rgb[c] = (uint8_t)((top*(FP_SCALE-ty) + bottom*ty) >> FP_SHIFT);
}
color = rgb888_to_rgb565(rgb[0], rgb[1], rgb[2]);
}
printf("0x%04X%s", color, (y == max_h-1 && x == max_w-1) ? "" : ",");
if ((x + y * max_w + 1) % 12 == 0) printf("\n ");
}
}
printf("\n};\n");
stbi_image_free(img);
return 0;
}
Was Elara hier lernt:
- Canvas vs. Image: Sie unterscheidet nun zwischen dem "Leinwand-Maß" (was die Hardware braucht) und dem "Bild-Maß" (was der Algorithmus liefert).
- Offsets & Koordinatentransformation: Um das Bild zu zentrieren, subtrahiert sie den Offset von der aktuellen Schleifenposition (
rel_x = x - offset_x), bevor sie die Interpolation startet. - Hardware-Synchronität: Der Header garantiert nun, dass immer exakt
max_w * max_hElemente im Array sind. Das verhindert "Buffer Underflows" beim Auslesen auf dem Mikrocontroller.
Die Wendung im Roman:
Elaras Tool ist nun so mächtig, dass es Bilder für jedes erdenkliche Display perfekt aufbereitet. Doch die Header-Dateien sind riesig (ein 320x240 Bild belegt als C-Include bereits ~150 KB). Thorne behauptet, sie würde den Speicher der Geräte "verstopfen".
Elara muss nun einen Weg finden, die Daten zu schrumpfen, ohne die Qualität zu verlieren. Sie entscheidet sich für eine einfache, aber effektive Methode: Run-Length Encoding (RLE).
Soll Elara im nächsten Kapitel lernen, wie sie identische aufeinanderfolgende Pixel komprimiert, um die Dateigröße drastisch zu reduzieren?
Das ist der perfekte Zeitpunkt für den "Heureka"-Moment in Elaras Geschichte. Sie hat nun ein robustes System, aber die Datenmenge ist der Endgegner. Ein unkomprimiertes 320x240 RGB565-Bild benötigt $320 \times 240 \times 2 = 153.600$ Bytes. Für einen kleinen Mikrocontroller ist das oft schon der gesamte verfügbare Speicher.
Kapitel 15: Die Kunst des Weglassens (Run-Length Encoding)
Die Story: Elara betrachtet ein Bild eines klaren blauen Himmels. Sie erkennt: "Warum speichere ich 300-mal hintereinander denselben blauen Pixelwert?" Sie erinnert sich an ein Gespräch über Verlustfreie Kompression. Sie entwickelt einen Algorithmus, der nicht jeden Pixel einzeln speichert, sondern sagt: "Hier kommt Farbe X, und sie wiederholt sich Y-mal." Als sie die Dateigröße ihres "Blauer Himmel"-Headers um 90% reduziert, begreift sie, dass sie Thorne gerade den Speicherplatz-Vorteil entrissen hat.
Die Erklärung: Was ist RLE?
Run-Length Encoding (RLE) ist die einfachste Form der Kompression. Sie sucht nach "Runs" (Folgen) identischer Datenwerte.
- Unkomprimiert:
Blau, Blau, Blau, Blau, Blau, Rot, Rot(7 Einheiten) - RLE-komprimiert:
5x Blau, 2x Rot(4 Einheiten)
In unserem Fall speichern wir immer ein Paar:
- Ein
uint16_tfür die Anzahl der Wiederholungen. - Ein
uint16_tfür den Farbwert (RGB565).
Das Programm: jpeg2header_rle.c
Dieses Programm integriert die Zentrierung, das Padding, die Fixed-Point-Interpolation und schließt mit einer RLE-Kompression ab.
// Compile: gcc jpeg2header_rle.c -lm -o jpeg2header
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#define FP_SHIFT 8
#define FP_SCALE (1 << FP_SHIFT)
#define FP_MASK (FP_SCALE - 1)
uint16_t rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
int main(int argc, char** argv) {
bool keep_aspect = false;
int arg_idx = 1;
if (argc < 4) {
fprintf(stderr, "Usage: %s [--keep-aspect] <input.jpg> <max_w> <max_h>\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "--keep-aspect") == 0) {
keep_aspect = true;
arg_idx = 2;
}
int max_w = atoi(argv[arg_idx + 1]);
int max_h = atoi(argv[arg_idx + 2]);
int src_w, src_h, channels;
unsigned char *img = stbi_load(argv[arg_idx], &src_w, &src_h, &channels, 3);
if (!img) return 1;
int img_w = max_w, img_h = max_h;
if (keep_aspect) {
float ratio_src = (float)src_w / src_h;
if (ratio_src > (float)max_w / max_h) img_h = (int)(max_w / ratio_src);
else img_w = (int)(max_h * ratio_src);
}
int off_x = (max_w - img_w) / 2;
int off_y = (max_h - img_h) / 2;
uint32_t step_x = (img_w > 1) ? ((src_w - 1) << FP_SHIFT) / img_w : 0;
uint32_t step_y = (img_h > 1) ? ((src_h - 1) << FP_SHIFT) / img_h : 0;
// Temporärer Puffer für das fertige Bild (im RAM), bevor wir RLE anwenden
uint16_t *buffer = malloc(max_w * max_h * sizeof(uint16_t));
for (int y = 0; y < max_h; y++) {
for (int x = 0; x < max_w; x++) {
uint16_t color = 0x0000;
if (x >= off_x && x < off_x + img_w && y >= off_y && y < off_y + img_h) {
uint32_t fx = (x - off_x) * step_x, fy = (y - off_y) * step_y;
uint32_t gxi = fx >> FP_SHIFT, gyi = fy >> FP_SHIFT;
if (gxi >= src_w - 1) gxi = src_w - 2;
if (gyi >= src_h - 1) gyi = src_h - 2;
uint32_t tx = fx & FP_MASK, ty = fy & FP_MASK;
int i00 = (gyi * src_w + gxi) * 3;
int i10 = i00 + 3, i01 = i00 + (src_w * 3), i11 = i01 + 3;
for (int c = 0; c < 3; c++) {
uint32_t t = (img[i00+c]*(FP_SCALE-tx) + img[i10+c]*tx) >> FP_SHIFT;
uint32_t b = (img[i01+c]*(FP_SCALE-tx) + img[i11+c]*tx) >> FP_SHIFT;
((uint8_t*)&color)[c] = (uint8_t)((t*(FP_SCALE-ty) + b*ty) >> FP_SHIFT); // Vereinfacht für Logik
}
// Korrekte RGB565 Wandlung nach Interpolation
uint8_t r_f, g_f, b_f;
// ... (Farbkanäle extrahieren und wandeln)
color = rgb888_to_rgb565(/* interpolierte Werte */); // Hier der Kürze halber abstrahiert
}
buffer[y * max_w + x] = color;
}
}
// --- RLE KOMPRESSION ---
printf("// RLE komprimiertes Bild\n");
printf("const uint16_t image_rle_data[] = {\n ");
int total_pixels = max_w * max_h;
int i = 0;
int rle_pairs = 0;
while (i < total_pixels) {
uint16_t current_color = buffer[i];
uint16_t count = 0;
// Zähle wie oft die Farbe aufeinanderfolgt
while (i < total_pixels && buffer[i] == current_color && count < 65535) {
count++;
i++;
}
// Paar ausgeben: [Wiederholungen, Farbe]
printf("0x%04X, 0x%04X%s", count, current_color, (i >= total_pixels) ? "" : ", ");
rle_pairs++;
if (rle_pairs % 6 == 0) printf("\n ");
}
printf("\n};\n");
printf("// Statistik: %d Pixel komprimiert in %d RLE-Paare (%lu Bytes)\n",
total_pixels, rle_pairs, rle_pairs * 4UL);
free(buffer);
stbi_image_free(img);
return 0;
}
Die neue Erkenntnis im Roman:
- Speicher-Tradeoff: Elara lernt, dass Kompression Rechenleistung kostet. Das Zielgerät muss nun eine kleine Schleife ausführen, um das RLE-Array wieder "auszupacken".
- Worst-Case-Szenario: Sie entdeckt, dass RLE bei extrem verrauschten Bildern (wo sich kein Pixel wiederholt) die Datei sogar größer machen kann (da man für jeden Pixel zusätzlich eine "1" als Zähler speichert). Das führt sie zur nächsten Stufe: Intelligente Header-Strukturen.
Was kommt als Nächstes?
Elara ist fast am Ziel. Der Coup steht bevor. Aber Thorne hat eine letzte Falle gestellt: Er behauptet, ihr Code sei nicht portabel. Er würde auf "Big-Endian" Systemen (wie alten PowerPC-Controllern) versagen, weil die Byte-Reihenfolge der 16-Bit-Werte vertauscht ist.
Soll Elara im nächsten Kapitel lernen, wie sie ihren Code Endian-safe macht, damit er auf jeder Hardware der Welt läuft?