// 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 #include #include // ============================================================ // 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; }