First commit

This commit is contained in:
2026-09-15 13:34:51 -05:00
commit db208b60a2
7 changed files with 1055 additions and 0 deletions

301
viewer.cpp Normal file
View File

@@ -0,0 +1,301 @@
/*
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 <https://www.gnu.org/licenses/>.
*/
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <vector>
#include <string>
#include <filesystem>
#include <iostream>
#include <unordered_set>
#include <algorithm>
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<std::string>{}(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<std::string,CaseInsensitiveHash,
CaseInsensitiveEqual> 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<std::string> 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);
}
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:
{
SetNewImage(this->imageIndex-20);
}
break;
case SDL_SCANCODE_PAGEDOWN:
{
SetNewImage(this->imageIndex+20);
}
break;
case SDL_SCANCODE_LEFT:
{
SetNewImage(this->imageIndex-1);
}
break;
case SDL_SCANCODE_RIGHT:
{
SetNewImage(this->imageIndex+1);
}
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;
}
}
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());
}