initial commit
This commit is contained in:
commit
82ae18ce43
7 changed files with 2836 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
2427
Cargo.lock
generated
Normal file
2427
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
26
Cargo.toml
Normal file
26
Cargo.toml
Normal 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
96
flake.lock
generated
Normal 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
49
flake.nix
Normal 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
33
package.nix
Normal 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
204
src/main.rs
Normal 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(())
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue