initial commit

This commit is contained in:
Veneficium 2025-11-06 23:48:28 +01:00
commit 82ae18ce43
7 changed files with 2836 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

2427
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

26
Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "image_viewer"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.98"
image = "0.25.6"
softbuffer = { version = "0.4.6", default-features = false, features = ["wayland", "wayland-dlopen", "kms"] }
winit = { version = "0.30.9", default-features = false, features = ["wayland", "wayland-dlopen", "rwh_06"] }
[profile.release]
opt-level = 2
debug = false
incremental = false
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
strip = true # Strip symbols from binary*
[profile.dev]
debug = true
opt-level = 0
[features]
gpu = []

96
flake.lock generated Normal file
View file

@ -0,0 +1,96 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1762111121,
"narHash": "sha256-4vhDuZ7OZaZmKKrnDpxLZZpGIJvAeMtK6FKLJYUtAdw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3d51a0365f6695e7dd5cdf3e180604530ed33b4",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1762396738,
"narHash": "sha256-BarSecuxtzp1boERdABLkkoxQTi6s/V33lJwUbWLrLY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "c63598992afd54d215d54f2b764adc0484c2b159",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

49
flake.nix Normal file
View file

@ -0,0 +1,49 @@
{
description = "i dont know what I'm doing";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
nixpkgs,
rust-overlay,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
in
with pkgs;
{
devShells.default = mkShell rec {
packages = [
cargo-bloat
upx
pkg-config
];
buildInputs = [
rust-bin.stable.latest.default
libxkbcommon
wayland
vulkan-loader
];
LD_LIBRARY_PATH = "${lib.makeLibraryPath buildInputs}";
};
packages = {
image_viewer = pkgs.callPackage ./package.nix { };
};
}
);
}

33
package.nix Normal file
View file

@ -0,0 +1,33 @@
{
lib,
rustPlatform,
pkg-config,
libxkbcommon,
wayland,
vulkan-loader,
}:
rustPlatform.buildRustPackage {
pname = "image_viewer";
version = "0.0.1";
src = lib.fileset.toSource {
root = ./.;
fileset = ./.;
};
strictDeps = true;
cargoDeps = rustPlatform.importCargoLock { lockFile = ./Cargo.lock; };
nativeBuildInputs = [
pkg-config
];
buildInputs = [
libxkbcommon
wayland
vulkan-loader
];
}

204
src/main.rs Normal file
View file

@ -0,0 +1,204 @@
use std::fs::File;
use std::io::BufReader;
use std::num::NonZeroU32;
use std::rc::Rc;
use std::{env, time::*};
use anyhow::Context;
use image::codecs::gif::GifDecoder;
use image::{AnimationDecoder, Delay, Frame, RgbImage};
use image::{DynamicImage, ImageFormat, ImageReader, imageops::FilterType};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::window::{Window, WindowId};
use softbuffer::Surface;
#[derive(Default)]
struct App {
window: Option<Rc<Window>>,
surface: Option<Surface<OwnedDisplayHandle, Rc<Window>>>,
image: DynamicImage,
resized_image: RgbImage,
// animated images stuff
animation: Option<Animation>,
}
#[derive(Default)]
struct Animation {
frames: Vec<Frame>,
resized_frames: Vec<Option<RgbImage>>,
frame_dur: Vec<Delay>,
//current frame
frame: usize,
last_update: Option<Instant>,
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let window_attributes = Window::default_attributes().with_title("A fantastic window!");
let window = Rc::new(event_loop.create_window(window_attributes).unwrap());
let context = softbuffer::Context::new(event_loop.owned_display_handle()).unwrap();
let surface = Surface::new(&context, window.clone()).unwrap();
let size = window.inner_size();
self.resized_image = self
.image
.resize(size.width, size.height, FilterType::Nearest)
.to_rgb8();
if let Some(anim) = self.animation.as_mut() {
anim.frame_dur = anim.frames.iter().map(|frame| frame.delay()).collect();
anim.last_update = Some(Instant::now());
anim.resized_frames = vec![None; anim.frames.len()];
}
self.surface = Some(surface);
self.window = Some(window);
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
println!("Close was requested; stopping");
event_loop.exit();
}
WindowEvent::RedrawRequested => {
let window = self
.window
.as_ref()
.expect("redraw request without a window");
let surface = self
.surface
.as_mut()
.expect("redraw request without a surface");
let size = window.inner_size();
if let Some(anim) = self.animation.as_mut() {
let curr_frame = anim.frame;
match anim.resized_frames[curr_frame].as_ref() {
Some(frame) => {
self.resized_image = frame.clone();
}
None => {
let original_buffer = anim.frames[curr_frame].buffer().clone();
let resized_frame = DynamicImage::ImageRgba8(original_buffer)
.resize(size.width, size.height, FilterType::Nearest)
.into_rgb8();
anim.resized_frames[curr_frame] = Some(resized_frame);
self.resized_image = anim.resized_frames[curr_frame].clone().unwrap();
}
}
}
let mut buffer = surface.buffer_mut().unwrap();
let x_offset = ((size.width - self.resized_image.width()) / 2) as usize;
let y_offset = ((size.height - self.resized_image.height()) / 2) as usize;
for (y, row) in buffer.chunks_exact_mut(size.width as usize).enumerate() {
if y < y_offset || y >= y_offset + self.resized_image.height() as usize {
row.fill(0);
continue;
}
for (x, buf_pix) in row.iter_mut().enumerate() {
if x < x_offset || x >= x_offset + self.resized_image.width() as usize {
*buf_pix = 0;
continue;
}
let img_pix = self
.resized_image
.get_pixel((x - x_offset) as u32, (y - y_offset) as u32);
*buf_pix =
u32::from_be_bytes([0, img_pix.0[0], img_pix.0[1], img_pix.0[2]]);
}
}
window.pre_present_notify();
buffer.present().unwrap();
if let Some(anim) = self.animation.as_mut() {
if anim.last_update.unwrap().elapsed() > anim.frame_dur[anim.frame].into() {
anim.last_update = Some(Instant::now());
anim.frame += 1;
if anim.frame >= anim.frames.len() {
anim.frame = 0;
}
}
//TODO don't fucking spam redraws
window.request_redraw();
}
}
WindowEvent::Resized(new_size) => {
let surface = self
.surface
.as_mut()
.expect("resize request without a surface");
surface
.resize(
NonZeroU32::new(new_size.width).unwrap(),
NonZeroU32::new(new_size.height).unwrap(),
)
.expect("surface resize gone wrong");
if let Some(anim) = self.animation.as_mut() {
anim.resized_frames = vec![None; anim.frames.len()];
} else {
self.resized_image = self
.image
.resize(new_size.width, new_size.height, FilterType::Nearest)
.to_rgb8();
}
}
_ => (),
}
}
}
fn main() -> anyhow::Result<()> {
let event_loop = EventLoop::new().context("Failed to create event loop")?;
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("No file provided!\n");
return Ok(());
};
let mut app = App::default();
let image = ImageReader::open(args[1].clone())
.context(format!("Failed to open image at {}", args[1]))?;
match image.format().unwrap() {
ImageFormat::Gif => {
let mut animation = Animation::default();
let file = BufReader::new(
File::open(args[1].clone())
.context(format!("Failed to open GIF file at {}", args[1]))?,
);
let decoder = GifDecoder::new(file).context("Failed to create GIF decoder")?;
let frames = decoder
.into_frames()
.collect_frames()
.context("Failed to grab GIF frames")?;
animation.frames = frames;
app.animation = Some(animation);
}
_ => {
app.image = image.decode().context("Failed to decode image")?;
}
};
event_loop.run_app(&mut app)?;
Ok(())
}