373 lines
11 KiB
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;
|
|
}
|