This commit is contained in:
2025-12-26 23:26:56 +01:00
parent 38ce0f0c43
commit 8c5aa4f948
7 changed files with 4170 additions and 88 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
**/*.rs.bk
/testserver
.*.swp
video/*

4015
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,10 +16,22 @@ categories = [
[dependencies]
bufstream = "0.1"
clap = { version = "4.4", features = [ "derive" ] }
eframe = "0.26"
glob = "0.3"
image = "0.23"
num_cpus = "1.13.1"
regex = "1.5"
rayon = "1.5.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[[bin]]
name = "pixelpwnr"
path = "src/main.rs"
[[bin]]
name = "pixelpwnr-gui"
path = "src/gui.rs"
[profile.release]
lto = true

View File

@@ -37,6 +37,10 @@ pub struct Arguments {
#[arg(short, value_name = "PIXELS", default_value_t = 0)]
y: u16,
/// Tile images across the screen, ignoring offsets
#[arg(short, long)]
tile: bool,
/// Number of concurrent threads [default: number of CPUs]
#[arg(short, long, aliases = ["thread", "threads"])]
count: Option<usize>,
@@ -99,6 +103,11 @@ impl ArgHandler {
(self.data.x, self.data.y)
}
/// Whether to tile images across the screen.
pub fn tile(&self) -> bool {
self.data.tile
}
/// Get the FPS.
pub fn fps(&self) -> u32 {
self.data.fps

View File

@@ -1,13 +1,14 @@
use image::codecs::gif::GifDecoder;
use rayon::prelude::*;
use std::fs::File;
use std::path::Path;
use glob::glob;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::thread::sleep;
use std::time::Duration;
use crate::pix::canvas::Canvas;
use image::imageops::FilterType;
use image::{AnimationDecoder, DynamicImage};
use image::{AnimationDecoder, DynamicImage, RgbaImage};
/// A manager that manages all images to print.
pub struct ImageManager {
@@ -31,7 +32,7 @@ impl ImageManager {
/// Instantiate the image manager, and load the images from the given paths.
pub fn load(paths: &[&str], size: (u16, u16)) -> ImageManager {
// Show a status message
println!("Load and process {} image(s)...", paths.len());
println!("Load and process {} path(s)...", paths.len());
// Load the images from the paths
let image_manager = ImageManager::from(
@@ -44,7 +45,36 @@ impl ImageManager {
// TODO: process the image slices
// We succeeded
println!("All images have been loaded successfully");
println!(
"Loaded {} frame(s) from {} path(s).",
image_manager.images.len(),
paths.len()
);
image_manager
}
/// Instantiate the image manager, and load and tile the images from the given paths.
pub fn load_tiled(
paths: &[&str],
tile_size: (u16, u16),
canvas_size: (u16, u16),
) -> ImageManager {
// Show a status message
println!("Load and process {} path(s)...", paths.len());
let image_manager = ImageManager::from(
paths
.par_iter()
.flat_map(|path| load_image_tiled(path, tile_size, canvas_size))
.collect(),
);
println!(
"Loaded {} frame(s) from {} path(s).",
image_manager.images.len(),
paths.len()
);
image_manager
}
@@ -96,22 +126,99 @@ impl ImageManager {
/// Load the image at the given path, and size it correctly
fn load_image(path: &str, size: (u16, u16)) -> Vec<(DynamicImage, Option<Duration>)> {
// Create a path instance
load_image_frames(path)
.into_iter()
.map(|(image, frame_delay)| {
(
image.resize_exact(size.0 as u32, size.1 as u32, FilterType::Gaussian),
frame_delay,
)
})
.collect()
}
fn load_image_tiled(
path: &str,
tile_size: (u16, u16),
canvas_size: (u16, u16),
) -> Vec<(DynamicImage, Option<Duration>)> {
load_image_frames(path)
.into_iter()
.map(|(image, frame_delay)| {
(
tile_image(
image,
(tile_size.0 as u32, tile_size.1 as u32),
(canvas_size.0 as u32, canvas_size.1 as u32),
),
frame_delay,
)
})
.collect()
}
fn load_image_frames(path: &str) -> Vec<(DynamicImage, Option<Duration>)> {
if has_glob_pattern(path) {
let mut entries: Vec<PathBuf> = glob(path)
.expect("failed to read frames glob")
.filter_map(Result::ok)
.filter(|entry| entry.is_file())
.collect();
entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
let mut frames = Vec::new();
for entry in entries {
frames.extend(load_file_frames(&entry));
}
if frames.is_empty() {
panic!("The given frames glob matched no files");
}
return frames;
}
let path = Path::new(&path);
// Check whether the path exists
if path.is_dir() {
let mut entries: Vec<PathBuf> = fs::read_dir(path)
.expect("failed to read frames directory")
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|entry| entry.is_file())
.collect();
entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
let mut frames = Vec::new();
for entry in entries {
frames.extend(load_file_frames(&entry));
}
if frames.is_empty() {
panic!("The given frames directory is empty");
}
return frames;
}
if !path.is_file() {
panic!("The given path does not exist or is not a file");
}
load_file_frames(path)
}
fn has_glob_pattern(path: &str) -> bool {
path.contains('*') || path.contains('?') || path.contains('[')
}
fn load_file_frames(path: &Path) -> Vec<(DynamicImage, Option<Duration>)> {
let extension = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase());
// Load image(s)
let images = match extension.as_deref() {
// Load all GIF frames
match extension.as_deref() {
Some("gif") => GifDecoder::new(File::open(path).unwrap())
.expect("failed to decode GIF file")
.into_frames()
@@ -126,19 +233,27 @@ fn load_image(path: &str, size: (u16, u16)) -> Vec<(DynamicImage, Option<Duratio
)
})
.collect(),
// Load single image
_ => vec![(image::open(path).unwrap(), None)],
};
// Resize images to fit the screen
images
.into_iter()
.map(|(image, frame_delay)| {
(
image.resize_exact(size.0 as u32, size.1 as u32, FilterType::Gaussian),
frame_delay,
)
})
.collect()
}
}
fn tile_image(image: DynamicImage, tile_size: (u32, u32), canvas_size: (u32, u32)) -> DynamicImage {
let tile = image
.resize_exact(tile_size.0, tile_size.1, FilterType::Gaussian)
.to_rgba8();
let mut tiled = RgbaImage::new(canvas_size.0, canvas_size.1);
let tile_w = tile.width();
let tile_h = tile.height();
let mut y = 0;
while y < canvas_size.1 {
let mut x = 0;
while x < canvas_size.0 {
image::imageops::overlay(&mut tiled, &tile, x, y);
x += tile_w;
}
y += tile_h;
}
DynamicImage::ImageRgba8(tiled)
}

View File

@@ -30,21 +30,29 @@ fn start(arg_handler: &ArgHandler) {
let screen_size =
gather_host_facts(arg_handler).expect("Failed to gather facts about pixelflut server");
// Determine the size to use
let size = arg_handler.size(Some(screen_size));
let tile_size = arg_handler.size(Some(screen_size));
let (size, offset) = if arg_handler.tile() {
(screen_size, (0, 0))
} else {
(tile_size, arg_handler.offset())
};
// Create a new pixelflut canvas
let mut canvas = Canvas::new(
arg_handler.host(),
arg_handler.count(),
size,
arg_handler.offset(),
offset,
arg_handler.binary(),
arg_handler.flush(),
);
// Load the image manager
let mut image_manager = ImageManager::load(&arg_handler.image_paths(), size);
let mut image_manager = if arg_handler.tile() {
ImageManager::load_tiled(&arg_handler.image_paths(), tile_size, size)
} else {
ImageManager::load(&arg_handler.image_paths(), size)
};
// Start the work in the image manager, to walk through the frames
image_manager.work(&mut canvas, arg_handler.fps());

View File

@@ -50,6 +50,31 @@ impl Canvas {
/// Spawn the painters for this canvas
fn spawn_painters(&mut self, binary: bool, flush: bool) {
let grid = (self.painter_count as f64).sqrt() as usize;
if grid * grid == self.painter_count {
let tile_w = self.size.0 / (grid as u16);
let tile_h = self.size.1 / (grid as u16);
for row in 0..grid {
for col in 0..grid {
let x = (col as u16) * tile_w;
let y = (row as u16) * tile_h;
let w = if col == grid - 1 {
self.size.0 - x
} else {
tile_w
};
let h = if row == grid - 1 {
self.size.1 - y
} else {
tile_h
};
let painter_area = Rect::from(x, y, w, h);
self.spawn_painter(painter_area, binary, flush);
}
}
} else {
// Spawn some painters
for i in 0..self.painter_count {
// Determine the slice width
@@ -62,6 +87,7 @@ impl Canvas {
self.spawn_painter(painter_area, binary, flush);
}
}
}
/// Spawn a single painter in a thread.
fn spawn_painter(&mut self, area: Rect, binary: bool, flush: bool) {