/* A simple SDL2 photo viewer that is keyboard navigated Copyright (C) 2026 Mike Nolan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include struct CaseInsensitiveHash { std::size_t operator()(const std::string& s) const { std::string lower = s; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); }); return std::hash{}(lower); } }; struct CaseInsensitiveEqual { bool operator()(const std::string& a, const std::string& b) const { return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](unsigned char ca, unsigned char cb) { return std::tolower(ca) == std::tolower(cb); }); } }; std::unordered_set exts = { ".bmp", ".png", ".jpg", ".jpeg", ".avif", ".tif", ".tiff", ".webp" //.bmp, .jpg, .jpeg, .png, .avif, .tif, .tiff, .webp }; struct App { std::filesystem::path dir; std::vector filenames; SDL_Texture* texture = nullptr; SDL_Window* window = nullptr; SDL_Renderer* renderer = nullptr; size_t imageIndex = 0; size_t originalPhoto = 0; void SetNewImage(size_t index) { index %= filenames.size(); if(!filenames.empty()) { imageIndex = index; if(texture) SDL_DestroyTexture(texture); texture=nullptr; std::string& filename = filenames[index]; auto file = (dir / filename).string(); auto surface = IMG_Load(file.c_str()); if(surface) { std::string title = "["; title += std::to_string(index+1); title += '/'; title += std::to_string(filenames.size()); title += "] "; title += filename; title += " - Tesses Image Viewer"; SDL_SetWindowTitle(window,title.c_str()); texture = SDL_CreateTextureFromSurface(renderer,surface); SDL_FreeSurface(surface); } } } void LoadImageList(std::filesystem::path path) { dir = path.parent_path(); auto filename = path.filename(); size_t curPhoto = 0; size_t i = 0; for(auto& item : std::filesystem::directory_iterator(dir)) { auto curPath = item.path().filename(); if(item.is_regular_file() && isValid(curPath)) { this->filenames.push_back(curPath); if(curPath == filename) { curPhoto = i; } i++; } } if(i > 0) { SetNewImage(curPhoto); this->originalPhoto = curPhoto; } } bool isValid(const std::filesystem::path& path) { if(!path.has_extension()) return false; std::string ext = path.extension(); return exts.count(ext) != 0; } App(std::string curFile) { window = SDL_CreateWindow("[0/0] No Image - Tesses Image Viewer",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,640,480, SDL_WINDOW_MAXIMIZED|SDL_WINDOW_RESIZABLE); if(!window) return; renderer = SDL_CreateRenderer(window,-1,SDL_RENDERER_SOFTWARE); if(!renderer) return; LoadImageList(curFile); } void Prev() { this->imageIndex--; if(this->imageIndex >= this->filenames.size()) this->imageIndex=this->filenames.size()-1; } void Next() { this->imageIndex++; if(this->imageIndex >= this->filenames.size()) this->imageIndex=0; } bool Update() { if(!window || !renderer) return false; SDL_Event evt; if(SDL_WaitEvent(&evt) == 1) { switch(evt.type) { case SDL_QUIT: return false; case SDL_KEYUP: { switch(evt.key.keysym.scancode) { case SDL_SCANCODE_ESCAPE: { return false; } break; case SDL_SCANCODE_O: { SetNewImage(originalPhoto); } break; case SDL_SCANCODE_HOME: { SetNewImage(0); } break; case SDL_SCANCODE_END: { SetNewImage(filenames.size()-1); } break; case SDL_SCANCODE_PAGEUP: { for(size_t i = 0; i < 20; i++) Prev(); SetNewImage(this->imageIndex); } break; case SDL_SCANCODE_PAGEDOWN: { for(size_t i = 0; i < 20; i++) Next(); SetNewImage(this->imageIndex); } break; case SDL_SCANCODE_LEFT: { Prev(); SetNewImage(this->imageIndex); } break; case SDL_SCANCODE_RIGHT: { Next(); SetNewImage(this->imageIndex); } break; case SDL_SCANCODE_F: { auto flags = SDL_GetWindowFlags(window); if(flags & SDL_WINDOW_FULLSCREEN_DESKTOP) { SDL_SetWindowFullscreen(window, 0); } else { SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); } } break; default: break; } } break; } } SDL_SetRenderDrawColor(renderer,0,0,0,255); SDL_RenderClear(renderer); if(this->texture) { SDL_Rect src={.x=0,.y=0}; SDL_Rect dest = {.x=0,.y=0}; SDL_QueryTexture(this->texture,NULL,NULL,&src.w,&src.h); SDL_GetWindowSize(window,&dest.w,&dest.h); auto scale = std::min((double)dest.w/(double)src.w,(double)dest.h/(double)src.h); auto drawW = (double)src.w*scale; auto drawH = (double)src.h*scale; auto offsetX = ((double)dest.w - drawW) / 2; auto offsetY = ((double)dest.h - drawH) / 2; dest.x = (int)offsetX; dest.y = (int)offsetY; dest.w = (int)drawW; dest.h = (int)drawH; SDL_RenderCopy(renderer,texture,&src,&dest); } SDL_RenderPresent(renderer); return true; } ~App() { if(renderer) SDL_DestroyRenderer(renderer); if(window) SDL_DestroyWindow(window); } }; int main(int argc, char** argv) { std::string path; if(argc < 2 || !std::filesystem::is_regular_file(argv[1])) { printf( "USAGE %s imagefile\n" "Supported formats: .bmp, .jpg, .jpeg, .png, .avif, .tif, .tiff, .webp\n" "Licensed under GPLv3\n" "Keyboard Shortcuts\n" "<-: Previous image\n" "->: Next image\n" "HOME: First image\n" "END: Last image\n" "PGUP: Go previous 20\n" "PGDOWN: Go next 20\n" "O: The image that was loaded when you started\n" "Esc: Quit\n" ,argv[0]); return 1; } path = argv[1]; SDL_Init(SDL_INIT_VIDEO); IMG_Init(IMG_INIT_JPG|IMG_INIT_PNG|IMG_INIT_AVIF|IMG_INIT_TIF|IMG_INIT_WEBP); SDL_SetHint("SDL_VIDEO_X11_WMCLASS", "net.tesses.image-viewer"); SDL_SetHint("SDL_VIDEO_WAYLAND_WMCLASS", "net.tesses.image-viewer"); App app(path); while(app.Update()); }