-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
5 changed files
with
293 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]]) | ||
} | ||
} |