First commit
This commit is contained in:
301
viewer.cpp
Normal file
301
viewer.cpp
Normal 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());
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user