DVX_GUI/apps/imgview/imgview.c

373 lines
11 KiB
C

// imgview.c -- DVX Image Viewer
//
// Displays BMP, PNG, JPG, and GIF images. The image is scaled to fit
// the window while preserving aspect ratio. Resize the window to zoom.
// Open files via the File menu or by launching with Run in the Task Manager.
#include "dvxApp.h"
#include "dvxDialog.h"
#include "dvxWidget.h"
#include "dvxWm.h"
#include "shellApp.h"
#include "thirdparty/stb_image.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// App descriptor
// ============================================================
AppDescriptorT appDescriptor = {
.name = "Image Viewer",
.hasMainLoop = false,
.multiInstance = true,
.stackSize = SHELL_STACK_DEFAULT,
.priority = TS_PRIORITY_NORMAL
};
// ============================================================
// Constants
// ============================================================
#define IV_WIN_W 400
#define IV_WIN_H 320
#define CMD_OPEN 100
#define CMD_CLOSE 101
// ============================================================
// Prototypes
// ============================================================
int32_t appMain(DxeAppContextT *ctx);
static void loadAndDisplay(const char *path);
static void onMenu(WindowT *win, int32_t menuId);
static void onPaint(WindowT *win, RectT *dirty);
static void onResize(WindowT *win, int32_t contentW, int32_t contentH);
static void openFile(void);
// ============================================================
// Module state
// ============================================================
static DxeAppContextT *sCtx = NULL;
static AppContextT *sAc = NULL;
static WindowT *sWin = NULL;
// Source image (RGB, from stb_image)
static uint8_t *sImgRgb = NULL;
static int32_t sImgW = 0;
static int32_t sImgH = 0;
// Scaled image in native pixel format (for direct blit)
static uint8_t *sScaled = NULL;
static int32_t sScaledW = 0;
static int32_t sScaledH = 0;
static int32_t sScaledPitch = 0;
static int32_t sLastFitW = 0; // window size the image was last scaled for
static int32_t sLastFitH = 0;
// ============================================================
// buildScaled -- scale source image to fit window
// ============================================================
static void buildScaled(int32_t fitW, int32_t fitH) {
free(sScaled);
sScaled = NULL;
sLastFitW = fitW;
sLastFitH = fitH;
if (!sImgRgb || fitW < 1 || fitH < 1) {
return;
}
// Fit image into fitW x fitH preserving aspect ratio
int32_t dstW = fitW;
int32_t dstH = (sImgH * fitW) / sImgW;
if (dstH > fitH) {
dstH = fitH;
dstW = (sImgW * fitH) / sImgH;
}
if (dstW < 1) {
dstW = 1;
}
if (dstH < 1) {
dstH = 1;
}
int32_t bpp = sAc->display.format.bytesPerPixel;
int32_t pitch = dstW * bpp;
int32_t bitsPerPx = sAc->display.format.bitsPerPixel;
sScaled = (uint8_t *)malloc(pitch * dstH);
sScaledW = dstW;
sScaledH = dstH;
sScaledPitch = pitch;
if (!sScaled) {
return;
}
// Bilinear scale from sImgRgb to native pixel format
int32_t srcStride = sImgW * 3;
for (int32_t y = 0; y < dstH; y++) {
if ((y & 31) == 0 && y > 0) {
dvxUpdate(sAc);
}
int32_t srcYfp = (int32_t)((int64_t)y * sImgH * 65536 / dstH);
int32_t sy0 = srcYfp >> 16;
int32_t sy1 = sy0 + 1;
int32_t fy = (srcYfp >> 8) & 0xFF;
int32_t ify = 256 - fy;
uint8_t *dst = sScaled + y * pitch;
if (sy1 >= sImgH) {
sy1 = sImgH - 1;
}
uint8_t *row0 = sImgRgb + sy0 * srcStride;
uint8_t *row1 = sImgRgb + sy1 * srcStride;
for (int32_t x = 0; x < dstW; x++) {
int32_t srcXfp = (int32_t)((int64_t)x * sImgW * 65536 / dstW);
int32_t sx0 = srcXfp >> 16;
int32_t sx1 = sx0 + 1;
int32_t fx = (srcXfp >> 8) & 0xFF;
int32_t ifx = 256 - fx;
if (sx1 >= sImgW) {
sx1 = sImgW - 1;
}
uint8_t *p00 = row0 + sx0 * 3;
uint8_t *p10 = row0 + sx1 * 3;
uint8_t *p01 = row1 + sx0 * 3;
uint8_t *p11 = row1 + sx1 * 3;
int32_t r = (p00[0] * ifx * ify + p10[0] * fx * ify + p01[0] * ifx * fy + p11[0] * fx * fy) >> 16;
int32_t g = (p00[1] * ifx * ify + p10[1] * fx * ify + p01[1] * ifx * fy + p11[1] * fx * fy) >> 16;
int32_t b = (p00[2] * ifx * ify + p10[2] * fx * ify + p01[2] * ifx * fy + p11[2] * fx * fy) >> 16;
uint32_t px = packColor(&sAc->display, (uint8_t)r, (uint8_t)g, (uint8_t)b);
if (bitsPerPx == 8) {
dst[x] = (uint8_t)px;
} else if (bitsPerPx == 15 || bitsPerPx == 16) {
((uint16_t *)dst)[x] = (uint16_t)px;
} else {
((uint32_t *)dst)[x] = px;
}
}
}
}
// ============================================================
// loadAndDisplay
// ============================================================
static void loadAndDisplay(const char *path) {
// Free previous image
if (sImgRgb) {
stbi_image_free(sImgRgb);
sImgRgb = NULL;
}
int32_t channels;
sImgRgb = stbi_load(path, &sImgW, &sImgH, &channels, 3);
if (!sImgRgb) {
dvxMessageBox(sAc, "Error", "Could not load image.", MB_OK | MB_ICONERROR);
return;
}
// Update title bar
const char *fname = strrchr(path, '/');
const char *bslash = strrchr(path, '\\');
if (bslash > fname) {
fname = bslash;
}
fname = fname ? fname + 1 : path;
char title[128];
snprintf(title, sizeof(title), "%s - Image Viewer", fname);
dvxSetTitle(sAc, sWin, title);
// Scale and repaint
buildScaled(sWin->contentW, sWin->contentH);
RectT fullRect = {0, 0, sWin->contentW, sWin->contentH};
sWin->onPaint(sWin, &fullRect);
sWin->contentDirty = true;
dvxInvalidateWindow(sAc, sWin);
}
// ============================================================
// onMenu
// ============================================================
static void onMenu(WindowT *win, int32_t menuId) {
(void)win;
switch (menuId) {
case CMD_OPEN:
openFile();
break;
case CMD_CLOSE:
dvxDestroyWindow(sAc, sWin);
sWin = NULL;
break;
}
}
// ============================================================
// onPaint
// ============================================================
static void onPaint(WindowT *win, RectT *dirty) {
(void)dirty;
// If the resize drag has ended and the window size changed since
// the last scale, rebuild now. During drag, just show the old
// scaled image (or dark background) to avoid expensive per-frame scaling.
if (sImgRgb && sAc->stack.resizeWindow < 0) {
if (sLastFitW != win->contentW || sLastFitH != win->contentH) {
buildScaled(win->contentW, win->contentH);
}
}
DisplayT cd = sAc->display;
cd.backBuf = win->contentBuf;
cd.width = win->contentW;
cd.height = win->contentH;
cd.pitch = win->contentPitch;
cd.clipX = 0;
cd.clipY = 0;
cd.clipW = win->contentW;
cd.clipH = win->contentH;
// Fill background
uint32_t bg = packColor(&sAc->display, 32, 32, 32);
rectFill(&cd, &sAc->blitOps, 0, 0, win->contentW, win->contentH, bg);
// Blit scaled image centered, clipped to content bounds
if (sScaled) {
int32_t offX = (win->contentW - sScaledW) / 2;
int32_t offY = (win->contentH - sScaledH) / 2;
int32_t bpp = sAc->display.format.bytesPerPixel;
// Compute visible region (clip source and dest)
int32_t srcX = 0;
int32_t srcY = 0;
int32_t blitW = sScaledW;
int32_t blitH = sScaledH;
if (offX < 0) {
srcX = -offX;
blitW += offX;
offX = 0;
}
if (offY < 0) {
srcY = -offY;
blitH += offY;
offY = 0;
}
if (offX + blitW > win->contentW) {
blitW = win->contentW - offX;
}
if (offY + blitH > win->contentH) {
blitH = win->contentH - offY;
}
for (int32_t y = 0; y < blitH; y++) {
uint8_t *src = sScaled + (srcY + y) * sScaledPitch + srcX * bpp;
uint8_t *dst = win->contentBuf + (offY + y) * win->contentPitch + offX * bpp;
memcpy(dst, src, blitW * bpp);
}
}
}
// ============================================================
// onResize
// ============================================================
static void onResize(WindowT *win, int32_t contentW, int32_t contentH) {
(void)win;
(void)contentW;
(void)contentH;
// Don't rescale here -- onPaint handles it after the drag ends.
// This avoids expensive bilinear scaling on every frame of a drag.
}
// ============================================================
// openFile
// ============================================================
static void openFile(void) {
FileFilterT filters[] = {
{ "Images (*.bmp;*.jpg;*.png;*.gif)", "*.bmp;*.jpg;*.png;*.gif" },
{ "All Files (*.*)", "*.*" }
};
char path[260];
if (dvxFileDialog(sAc, "Open Image", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
loadAndDisplay(path);
}
}
// ============================================================
// appMain
// ============================================================
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
sAc = ctx->shellCtx;
int32_t winX = (sAc->display.width - IV_WIN_W) / 2;
int32_t winY = (sAc->display.height - IV_WIN_H) / 2;
sWin = dvxCreateWindow(sAc, "Image Viewer", winX, winY, IV_WIN_W, IV_WIN_H, true);
if (!sWin) {
return -1;
}
sWin->onPaint = onPaint;
sWin->onResize = onResize;
sWin->onMenu = onMenu;
MenuBarT *menuBar = wmAddMenuBar(sWin);
MenuT *fileMenu = wmAddMenu(menuBar, "&File");
wmAddMenuItem(fileMenu, "&Open...\tCtrl+O", CMD_OPEN);
wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "&Close", CMD_CLOSE);
AccelTableT *accel = dvxCreateAccelTable();
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN);
sWin->accelTable = accel;
// Initial paint (dark background)
RectT fullRect = {0, 0, sWin->contentW, sWin->contentH};
onPaint(sWin, &fullRect);
sWin->contentDirty = true;
return 0;
}