Skip to content

Commit

Permalink
Add initial iced GUI application
Browse files Browse the repository at this point in the history
Just a simple image viewer for now. It can only read P6 PPM images from stdin.

Signed-off-by: Christopher N. Hesse <[email protected]>
  • Loading branch information
raymanfx committed Oct 29, 2023
1 parent ff01523 commit fb7216b
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[workspace]
members = ["ffimage", "ffimage-yuv"]
members = ["ffimage", "ffimage-yuv", "ffimage-app"]
resolver="2"
18 changes: 18 additions & 0 deletions ffimage-app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "ffimage-app"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "ffimage hero application"
authors = ["Christopher N. Hesse <[email protected]>"]
repository= "https://github.com/raymanfx/ffimage"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-std = "1.12.0"
atty = "0.2.14"
iced = { version = "0.7.0", features = ["async-std", "image"] }

ffimage = { version = "0.10.0", path = "../ffimage" }
ffimage_yuv = { version = "0.10.0", path = "../ffimage-yuv" }
149 changes: 149 additions & 0 deletions ffimage-app/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use std::{env, io, io::Read};

use iced::{
executor,
widget::{column, container, image, text::Text},
Application, Command, Length, Renderer, Settings, Subscription, Theme,
};

use ffimage::{
color::Rgb,
iter::{BytesExt, ColorConvertExt, PixelsExt},
};

mod ppm;

mod rgba;
use rgba::Rgba;

#[derive(Debug)]
enum App {
Empty,
Loading,
Loaded { image: Image, handle: image::Handle },
Error(String),
}

#[derive(Debug, Clone)]
pub enum Message {
Loaded(Result<Image, &'static str>),
}

fn main() -> iced::Result {
App::run(Settings::default())
}

impl Application for App {
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();

fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
let args: Vec<String> = env::args().collect();

if let Some(last) = args.last() {
if last == "-" {
return (
App::Loading,
Command::perform(load_from_stdin(), Message::Loaded),
);
}
}

(App::Empty, Command::none())
}

fn title(&self) -> String {
match self {
App::Empty => String::from("ffimage"),
App::Loading => String::from("ffimage - Loading"),
App::Loaded { image, handle: _ } => {
format!("ffimage - {} x {}", image.width, image.height)
}
App::Error(_) => String::from("ffimage - Error"),
}
}

fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match self {
App::Loading => match message {
Message::Loaded(res) => {
match res {
Ok(image) => {
let rgba: Vec<u8> = image
.rgb
.iter()
.copied()
.pixels::<Rgb<u8>>()
.colorconvert::<Rgba<u8>>()
.bytes()
.flatten()
.collect();

let handle =
image::Handle::from_pixels(image.width, image.height, rgba);

*self = App::Loaded { image, handle }
}
Err(reason) => *self = App::Error(String::from(reason)),
}
Command::none()
}
},
_ => Command::none(),
}
}

fn view(&self) -> iced::Element<'_, Self::Message, Renderer<Self::Theme>> {
let content = match self {
App::Empty => column!(Text::new("No data")),
App::Loading => column![Text::new("Loading ..")],
App::Loaded { image: _, handle } => column![image::Viewer::new(handle.clone())],
App::Error(reason) => column![container(Text::new(format!("Error: {reason}")))],
};

container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.padding(20)
.into()
}

fn subscription(&self) -> Subscription<Self::Message> {
Subscription::none()
}
}

#[derive(Debug, Clone)]
pub struct Image {
width: u32,
height: u32,
rgb: Vec<u8>,
}

async fn load_from_stdin() -> Result<Image, &'static str> {
if atty::isnt(atty::Stream::Stdin) {
return Err("stdin is no tty");
}

// read bytes from stdin
let stdin = io::stdin().lock();
let bytes = io::BufReader::new(stdin).bytes();
let bytes = bytes.filter_map(|res| match res {
Ok(byte) => Some(byte),
Err(_) => None,
});

let res = ppm::read(bytes);
match res {
Ok(ppm) => Ok(Image {
width: ppm.width,
height: ppm.height,
rgb: ppm.bytes,
}),
Err(e) => Err(e),
}
}
87 changes: 87 additions & 0 deletions ffimage-app/src/ppm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
pub struct Ppm {
pub width: u32,
pub height: u32,
pub range: u32,
pub bytes: Vec<u8>,
}

pub fn read(bytes: impl IntoIterator<Item = u8>) -> Result<Ppm, &'static str> {
let mut bytes = bytes.into_iter();

// parse format from first line
let mut magic = [0u8; 2];
magic[0] = if let Some(byte) = bytes.next() {
byte
} else {
return Err("ppm: not enough bytes");
};
magic[1] = if let Some(byte) = bytes.next() {
byte
} else {
return Err("ppm: not enough bytes");
};

// is this a P6 PPM?
if magic != *b"P6" {
return Err("ppm: cannot handle magic");
}

fn real_bytes(iter: &mut impl Iterator<Item = u8>, limit: usize) -> Vec<u8> {
let mut bytes = Vec::new();
for byte in iter {
if bytes.len() == limit {
break;
}

if byte == b' ' || byte == b'\n' {
if !bytes.is_empty() {
break;
}
} else {
bytes.push(byte);
}
}
bytes
}

// parse width
let width_bytes = real_bytes(&mut bytes, 10);
let width = std::str::from_utf8(&width_bytes)
.expect("bytes should contain ASCII data")
.parse::<usize>()
.expect("value should be integer");

// parse height
let height_bytes = real_bytes(&mut bytes, 10);
let height = std::str::from_utf8(&height_bytes)
.expect("bytes should contain ASCII data")
.parse::<usize>()
.expect("value should be integer");

// parse range
let range_bytes = real_bytes(&mut bytes, 10);
let range = std::str::from_utf8(&range_bytes)
.expect("bytes should contain ASCII data")
.parse::<usize>()
.expect("value should be integer");

if range > 255 {
return Err("ppm: cannot handle range: {range}");
}

// take only as many bytes as we expect there to be in the image
let ppm_len = width * height * 3;
let bytes: Vec<u8> = bytes.take(ppm_len).collect();

// verify buffer length
if bytes.len() != width * height * 3 {
return Err("ppm: invalid length");
}

Ok(Ppm {
width: width as u32,
height: height as u32,
range: range as u32,
bytes,
})
}
37 changes: 37 additions & 0 deletions ffimage-app/src/rgba.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::ops::Deref;

use ffimage::color::{Gray, Rgb};

pub struct Rgba<T>([T; 4]);

impl<T> Deref for Rgba<T> {
type Target = [T; 4];

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T> From<[T; 4]> for Rgba<T> {
fn from(value: [T; 4]) -> Self {
Rgba(value)
}
}

impl From<Rgb<u8>> for Rgba<u8> {
fn from(value: Rgb<u8>) -> Self {
Rgba([value[0], value[1], value[2], 255u8])
}
}

impl From<Gray<u8>> for Rgba<u8> {
fn from(value: Gray<u8>) -> Self {
Rgba([value[0], value[0], value[0], 255u8])
}
}

impl<T: Copy> From<Rgba<T>> for Rgb<T> {
fn from(value: Rgba<T>) -> Self {
Rgb([value[0], value[1], value[2]])
}
}

0 comments on commit fb7216b

Please sign in to comment.