diff --git a/apps/dvxdemo/dvxdemo.c b/apps/dvxdemo/dvxdemo.c index 9bd293b..6d2e687 100644 --- a/apps/dvxdemo/dvxdemo.c +++ b/apps/dvxdemo/dvxdemo.c @@ -583,18 +583,39 @@ static void setupControlsWindow(void) { WidgetT *tb = wgtToolbar(page5tb); - WidgetT *btnNew = wgtButton(tb, "&New"); + char iconPath[272]; + snprintf(iconPath, sizeof(iconPath), "%s/new.bmp", sDxeCtx->appDir); + WidgetT *btnNew = wgtImageButtonFromFile(tb, iconPath); + + if (!btnNew) { + btnNew = wgtButton(tb, "&New"); + } + btnNew->onClick = onToolbarClick; - WidgetT *btnOpen = wgtButton(tb, "&Open"); + + snprintf(iconPath, sizeof(iconPath), "%s/open.bmp", sDxeCtx->appDir); + WidgetT *btnOpen = wgtImageButtonFromFile(tb, iconPath); + + if (!btnOpen) { + btnOpen = wgtButton(tb, "&Open"); + } + btnOpen->onClick = onToolbarClick; - WidgetT *btnSave = wgtButton(tb, "&Save"); + + snprintf(iconPath, sizeof(iconPath), "%s/save.bmp", sDxeCtx->appDir); + WidgetT *btnSave = wgtImageButtonFromFile(tb, iconPath); + + if (!btnSave) { + btnSave = wgtButton(tb, "&Save"); + } + btnSave->onClick = onToolbarClick; wgtVSeparator(tb); WidgetT *btnHelp = wgtButton(tb, "&Help"); btnHelp->onClick = onToolbarClick; - wgtLabel(page5tb, "Toolbar with text buttons and VSeparator."); + wgtLabel(page5tb, "Toolbar with image buttons, text fallback, and VSeparator."); // --- Tab 6: Media (Image from file) --- WidgetT *page6m = wgtTabPage(tabs, "&Media"); diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index da299f2..0890f5f 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -40,6 +40,7 @@ #include #include +#include "thirdparty/stb_image.h" #include "thirdparty/stb_image_write.h" // Double-click timing uses CLOCKS_PER_SEC so it's portable between DJGPP @@ -1447,6 +1448,15 @@ void dvxFreeAccelTable(AccelTableT *table) { } +// ============================================================ +// dvxFreeImage +// ============================================================ + +void dvxFreeImage(uint8_t *data) { + free(data); +} + + // ============================================================ // dvxGetBlitOps // ============================================================ @@ -1549,6 +1559,75 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ } +// ============================================================ +// dvxLoadImage +// ============================================================ +// +// Public image loading API. Loads any image file supported by stb_image +// (BMP, PNG, JPEG, GIF, etc.) and converts the RGB pixels to the +// display's native pixel format for direct use with rectCopy, wgtImage, +// wgtImageButton, or any other pixel-data consumer. The caller owns the +// returned buffer and must free it with dvxFreeImage(). + +uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch) { + if (!ctx || !path) { + return NULL; + } + + const DisplayT *d = &ctx->display; + + int imgW; + int imgH; + int channels; + uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); + + if (!rgb) { + return NULL; + } + + int32_t bpp = d->format.bytesPerPixel; + int32_t pitch = imgW * bpp; + uint8_t *buf = (uint8_t *)malloc(pitch * imgH); + + if (!buf) { + stbi_image_free(rgb); + return NULL; + } + + for (int32_t y = 0; y < imgH; y++) { + for (int32_t x = 0; x < imgW; x++) { + const uint8_t *src = rgb + (y * imgW + x) * 3; + uint32_t color = packColor(d, src[0], src[1], src[2]); + uint8_t *dst = buf + y * pitch + x * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + } + + stbi_image_free(rgb); + + if (outW) { + *outW = imgW; + } + + if (outH) { + *outH = imgH; + } + + if (outPitch) { + *outPitch = pitch; + } + + return buf; +} + + // ============================================================ // dvxInvalidateRect // ============================================================ @@ -1692,6 +1771,35 @@ bool dvxUpdate(AppContextT *ctx) { } +// ============================================================ +// dvxSaveImage +// ============================================================ +// +// Save native-format pixel data to a PNG file. Converts from the +// display's native pixel format to RGB, then encodes as PNG via +// stb_image_write. This is the general-purpose image save function; +// dvxScreenshot and dvxWindowScreenshot are convenience wrappers +// around it for common use cases. + +int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path) { + if (!ctx || !data || !path || w <= 0 || h <= 0) { + return -1; + } + + uint8_t *rgb = bufferToRgb(&ctx->display, data, w, h, pitch); + + if (!rgb) { + return -1; + } + + int32_t result = stbi_write_png(path, w, h, 3, rgb, w * 3) ? 0 : -1; + + free(rgb); + + return result; +} + + // ============================================================ // dvxScreenshot // ============================================================ diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index 9c8d402..4d3cbba 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -199,6 +199,20 @@ void dvxTileWindowsH(AppContextT *ctx); // Vertical tile: stacked, full width, equal height. void dvxTileWindowsV(AppContextT *ctx); +// Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's +// native pixel format. Returns the pixel buffer on success (caller must +// free with dvxFreeImage), or NULL on failure. Output params receive the +// image dimensions and row pitch in bytes. +uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch); + +// Free a pixel buffer returned by dvxLoadImage. +void dvxFreeImage(uint8_t *data); + +// Save native-format pixel data to a PNG file. The pixel data must be +// in the display's native format (as returned by dvxLoadImage or +// captured from a content buffer). Returns 0 on success, -1 on failure. +int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path); + // Copy text to the process-wide clipboard buffer. The clipboard is a // simple static buffer (not inter-process) — adequate for copy/paste // within the DVX environment on single-tasking DOS. diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index b2b0fee..d60519a 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -774,6 +774,10 @@ int32_t wgtSplitterGetPos(const WidgetT *w); // Takes ownership of the data buffer (freed on destroy). WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch); +// Load an image button from a file (BMP, PNG, JPEG, GIF). +// Returns NULL on load failure; falls through gracefully. +WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path); + // Replace the image data. Takes ownership of the new buffer. void wgtImageButtonSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch); diff --git a/dvx/widgets/widgetCanvas.c b/dvx/widgets/widgetCanvas.c index 9e04985..ce4cac9 100644 --- a/dvx/widgets/widgetCanvas.c +++ b/dvx/widgets/widgetCanvas.c @@ -22,8 +22,6 @@ // widget-space coordinates to canvas-space by subtracting the border offset. #include "widgetInternal.h" -#include "../thirdparty/stb_image.h" -#include "../thirdparty/stb_image_write.h" #define CANVAS_BORDER 2 @@ -34,7 +32,6 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy); static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1); -static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b); // ============================================================ @@ -197,37 +194,6 @@ static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32 } -// ============================================================ -// canvasUnpackColor -// ============================================================ -// -// Reverse of packColor — extract RGB from a display-format pixel. -// Only used during PNG save (wgtCanvasSave) to convert the canvas's -// native-format pixels back to RGB for stb_image_write. The bit-shift -// approach works for 15/16/24/32-bit modes; 8-bit paletted mode uses -// a direct palette table lookup instead. - -static void canvasUnpackColor(const DisplayT *d, uint32_t pixel, uint8_t *r, uint8_t *g, uint8_t *b) { - if (d->format.bitsPerPixel == 8) { - // 8-bit paletted — look up the palette entry - int32_t idx = pixel & 0xFF; - *r = d->palette[idx * 3 + 0]; - *g = d->palette[idx * 3 + 1]; - *b = d->palette[idx * 3 + 2]; - return; - } - - uint32_t rv = (pixel >> d->format.redShift) & ((1u << d->format.redBits) - 1); - uint32_t gv = (pixel >> d->format.greenShift) & ((1u << d->format.greenBits) - 1); - uint32_t bv = (pixel >> d->format.blueShift) & ((1u << d->format.blueBits) - 1); - - // Scale back up to 8 bits - *r = (uint8_t)(rv << (8 - d->format.redBits)); - *g = (uint8_t)(gv << (8 - d->format.greenBits)); - *b = (uint8_t)(bv << (8 - d->format.blueBits)); -} - - // ============================================================ // wgtCanvas // ============================================================ @@ -559,67 +525,35 @@ uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) { // ============================================================ // Load an image file into the canvas, replacing the current content. -// Uses stb_image for decoding (supports BMP, PNG, JPEG, GIF, etc.). -// The loaded RGB pixels are converted to the display's native pixel format -// during load so that subsequent repaints are just a memcpy. The old buffer -// is freed and replaced with the new one — canvas dimensions change to match -// the loaded image. +// Delegates to dvxLoadImage for format decoding and pixel conversion. +// The old buffer is freed and replaced with the new one — canvas +// dimensions change to match the loaded image. int32_t wgtCanvasLoad(WidgetT *w, const char *path) { if (!w || w->type != WidgetCanvasE || !path) { return -1; } - // Find the AppContextT to get display format - WidgetT *root = w; - - while (root->parent) { - root = root->parent; - } - - AppContextT *ctx = (AppContextT *)root->userData; + AppContextT *ctx = wgtGetContext(w); if (!ctx) { return -1; } - const DisplayT *d = &ctx->display; - - int imgW; - int imgH; - int channels; - uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); - - if (!rgb) { - return -1; - } - - int32_t bpp = d->format.bytesPerPixel; - int32_t pitch = imgW * bpp; - uint8_t *data = (uint8_t *)malloc(pitch * imgH); + int32_t imgW; + int32_t imgH; + int32_t pitch; + uint8_t *data = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); if (!data) { - stbi_image_free(rgb); return -1; } - for (int32_t y = 0; y < imgH; y++) { - for (int32_t x = 0; x < imgW; x++) { - const uint8_t *src = rgb + (y * imgW + x) * 3; - uint32_t color = packColor(d, src[0], src[1], src[2]); - uint8_t *dst = data + y * pitch + x * bpp; - - canvasPutPixel(dst, color, bpp); - } - } - - stbi_image_free(rgb); - free(w->as.canvas.data); w->as.canvas.data = data; w->as.canvas.canvasW = imgW; w->as.canvas.canvasH = imgH; w->as.canvas.canvasPitch = pitch; - w->as.canvas.canvasBpp = bpp; + w->as.canvas.canvasBpp = ctx->display.format.bytesPerPixel; return 0; } @@ -629,54 +563,20 @@ int32_t wgtCanvasLoad(WidgetT *w, const char *path) { // wgtCanvasSave // ============================================================ -// Save the canvas content to a PNG file. Since the canvas stores pixels in -// the display's native format (which varies per video mode), we must convert -// back to RGB before writing. This is the inverse of the load conversion. +// Save the canvas content to a PNG file. Delegates to dvxSaveImage +// which handles native-to-RGB conversion and PNG encoding. int32_t wgtCanvasSave(WidgetT *w, const char *path) { if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) { return -1; } - // Find the AppContextT to get display format - WidgetT *root = w; - - while (root->parent) { - root = root->parent; - } - - AppContextT *ctx = (AppContextT *)root->userData; + AppContextT *ctx = wgtGetContext(w); if (!ctx) { return -1; } - const DisplayT *d = &ctx->display; - int32_t cw = w->as.canvas.canvasW; - int32_t ch = w->as.canvas.canvasH; - int32_t bpp = d->format.bytesPerPixel; - int32_t pitch = w->as.canvas.canvasPitch; - - // Convert display format back to RGB - uint8_t *rgb = (uint8_t *)malloc(cw * ch * 3); - - if (!rgb) { - return -1; - } - - for (int32_t y = 0; y < ch; y++) { - for (int32_t x = 0; x < cw; x++) { - const uint8_t *src = w->as.canvas.data + y * pitch + x * bpp; - uint32_t pixel = canvasGetPixel(src, bpp); - - uint8_t *dst = rgb + (y * cw + x) * 3; - canvasUnpackColor(d, pixel, &dst[0], &dst[1], &dst[2]); - } - } - - int32_t result = stbi_write_png(path, cw, ch, 3, rgb, cw * 3); - free(rgb); - - return result ? 0 : -1; + return dvxSaveImage(ctx, w->as.canvas.data, w->as.canvas.canvasW, w->as.canvas.canvasH, w->as.canvas.canvasPitch, path); } diff --git a/dvx/widgets/widgetImage.c b/dvx/widgets/widgetImage.c index 94c1f1e..f034856 100644 --- a/dvx/widgets/widgetImage.c +++ b/dvx/widgets/widgetImage.c @@ -17,7 +17,6 @@ // centering if the widget is larger than the image. #include "widgetInternal.h" -#include "../thirdparty/stb_image.h" // ============================================================ @@ -47,67 +46,28 @@ WidgetT *wgtImage(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t // ============================================================ // // Load an image from a file (BMP, PNG, JPEG, GIF), convert to -// display pixel format, and create an image widget. - -// Load an image from disk and create an Image widget. Uses stb_image for -// decoding (any format it supports: PNG, BMP, JPEG, GIF, etc.). The RGB -// pixels are converted to the display's native pixel format during load -// using packColor, then the raw RGB data is freed. The per-pixel bpp switch -// is duplicated here rather than using canvasPutPixel because this function -// is in a different compilation unit and inlining across units isn't guaranteed -// on DJGPP. +// display pixel format, and create an image widget. Delegates to +// dvxLoadImage for format decoding and pixel conversion. WidgetT *wgtImageFromFile(WidgetT *parent, const char *path) { if (!parent || !path) { return NULL; } - // Find the AppContextT to get display format AppContextT *ctx = wgtGetContext(parent); if (!ctx) { return NULL; } - const DisplayT *d = &ctx->display; - - // Load image via stb_image (force RGB) - int imgW; - int imgH; - int channels; - uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); - - if (!rgb) { - return NULL; - } - - // Convert RGB to display pixel format - int32_t bpp = d->format.bytesPerPixel; - int32_t pitch = imgW * bpp; - uint8_t *buf = (uint8_t *)malloc(pitch * imgH); + int32_t imgW; + int32_t imgH; + int32_t pitch; + uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); if (!buf) { - stbi_image_free(rgb); return NULL; } - for (int32_t y = 0; y < imgH; y++) { - for (int32_t x = 0; x < imgW; x++) { - const uint8_t *src = rgb + (y * imgW + x) * 3; - uint32_t color = packColor(d, src[0], src[1], src[2]); - uint8_t *dst = buf + y * pitch + x * bpp; - - if (bpp == 1) { - *dst = (uint8_t)color; - } else if (bpp == 2) { - *(uint16_t *)dst = (uint16_t)color; - } else { - *(uint32_t *)dst = color; - } - } - } - - stbi_image_free(rgb); - return wgtImage(parent, buf, imgW, imgH, pitch); } diff --git a/dvx/widgets/widgetImageButton.c b/dvx/widgets/widgetImageButton.c index c5b13f3..4bc0b4c 100644 --- a/dvx/widgets/widgetImageButton.c +++ b/dvx/widgets/widgetImageButton.c @@ -44,6 +44,37 @@ WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, in } +// ============================================================ +// wgtImageButtonFromFile +// ============================================================ +// +// Load an image from disk and create an ImageButton widget. +// Delegates to dvxLoadImage for format decoding and pixel conversion. + +WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path) { + if (!parent || !path) { + return NULL; + } + + AppContextT *ctx = wgtGetContext(parent); + + if (!ctx) { + return NULL; + } + + int32_t imgW; + int32_t imgH; + int32_t pitch; + uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); + + if (!buf) { + return NULL; + } + + return wgtImageButton(parent, buf, imgW, imgH, pitch); +} + + // ============================================================ // wgtImageButtonSetData // ============================================================ diff --git a/dvxshell/shellExport.c b/dvxshell/shellExport.c index d744c3c..5ffc2b1 100644 --- a/dvxshell/shellExport.c +++ b/dvxshell/shellExport.c @@ -159,6 +159,9 @@ DXE_EXPORT_TABLE(shellExportTable) DXE_EXPORT(dvxGetDisplay) DXE_EXPORT(dvxGetBlitOps) DXE_EXPORT(dvxSetWindowIcon) + DXE_EXPORT(dvxLoadImage) + DXE_EXPORT(dvxFreeImage) + DXE_EXPORT(dvxSaveImage) DXE_EXPORT(dvxScreenshot) DXE_EXPORT(dvxWindowScreenshot) DXE_EXPORT(dvxCreateAccelTable) @@ -311,6 +314,7 @@ DXE_EXPORT_TABLE(shellExportTable) // dvxWidget.h — image / image button DXE_EXPORT(wgtImageButton) + DXE_EXPORT(wgtImageButtonFromFile) DXE_EXPORT(wgtImageButtonSetData) DXE_EXPORT(wgtImage) DXE_EXPORT(wgtImageFromFile)