├── .gitignore ├── src ├── core.rs ├── ext.rs ├── core │ ├── params.rs │ ├── params │ │ ├── frame_cut.rs │ │ ├── threshold.rs │ │ ├── values.rs │ │ ├── scale_type.rs │ │ ├── alignment.rs │ │ ├── background.rs │ │ ├── args.rs │ │ └── params.rs │ ├── bitmap.rs │ ├── meta.rs │ └── img2bm.rs ├── ext │ ├── unit_ext.rs │ ├── range_ext.rs │ ├── string_ext.rs │ ├── iter_ext.rs │ ├── image_ext.rs │ └── path_ext.rs ├── unused.rs └── main.rs ├── Cargo.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | img2fbm.iml 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | pub mod bitmap; 2 | pub mod img2bm; 3 | pub mod meta; 4 | pub mod params; 5 | -------------------------------------------------------------------------------- /src/ext.rs: -------------------------------------------------------------------------------- 1 | pub mod iter_ext; 2 | pub mod path_ext; 3 | pub mod string_ext; 4 | pub mod range_ext; 5 | pub mod image_ext; 6 | pub mod unit_ext; 7 | -------------------------------------------------------------------------------- /src/core/params.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod values; 3 | pub mod params; 4 | pub mod scale_type; 5 | pub mod threshold; 6 | pub mod frame_cut; 7 | pub mod background; 8 | pub mod alignment; 9 | -------------------------------------------------------------------------------- /src/ext/unit_ext.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | pub trait UnitUtil { 4 | fn flush(self: Self); 5 | } 6 | 7 | impl UnitUtil for () { 8 | 9 | fn flush(self: Self) { 10 | std::io::stdout().flush().unwrap(); 11 | } 12 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "img2fbm" 3 | version = "0.2.1" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | image = "0.25.2" 10 | clap = { version = "4.3.19", features = ["derive"] } 11 | indicatif = "0.17.5" 12 | shell-words = "1.1.0" 13 | -------------------------------------------------------------------------------- /src/ext/range_ext.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | pub fn for_each( 4 | ordinates: Range, 5 | abscissa: Range, 6 | mut action: F, 7 | ) where F: FnMut(u32,u32) { 8 | for y in ordinates { 9 | for x in abscissa.clone() { 10 | action(x, y); 11 | } 12 | } 13 | } 14 | 15 | /*pub fn for_each( 16 | ordinates: Range, 17 | abscissa: Range, 18 | action: F, 19 | ) where F: Fn(T,T), T: Copy + std::iter::Step { 20 | for y in ordinates { 21 | for x in abscissa { 22 | action(x, y); 23 | } 24 | } 25 | }*/ 26 | -------------------------------------------------------------------------------- /src/core/params/frame_cut.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter}; 2 | 3 | 4 | pub struct FrameCut { 5 | pub start: usize, 6 | pub end: usize, 7 | } 8 | 9 | impl Display for FrameCut { 10 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 11 | write!(f, "{}:{}", self.start, self.end) 12 | } 13 | } 14 | 15 | impl Debug for FrameCut { 16 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 17 | std::fmt::Display::fmt(self, f) 18 | } 19 | } 20 | 21 | impl Clone for FrameCut { 22 | fn clone(&self) -> Self { 23 | FrameCut { 24 | start: self.start, 25 | end: self.end, 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/core/params/threshold.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter}; 2 | 3 | 4 | pub struct Threshold { 5 | pub dark: f32, 6 | pub light: f32, 7 | } 8 | 9 | impl Threshold { 10 | 11 | pub fn is_empty(&self) -> bool { 12 | self.dark == self.light 13 | } 14 | 15 | pub fn size(&self) -> f32 { 16 | self.light - self.dark 17 | } 18 | 19 | pub fn contains(&self, other: f32) -> bool { 20 | self.dark < other && other < self.light 21 | } 22 | } 23 | 24 | impl Display for Threshold { 25 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 26 | write!(f, "{}:{}", self.dark, self.light) 27 | } 28 | } 29 | 30 | impl Debug for Threshold { 31 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 32 | std::fmt::Display::fmt(self, f) 33 | } 34 | } 35 | 36 | impl Clone for Threshold { 37 | fn clone(&self) -> Self { 38 | Threshold { 39 | dark: self.dark, 40 | light: self.light, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ext/string_ext.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | pub trait StringUtil { 4 | fn substring(&self, range: Range) -> Self; 5 | fn index_of(&self, char: char) -> Option; 6 | fn signed_index_of(&self, char: char) -> i32; 7 | fn last_index_of(&self, char: char) -> Option; 8 | fn signed_last_index_of(&self, char: char) -> i32; 9 | } 10 | 11 | impl StringUtil for String { 12 | 13 | fn substring(&self, range: Range) -> Self { 14 | String::from(&self[range]) 15 | } 16 | 17 | fn index_of(&self, char: char) -> Option { 18 | self.chars().position(|c| c == char) 19 | } 20 | 21 | fn signed_index_of(&self, char: char) -> i32 { 22 | self.index_of(char).map_or_else(|| -1, |i| i as i32) 23 | } 24 | 25 | fn last_index_of(&self, char: char) -> Option { 26 | self.chars().rev().position(|c| c == char).map(|i| self.len() - 1 - i) 27 | } 28 | 29 | fn signed_last_index_of(&self, char: char) -> i32 { 30 | self.last_index_of(char).map_or_else(|| -1, |i| i as i32) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/core/params/values.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | pub struct Values where T: FromStr + Copy { 4 | pub first: T, 5 | pub second: T, 6 | } 7 | 8 | impl Values where T: FromStr + Copy { 9 | pub fn from(value: &str, default_first: V, default_second: V) -> Result, String> where V: FromStr + Copy { 10 | let parts = value.split(':').collect::>(); 11 | let cause = || format!("'{value}' isn't a valid values"); 12 | if parts.is_empty() || parts.len() > 2 { 13 | return Err(cause()); 14 | } 15 | let first = *parts.first().unwrap(); 16 | let first = if first.is_empty() { default_first } else { 17 | first.parse::().map_err(|_| cause())? 18 | }; 19 | if parts.len() == 1 { 20 | return Ok(Values { first, second: first }) 21 | } 22 | let second = *parts.last().unwrap(); 23 | let second = if second.is_empty() { default_second } else { 24 | second.parse::().map_err(|_| cause())? 25 | }; 26 | return Ok(Values { first, second }); 27 | } 28 | } -------------------------------------------------------------------------------- /src/ext/iter_ext.rs: -------------------------------------------------------------------------------- 1 | use std::ops::AddAssign; 2 | use std::slice::Iter; 3 | 4 | pub trait Sum { 5 | fn sum_of(&self, init: R, block: F) -> R where F: Fn(&T) -> R, R: AddAssign; 6 | fn min_of(&self, init: R, block: F) -> R where F: Fn(&T) -> R, R: Ord + Eq + Copy; 7 | fn max_of(&self, init: R, block: F) -> R where F: Fn(&T) -> R, R: Ord; 8 | } 9 | 10 | impl<'a, T> Sum for Iter<'a, T> { 11 | 12 | fn sum_of(&self, init: R, block: F) -> R where F: Fn(&'a T) -> R, R: AddAssign { 13 | let mut sum: R = init; 14 | for it in self.to_owned() { 15 | sum += block(it); 16 | } 17 | return sum; 18 | } 19 | 20 | fn min_of(&self, init: R, block: F) -> R where F: Fn(&'a T) -> R, R: Ord + Eq + Copy { 21 | let mut min: R = init; 22 | for it in self.to_owned() { 23 | let next = block(it); 24 | if min == init || next < min { 25 | min = next 26 | } 27 | } 28 | return min; 29 | } 30 | 31 | fn max_of(&self, init: R, block: F) -> R where F: Fn(&'a T) -> R, R: Ord { 32 | let mut max: R = init; 33 | for it in self.to_owned() { 34 | let next = block(it); 35 | if next > max { 36 | max = next 37 | } 38 | } 39 | return max; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/core/params/scale_type.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter}; 2 | use clap::builder::PossibleValue; 3 | use clap::ValueEnum; 4 | 5 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 6 | pub enum ScaleType { 7 | Fill, Fit 8 | } 9 | 10 | impl ValueEnum for ScaleType { 11 | fn value_variants<'a>() -> &'a [Self] { 12 | &[ScaleType::Fill, ScaleType::Fit] 13 | } 14 | 15 | fn to_possible_value<'a>(&self) -> Option { 16 | Some(match self { 17 | ScaleType::Fill => PossibleValue::new("fill").help("Scale to fill animation bounds"), 18 | ScaleType::Fit => PossibleValue::new("fit").help("Scale to fit in animation bounds"), 19 | }) 20 | } 21 | } 22 | 23 | impl Display for ScaleType { 24 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 25 | write!(f, "{:?}", self) 26 | } 27 | } 28 | 29 | impl Debug for ScaleType { 30 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 31 | Display::fmt(self, f) 32 | } 33 | } 34 | 35 | impl std::str::FromStr for ScaleType { 36 | type Err = String; 37 | 38 | fn from_str(s: &str) -> Result { 39 | for variant in Self::value_variants() { 40 | if variant.to_possible_value().unwrap().matches(s, false) { 41 | return Ok(*variant); 42 | } 43 | } 44 | Err(format!("invalid variant: {s}")) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/core/bitmap.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Shl, Shr}; 2 | 3 | 4 | #[derive(Hash)] 5 | pub struct Bitmap { 6 | pub width: u8, 7 | pub height: u8, 8 | pub bytes: Vec, 9 | pub dx: i32, 10 | pub dy: i32, 11 | } 12 | 13 | impl Bitmap { 14 | 15 | pub fn new(width: u8, height: u8, dx: i32, dy: i32) -> Bitmap { 16 | Bitmap { width, height, bytes: vec![0x00], dx, dy } 17 | } 18 | 19 | pub fn set(&mut self, x: u32, y: u32) { 20 | let (byte, bit) = self.get_indexes(x, y); 21 | while self.bytes.len() <= byte { 22 | self.bytes.push(0x00) 23 | } 24 | let bit: u8 = 1u8.shl(bit); 25 | self.bytes[byte] |= bit; 26 | } 27 | 28 | pub fn invert(&mut self) { 29 | for index in 1..self.bytes.len() { 30 | self.bytes[index] = !self.bytes[index]; 31 | } 32 | } 33 | 34 | pub fn get(&self, x: u32, y: u32) -> bool { 35 | let (byte, bit) = self.get_indexes(x, y); 36 | return self.bytes.get(byte) 37 | .map(|it| (it.shr(bit) & 1u8) == 1) 38 | .unwrap_or(false); 39 | } 40 | 41 | pub fn get_src_x(&self, dst_x: u32) -> i32 { 42 | dst_x as i32 + self.dx 43 | } 44 | 45 | pub fn get_src_y(&self, dst_y: u32) -> i32 { 46 | dst_y as i32 + self.dy 47 | } 48 | 49 | fn get_indexes(&self, x: u32, y: u32) -> (usize, usize) { 50 | let offset = (self.width as u32 * y + x) as usize; 51 | (offset / 8 + 1, offset % 8) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ext/image_ext.rs: -------------------------------------------------------------------------------- 1 | use image::DynamicImage; 2 | use image::imageops::FilterType; 3 | 4 | pub trait Resizing { 5 | fn resize(&self, nwidth: u32, nheight: u32, fill: bool, filter: FilterType) -> Self; 6 | } 7 | 8 | impl Resizing for DynamicImage { 9 | 10 | fn resize(&self, to_width: u32, to_height: u32, fill: bool, filter: FilterType) -> Self { 11 | let (width, height) = resize_dimensions(self.width(), self.height(), to_width, to_height, fill); 12 | return self.resize_exact(width, height, filter); 13 | } 14 | } 15 | 16 | // image::math::utils::resize_dimensions() 17 | fn resize_dimensions( 18 | width: u32, 19 | height: u32, 20 | nwidth: u32, 21 | nheight: u32, 22 | fill: bool, 23 | ) -> (u32, u32) { 24 | let wratio = nwidth as f64 / width as f64; 25 | let hratio = nheight as f64 / height as f64; 26 | 27 | let ratio = if fill { 28 | f64::max(wratio, hratio) 29 | } else { 30 | f64::min(wratio, hratio) 31 | }; 32 | 33 | let nw = 1.max((width as f64 * ratio).round() as u64); 34 | let nh = 1.max((height as f64 * ratio).round() as u64); 35 | 36 | if nw > u64::from(u32::MAX) { 37 | let ratio = u32::MAX as f64 / width as f64; 38 | (u32::MAX, 1.max((height as f64 * ratio).round() as u32)) 39 | } else if nh > u64::from(u32::MAX) { 40 | let ratio = u32::MAX as f64 / height as f64; 41 | (1.max((width as f64 * ratio).round() as u32), u32::MAX) 42 | } else { 43 | (nw as u32, nh as u32) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/core/params/alignment.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter}; 2 | use clap::builder::PossibleValue; 3 | use clap::ValueEnum; 4 | 5 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 6 | pub enum Alignment { 7 | Left, Top, Right, Bottom 8 | } 9 | 10 | impl ValueEnum for Alignment { 11 | fn value_variants<'a>() -> &'a [Self] { 12 | &[Alignment::Left, Alignment::Top, Alignment::Right, Alignment::Bottom] 13 | } 14 | 15 | fn to_possible_value<'a>(&self) -> Option { 16 | Some(match self { 17 | Alignment::Left => PossibleValue::new("left").help("Align source picture to left"), 18 | Alignment::Top => PossibleValue::new("top").help("Align source picture to top"), 19 | Alignment::Right => PossibleValue::new("right").help("Align source picture to right"), 20 | Alignment::Bottom => PossibleValue::new("bottom").help("Align source picture to bottom"), 21 | }) 22 | } 23 | } 24 | 25 | impl Display for Alignment { 26 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 27 | write!(f, "{:?}", self) 28 | } 29 | } 30 | 31 | impl Debug for Alignment { 32 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 33 | Display::fmt(self, f) 34 | } 35 | } 36 | 37 | impl std::str::FromStr for Alignment { 38 | type Err = String; 39 | 40 | fn from_str(s: &str) -> Result { 41 | for variant in Self::value_variants() { 42 | if variant.to_possible_value().unwrap().matches(s, false) { 43 | return Ok(*variant); 44 | } 45 | } 46 | Err(format!("invalid variant: {s}")) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/core/params/background.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Debug, Formatter}; 2 | use clap::builder::PossibleValue; 3 | use clap::ValueEnum; 4 | 5 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 6 | pub enum Background { 7 | Invisible, Start, End, Visible, 8 | } 9 | 10 | impl ValueEnum for Background { 11 | fn value_variants<'a>() -> &'a [Self] { 12 | &[Background::Invisible, Background::Start, Background::End, Background::Visible] 13 | } 14 | 15 | fn to_possible_value<'a>(&self) -> Option { 16 | Some(match self { 17 | Background::Invisible => PossibleValue::new("invisible").help("Keep transparent, white, unset, zero"), 18 | Background::Start => PossibleValue::new("start").help("Make visible on the left or top side"), 19 | Background::End => PossibleValue::new("end").help("Make visible on the right or bottom side"), 20 | Background::Visible => PossibleValue::new("visible").help("Make visible, black, set, unit"), 21 | }) 22 | } 23 | } 24 | 25 | impl Display for Background { 26 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 27 | write!(f, "{:?}", self) 28 | } 29 | } 30 | 31 | impl Debug for Background { 32 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 33 | Display::fmt(self, f) 34 | } 35 | } 36 | 37 | impl std::str::FromStr for Background { 38 | type Err = String; 39 | 40 | fn from_str(s: &str) -> core::result::Result { 41 | for variant in Self::value_variants() { 42 | if variant.to_possible_value().unwrap().matches(s, false) { 43 | return Ok(*variant); 44 | } 45 | } 46 | Err(format!("invalid variant: {s}")) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ext/path_ext.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | 4 | pub const EXT_PICTURE: [&str; 3] = ["png", "jpg", "jpeg"]; 5 | pub const EXT_BM: &str = "bm"; 6 | pub const EXT_PNG: &str = "png"; 7 | pub const EXT_GIF: &str = "gif"; 8 | 9 | pub trait PathExt { 10 | fn to_string(&self) -> String; 11 | fn get_path_name(&self) -> String; 12 | fn get_name_no_ext(&self) -> String; 13 | fn get_parent(&self) -> String; 14 | fn get_ext(&self) -> String; 15 | fn as_dir(&self) -> String; 16 | } 17 | 18 | impl PathExt for PathBuf { 19 | 20 | fn to_string(&self) -> String { 21 | String::from(self.to_str().unwrap()) 22 | } 23 | 24 | fn get_path_name(&self) -> String { 25 | let path = self.to_str().unwrap(); 26 | let ext = self.extension().unwrap(); 27 | return String::from(&path[..(path.len() - ext.len() - 1)]); 28 | } 29 | 30 | fn get_name_no_ext(&self) -> String { 31 | let ext = self.extension().unwrap(); 32 | let value = self.file_name().unwrap().to_str().unwrap(); 33 | String::from(&value[..(value.len() - ext.len() - 1)]) 34 | } 35 | 36 | fn get_parent(&self) -> String { 37 | if let Some("/") = self.to_str() { 38 | return String::new(); 39 | } 40 | let mut value = self.parent().unwrap().to_str().unwrap(); 41 | if value.is_empty() { 42 | value = "." 43 | } 44 | String::from(format!("{value}/")) 45 | } 46 | 47 | fn get_ext(&self) -> String { 48 | let value = self.extension().unwrap().to_str().unwrap(); 49 | String::from(value) 50 | } 51 | 52 | fn as_dir(&self) -> String { 53 | let path = self.to_string(); 54 | match path.ends_with('/') { 55 | true => path, 56 | _ => format!("{path}/"), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/core/meta.rs: -------------------------------------------------------------------------------- 1 | use image::Delay; 2 | use crate::ext::iter_ext::Sum; 3 | 4 | pub struct FrameData { 5 | pub index: usize, 6 | pub duration: f32, 7 | } 8 | 9 | impl FrameData { 10 | pub fn from(index: usize, delay: &Delay) -> FrameData { 11 | let (num, del) = delay.clone().numer_denom_ms(); 12 | FrameData { index, duration: num as f32 / del as f32 } 13 | } 14 | } 15 | 16 | pub fn get_meta(height: u8, frames: &Vec) -> String { 17 | let duration = frames.iter().sum_of(0f32, |it| it.duration) as usize; 18 | let min_dur = frames.iter() 19 | .min_by(|&f,&s| f.duration.partial_cmp(&s.duration).unwrap()) 20 | .unwrap() 21 | .duration; 22 | let mut order = Vec::::new(); 23 | for it in frames { 24 | let times = (it.duration / min_dur) as u32; 25 | for _ in 0..times { 26 | order.push(it.index); 27 | } 28 | } 29 | let frame_rate = (1000.0 / min_dur) as u32; 30 | let a_frames = frames.iter() 31 | .max_of(0, |it| it.index) + 1; 32 | let p_frames = order.len() - a_frames; 33 | let order = order.iter() 34 | .map(|it| it.to_string()) 35 | .collect::>() 36 | .join(" "); 37 | return format!("Filetype: Flipper Animation 38 | Version: 1 39 | 40 | Width: 128 41 | Height: {height} 42 | Passive frames: {p_frames} 43 | Active frames: {a_frames} 44 | Frames order: {order} 45 | Active cycles: 1 46 | Frame rate: {frame_rate} 47 | Duration: {duration} 48 | Active cooldown: 0 49 | 50 | Bubble slots: 0 51 | ") 52 | } 53 | 54 | pub fn get_manifest(with_header: bool, name: String) -> String { 55 | let header = if with_header { "Filetype: Flipper Animation Manifest\nVersion: 1" } else { "" }; 56 | return format!("{header} 57 | 58 | Name: {name} 59 | Min butthurt: 0 60 | Max butthurt: 13 61 | Min level: 1 62 | Max level: 3 63 | Weight: 8 64 | ") 65 | } 66 | -------------------------------------------------------------------------------- /src/unused.rs: -------------------------------------------------------------------------------- 1 | use image::{DynamicImage, GenericImage, Rgba}; 2 | use crate::core::color::Color; 3 | 4 | const BYTE_LIMIT: u16 = 256; 5 | 6 | pub struct Color { 7 | pub r: u8, 8 | pub g: u8, 9 | pub b: u8, 10 | } 11 | 12 | impl Color { 13 | pub fn parse(value: u32) -> Color { 14 | let limit = BYTE_LIMIT as u32; 15 | Color { 16 | r: (value / limit.pow(2) % limit) as u8, 17 | g: (value / limit % limit.pow(2)) as u8, 18 | b: (value % limit.pow(3)) as u8, 19 | } 20 | } 21 | } 22 | 23 | fn try_get_pixel(image: &T, x: i32, y: i32) -> Option 24 | where 25 | T: GenericImage 26 | { 27 | match () { 28 | _ if x < 0 => None, 29 | _ if y < 0 => None, 30 | _ if x >= image.width() as i32 => None, 31 | _ if y >= image.height() as i32 => None, 32 | _ => Some(image.get_pixel(x as u32, y as u32)) 33 | } 34 | } 35 | 36 | fn for_each(image: &T, action: F) 37 | where 38 | T: GenericImage, 39 | F: Fn(u32, u32, T::Pixel) /*+ Copy*/, 40 | { 41 | for y in 0..image.height() { 42 | for x in 0..image.width() { 43 | action(x, y, image.get_pixel(x, y)); 44 | } 45 | } 46 | } 47 | 48 | fn for_each_mut(image: &mut T, action: F) 49 | where 50 | T: GenericImage, 51 | F: Fn(&mut T, T::Pixel) -> Option /*+ Copy*/, 52 | { 53 | for y in 0..image.height() { 54 | for x in 0..image.width() { 55 | let new = action(image, image.get_pixel(x, y)); 56 | if let Some(pixel) = new { 57 | image.put_pixel(x, y, pixel) 58 | } 59 | } 60 | } 61 | } 62 | 63 | fn abs_dif(first: u8, second: u8) -> u8 { 64 | let mut dif = first as i16 - second as i16; 65 | dif *= dif.signum(); 66 | dif as u8 67 | } 68 | 69 | fn remove_background(image: &mut DynamicImage, color: Color, to_visible: bool) { 70 | let target: u8 = if to_visible { 0 } else { 255 }; 71 | for_each_mut(image, |image, pixel| { 72 | let r = pixel[0]; 73 | let g = pixel[1]; 74 | let b = pixel[2]; 75 | match () { 76 | _ if abs_dif(color.r, r) > 50 => None, 77 | _ if abs_dif(color.g, g) > 50 => None, 78 | _ if abs_dif(color.b, b) > 50 => None, 79 | _ if abs_dif(abs_dif(color.r, color.g), abs_dif(r, g)) > 30 => None, 80 | _ if abs_dif(abs_dif(color.g, color.b), abs_dif(g, b)) > 30 => None, 81 | _ => Some(Rgba([target, target, target, pixel.0[3]])) 82 | } 83 | }); 84 | } 85 | 86 | fn color_to_u32(color: &str) -> u32 { 87 | if color.len() != 6 && color.len() != 8 { 88 | panic!("Color must contains 6 or 8 0..f-chars ({})", color); 89 | } 90 | let mut color_int = u32::from_str_radix(color, 16).unwrap(); 91 | if color.len() == 6 { 92 | color_int += 0xff000000; 93 | } 94 | return color_int; 95 | } 96 | -------------------------------------------------------------------------------- /src/core/params/args.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::path::PathBuf; 3 | use clap::Parser; 4 | use crate::core::params::alignment::Alignment; 5 | use crate::core::params::background::Background; 6 | use crate::core::params::frame_cut::FrameCut; 7 | use crate::core::params::values::Values; 8 | use crate::core::params::scale_type::ScaleType; 9 | use crate::core::params::threshold::Threshold; 10 | 11 | #[derive(Debug, Parser)] 12 | 13 | #[command(name = "img2fbm")] 14 | #[command(author = "Nesterov Y. ")] 15 | #[command(version = "1.0")] 16 | #[command(about = "Flipper bitmap files generator", long_about = None)] 17 | #[command(arg_required_else_help = true)] 18 | pub struct Cli { 19 | /// Path to png|jpg|jpeg|gif file 20 | #[arg(value_name = "source")] 21 | pub source_path: PathBuf, 22 | 23 | /// Path to the 'dolphin' directory, if the GIF passed 24 | #[arg(value_name = "dolphin")] 25 | pub dolphin_path: Option, 26 | 27 | /// Sets the height of output frame(s) 28 | #[arg( 29 | required = false, 30 | short = 'H', 31 | long, 32 | value_name = "1-64", 33 | value_parser = clap::value_parser!(u8).range(1..=64), 34 | default_value_t = 64, 35 | )] 36 | pub height: u8, 37 | 38 | /// Scale type 39 | #[arg(long = "st", value_name = "type", default_value = "fit")] 40 | pub scale_type: ScaleType, 41 | 42 | /// Applied alignment if the source picture has an aspect ratio different from the target 43 | #[arg(short, long, value_name = "side", default_value = "bottom")] 44 | pub alignment: Alignment, 45 | 46 | /// Generate the previews of result pictures 47 | #[arg(short, long)] 48 | pub preview: bool, 49 | 50 | /// Only preview, do not generate .bm and other Flipper Animation files 51 | #[arg(long = "op")] 52 | pub only_preview: bool, 53 | 54 | /// Preview scale ratio 55 | #[arg(long = "ps", default_value_t = 3, value_name = "multiplier")] 56 | pub preview_scale: u8, 57 | 58 | /// Inverse output pixels 59 | #[arg(short, long)] 60 | pub inverse: bool, 61 | 62 | /// Replace a dolphin/manifest.txt file with a new one. 63 | #[arg(short, long)] 64 | pub replace_manifest: bool, 65 | 66 | /// Set background pixels visible 67 | #[arg(short, long, value_name = "background", default_value = "invisible")] 68 | pub background: Background, 69 | // thread 'main' has overflowed its stack 70 | // fatal runtime error: stack overflow 71 | // caused by default_value_t = Background::Invisible 72 | 73 | /// Threshold value or range of pixel brightness as a percentage, such as 20:80, 40:, :60, 50:50 or 50 74 | #[arg(short, long, value_name = "percentage[:percentage]", value_parser = str_to_threshold, default_value = "20:80")] 75 | pub threshold: Threshold, 76 | 77 | /// Animation speed ratio 78 | #[arg(short, long, value_name = "speed", default_value_t = 1.0, value_parser = str_to_speed)] 79 | pub speed: f32, 80 | 81 | /// Drop some frames from the start and from the end. For example, 5:, :8 or 2:3, the last one drops 2 frames from start and 3 from the end. 82 | #[arg(short, long, value_name = "count[:count]", value_parser = str_to_frame_cut, default_value = "0:0")] 83 | pub cut: FrameCut, 84 | } 85 | 86 | fn str_to_threshold(value: &str) -> Result { 87 | let from_to = Values::::from::(value, 0, 100)?; 88 | if from_to.first > from_to.second { 89 | panic!("The first value must be greater than the second value") 90 | } 91 | let dark = from_to.first as f32 / 100.0; 92 | let light = from_to.second as f32 / 100.0; 93 | return Ok(Threshold { dark, light }); 94 | } 95 | 96 | fn str_to_frame_cut(value: &str) -> Result { 97 | let from_to = Values::::from::(value, 0, 0)?; 98 | return Ok(FrameCut { start: from_to.first, end: from_to.second }); 99 | } 100 | 101 | fn str_to_speed(value: &str) -> Result { 102 | let value = value.parse::().map_err(|err| err.to_string())?; 103 | if value <= 0.0 { 104 | panic!("Invalid speed ratio must be greater than 0"); 105 | } 106 | return Ok(value); 107 | } 108 | -------------------------------------------------------------------------------- /src/core/params/params.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use clap::{CommandFactory, Parser}; 3 | use clap::Error; 4 | use clap::error::ErrorKind; 5 | use ErrorKind::InvalidValue; 6 | use crate::core::params::alignment::Alignment; 7 | use crate::core::params::args::Cli; 8 | use crate::core::params::background::Background; 9 | use crate::core::params::frame_cut::FrameCut; 10 | use crate::core::params::scale_type::ScaleType; 11 | use crate::core::params::threshold::Threshold; 12 | use crate::ext::path_ext::{PathExt, EXT_PNG, EXT_GIF, EXT_BM, EXT_PICTURE}; 13 | 14 | 15 | const TARGET_WIDTH: u8 = 128; 16 | 17 | pub enum FileType { 18 | Picture, Gif 19 | } 20 | 21 | pub struct Params { 22 | pub file_type: FileType, 23 | pub width: u8, 24 | pub height: u8, 25 | pub preview: bool, 26 | pub only_preview: bool, 27 | pub preview_scale: u32, 28 | pub inverse: bool, 29 | pub background: Background, 30 | pub threshold: Threshold, 31 | pub cut: FrameCut, 32 | pub scale_type: ScaleType, 33 | pub alignment: Alignment, 34 | pub speed: f32, 35 | pub with_manifest: bool, 36 | pub replace_manifest: bool, 37 | 38 | pub path_src: String, 39 | pub path_name: String, 40 | pub input_ext: String, 41 | pub preview_path_name: String, 42 | pub preview_picture_path: String, 43 | pub preview_gif_path: String, 44 | pub picture_path_bm: String, 45 | pub dolphin_path: String, 46 | pub dolphin_anim_name: String, 47 | pub dolphin_anim_path: String, 48 | pub meta_path: String, 49 | pub manifest_path: String, 50 | } 51 | 52 | impl Params { 53 | 54 | pub fn print_help() { 55 | Cli::command().print_help().unwrap(); 56 | } 57 | 58 | pub fn try_parse() -> Result { 59 | return Params::from(Cli::parse()); 60 | } 61 | 62 | pub fn try_parse_from(string: String) -> Result { 63 | let mut args = shell_words::split(string.as_str()).expect("wrong arguments format"); 64 | args.insert(0, "stub".to_string()); 65 | let cli = Cli::try_parse_from(args)?; 66 | return Params::from(cli); 67 | } 68 | 69 | pub fn from(cli: Cli) -> Result { 70 | cli.source_path.extension().ok_or(Error::raw(InvalidValue, "invalid input file"))?; 71 | cli.source_path.file_name().ok_or(Error::raw(InvalidValue, "invalid input file path"))?; 72 | let input_ext = cli.source_path.get_ext().to_lowercase(); 73 | let file_type = match () { 74 | _ if EXT_PICTURE.contains(&&*input_ext) => FileType::Picture, 75 | _ if input_ext == EXT_GIF => FileType::Gif, 76 | _ => return Err(Error::raw(InvalidValue, "invalid input file format")), 77 | }; 78 | let path_name = cli.source_path.get_path_name(); 79 | let preview_path_name = format!("{}_preview", cli.source_path.get_path_name()); 80 | let preview_picture_path = format!("{preview_path_name}.{EXT_PNG}"); 81 | let preview_gif_path = format!("{preview_path_name}.{EXT_GIF}"); 82 | let picture_path_bm = format!("{path_name}.{EXT_BM}"); 83 | let dolphin_path = cli.dolphin_path.clone() 84 | .map(|it| it.as_dir()) 85 | .unwrap_or_else(|| cli.source_path.get_parent()); 86 | let dolphin_anim_name = format!("{}_{TARGET_WIDTH}x{}", cli.source_path.get_name_no_ext(), cli.height); 87 | let dolphin_anim_path = format!("{dolphin_path}{dolphin_anim_name}/"); 88 | let meta_path = format!("{dolphin_anim_path}meta.txt"); 89 | let manifest_path = format!("{dolphin_path}manifest.txt"); 90 | let params = Params { 91 | file_type, 92 | width: TARGET_WIDTH, 93 | height: cli.height, 94 | preview: cli.preview || cli.only_preview, 95 | only_preview: cli.only_preview, 96 | preview_scale: cli.preview_scale as u32, 97 | inverse: cli.inverse, 98 | background: cli.background, 99 | threshold: cli.threshold, 100 | cut: cli.cut, 101 | scale_type: cli.scale_type, 102 | alignment: cli.alignment, 103 | speed: cli.speed, 104 | with_manifest: cli.dolphin_path.is_some(), 105 | replace_manifest: cli.replace_manifest, 106 | 107 | path_src: cli.source_path.to_string(), 108 | path_name, 109 | input_ext, 110 | preview_path_name, 111 | preview_picture_path, 112 | preview_gif_path, 113 | picture_path_bm, 114 | dolphin_path, 115 | dolphin_anim_name, 116 | dolphin_anim_path, 117 | meta_path, 118 | manifest_path, 119 | }; 120 | return Ok(params); 121 | } 122 | 123 | pub fn path_bm(&self, index: I) -> String where I: Display { 124 | format!("{}frame_{}.{EXT_BM}", self.dolphin_anim_path, index) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/core/img2bm.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | use image::{DynamicImage, GrayImage, RgbaImage}; 3 | use image::imageops::FilterType; 4 | use crate::core::params::background::Background; 5 | use crate::core::bitmap::Bitmap; 6 | use crate::core::params::alignment::Alignment; 7 | use crate::core::params::params::Params; 8 | use crate::core::params::scale_type::ScaleType; 9 | use crate::core::params::threshold::Threshold; 10 | use crate::ext::range_ext::for_each; 11 | use crate::ext::image_ext::Resizing; 12 | 13 | 14 | const MAX_RADIUS: f32 = 4.0; 15 | 16 | pub fn img2bm(image: &RgbaImage, params: &Params) -> Bitmap { 17 | let resized = resize(image, params).to_luma8(); 18 | let mut bitmap = create_bitmap(&resized, params); 19 | if params.threshold.dark > 0.0 { 20 | process_dark(params, &resized, &mut bitmap); 21 | } 22 | if !params.threshold.is_empty() { 23 | // todo replace with sorted pixels 24 | process(¶ms.threshold, &resized, &mut bitmap, 0.0..0.1); 25 | process(¶ms.threshold, &resized, &mut bitmap, 0.1..0.2); 26 | process(¶ms.threshold, &resized, &mut bitmap, 0.2..0.4); 27 | process(¶ms.threshold, &resized, &mut bitmap, 0.4..0.65); 28 | process(¶ms.threshold, &resized, &mut bitmap, 0.65..0.1); 29 | } 30 | if params.background != Background::Invisible { 31 | process_outside_and_inverting(&resized, &mut bitmap, params.background); 32 | } 33 | if params.inverse { 34 | bitmap.invert(); 35 | } 36 | return bitmap; 37 | } 38 | 39 | fn create_bitmap(image: &GrayImage, params: &Params) -> Bitmap { 40 | let mut dx = 0; 41 | if params.alignment != Alignment::Left { 42 | dx = image.width() as i32 - params.width as i32; 43 | } 44 | if params.alignment != Alignment::Right { 45 | dx = dx / 2 + dx % 2; 46 | } 47 | let mut dy = 0; 48 | if params.alignment != Alignment::Top { 49 | dy = image.height() as i32 - params.height as i32; 50 | } 51 | if params.alignment != Alignment::Bottom { 52 | dy = dy / 2 + dy % 2; 53 | } 54 | return Bitmap::new(params.width, params.height, dx, dy); 55 | } 56 | 57 | fn process_dark(params: &Params, resized: &GrayImage, bitmap: &mut Bitmap) { 58 | for_each_luminance(resized, bitmap, |bitmap, x, y, outside, luminance| { 59 | if !outside && luminance < params.threshold.dark { 60 | bitmap.set(x, y); 61 | } 62 | }); 63 | } 64 | 65 | fn process_outside_and_inverting( 66 | resized: &GrayImage, 67 | bitmap: &mut Bitmap, 68 | background: Background, 69 | ) { 70 | let height_dif = bitmap.height as u32 - resized.height(); 71 | let width_dif = bitmap.width as u32 - resized.width(); 72 | // if the source width or height is odd and alignment is centered, 73 | // number of outside pixels is above=below+1 and on the left=on the right+1 74 | let top_edge = height_dif / 2 + height_dif % 2; 75 | let bottom_edge = bitmap.height as u32 - height_dif / 2; 76 | let left_edge = width_dif / 2 + width_dif % 2; 77 | let right_edge = bitmap.width as u32 - width_dif / 2; 78 | for_each_luminance(resized, bitmap, |bitmap, x, y, outside, /*luminance*/_| { 79 | match () { 80 | _ if !outside => (), 81 | _ if background == Background::Visible => bitmap.set(x, y), 82 | _ if background == Background::Start && (x < left_edge || y < top_edge) => bitmap.set(x, y), 83 | _ if background == Background::End && (x >= right_edge || y >= bottom_edge) => bitmap.set(x, y), 84 | _ => (), 85 | } 86 | }); 87 | } 88 | 89 | fn process(threshold: &Threshold, resized: &GrayImage, bitmap: &mut Bitmap, range: Range) { 90 | for_each_luminance(resized, bitmap, |bitmap, x, y, outside, luminance| { 91 | if !outside && threshold.contains(luminance) { 92 | let luminance = (luminance - threshold.dark) / threshold.size(); 93 | if !range.contains(&luminance) { 94 | return; 95 | } 96 | let already_bit_nearby = find_in_radius(bitmap, luminance, x as i32, y as i32); 97 | if !already_bit_nearby { 98 | bitmap.set(x, y); 99 | } 100 | } 101 | }); 102 | } 103 | 104 | fn for_each_luminance( 105 | image: &GrayImage, 106 | bitmap: &mut Bitmap, 107 | mut action: F, 108 | ) where F: FnMut(&mut Bitmap, u32, u32, /*outside:*/bool, /*luminance:*/f32) { 109 | let width = bitmap.width; 110 | let height = bitmap.height; 111 | for_each(0..height as u32, 0..width as u32, |x,y| { 112 | if bitmap.get(x, y) { return; } 113 | let src_x = bitmap.get_src_x(x); 114 | let src_y = bitmap.get_src_y(y); 115 | if src_x < 0 || src_x >= image.width() as i32 || src_y < 0 || src_y >= image.height() as i32 { 116 | action(bitmap, x, y, true, 0.0); 117 | return; 118 | } 119 | let luminance = image.get_pixel(src_x as u32, src_y as u32).0[0] as f32 / 255.0; 120 | action(bitmap, x, y, false, luminance); 121 | }); 122 | } 123 | 124 | fn resize(image: &RgbaImage, params: &Params) -> DynamicImage { 125 | let dynamic = DynamicImage::from(image.clone()); 126 | let scale_type = match params.scale_type { 127 | ScaleType::Fill => true, 128 | ScaleType::Fit => false, 129 | }; 130 | return Resizing::resize(&dynamic, params.width as u32, params.height as u32, scale_type, FilterType::Nearest); 131 | } 132 | 133 | pub fn find_in_radius(bitmap: &Bitmap, luminance: f32, x: i32, y: i32) -> bool { 134 | let radius = luminance * MAX_RADIUS; 135 | let half = radius as i32; 136 | for dy in -half..half { 137 | for dx in -half..half { 138 | let x = x + dx; 139 | let y = y + dy; 140 | if x < 0 || y < 0 || x as u8 >= bitmap.width || y as u8 >= bitmap.height { 141 | continue; 142 | } else if !bitmap.get(x as u32, y as u32) { 143 | continue; 144 | } else if radius >= ((dx*dx + dy*dy) as f32).sqrt() { 145 | return true; 146 | } 147 | } 148 | } 149 | return false; 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # img2fbm 2 | Image to Flipper bitmap converter 3 | 4 | https://github.com/Atomofiron/img2fbm/assets/14147217/59cbb785-d17d-46e0-a8fe-b8a8210959ec 5 | 6 | # Functionality 7 | 8 |
9 | img2fbm --help 10 | 11 | ``` 12 | Flipper bitmap files generator 13 | 14 | Usage: img2fbm [OPTIONS] [dolphin] 15 | 16 | Arguments: 17 | 18 | Path to png|jpg|jpeg|gif file 19 | 20 | [dolphin] 21 | Path to the 'dolphin' directory, if the gif passed 22 | 23 | Options: 24 | -H, --height <1-64> 25 | Sets the height of output frame(s) 26 | 27 | [default: 64] 28 | 29 | --st 30 | Scale type 31 | 32 | [default: fit] 33 | 34 | Possible values: 35 | - fill: Scale to fill animation bounds 36 | - fit: Scale to fit in animation bounds 37 | 38 | -a, --alignment 39 | Applied alignment if the source picture has aspect ratio different from the target 40 | 41 | [default: bottom] 42 | 43 | Possible values: 44 | - left: Align source picture to left 45 | - top: Align source picture to top 46 | - right: Align source picture to right 47 | - bottom: Align source picture to bottom 48 | 49 | -p, --preview 50 | Generate the previews of result pictures 51 | 52 | --op 53 | Only preview, do not generate .bm and other Flipper Animation files 54 | 55 | --ps 56 | Preview scale ratio 57 | 58 | [default: 3] 59 | 60 | -i, --inverse 61 | Inverse output pixels 62 | 63 | -r, --replace-manifest 64 | Replace dolphin/manifest.txt file with a new one 65 | 66 | -b, --background 67 | Set background pixels visible 68 | 69 | [default: invisible] 70 | 71 | Possible values: 72 | - invisible: Keep transparent, white, unset, zero 73 | - start: Make visible on the left or top side 74 | - end: Make visible on the right or bottom side 75 | - visible: Make visible, black, set, unit 76 | 77 | -t, --threshold 78 | Threshold value or range of pixel brightness as a percentage, such as 20:80, 40:, :60, 50:50 or 50 79 | 80 | [default: 20:80] 81 | 82 | -s, --speed 83 | Animation speed ratio 84 | 85 | [default: 1] 86 | 87 | -c, --cut 88 | Drop some frames from the start and from the end. For example 5:, :8 or 2:3, the last one drops 2 frames from start and 3 from the end 89 | 90 | [default: 0:0] 91 | ``` 92 |
93 | 94 | # Download 95 | From [Releases](https://github.com/Atomofiron/img2fbm/releases) 96 |
:white_check_mark: MacOS x86_64 97 |
:white_check_mark: MacOS ARM 98 |
:white_check_mark: Linux x86_64 99 |
:zzz: Linux ARM 100 |
:white_check_mark: Windows x86_64 101 | 102 | # Samples 103 | yuno-eyes yuno-eyes_preview 104 |
105 | yuno-whisper 106 | yuno-whisper_preview 107 |
108 | yuno-shoot 109 | yuno-shoot_preview 110 |
111 | yuno-shadow 112 | yuno-shadow_preview 113 |
114 | yuno-run 115 | yuno-run_preview 116 |
117 | yuno-mysterious 118 | yuno-mysterious_preview 119 |
120 | yuno-knife 121 | yuno-knife_preview 122 |
123 | yuno-katana 124 | yuno-katana_preview 125 |
126 | yuno-heh 127 | yuno-heh_preview 128 |
129 | yuno-final 130 | yuno-final_preview 131 |
132 | yuno-fight 133 | yuno-fight_preview 134 |
135 | yuno-crying 136 | yuno-crying_preview 137 |
138 | yuno-confused 139 | yuno-confused_preview 140 |
141 | yuno-axe 142 | yuno-axe_preview 143 |
144 | yuno-afraid 145 | yuno-afraid_preview 146 |
147 | yuno-shooting 148 | yuno-shooting_preview 149 | 150 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod core; 2 | mod ext; 3 | 4 | use crate::core::bitmap::Bitmap; 5 | use crate::core::img2bm::img2bm; 6 | use crate::core::meta::{get_manifest, get_meta, FrameData}; 7 | use crate::core::params::params::{FileType, Params}; 8 | use crate::ext::unit_ext::UnitUtil; 9 | use image::codecs::gif::{GifDecoder, GifEncoder, Repeat}; 10 | use image::{AnimationDecoder, ColorType, Delay, DynamicImage, Frame, GrayImage, ImageFormat, Luma}; 11 | use indicatif::{ProgressBar, ProgressStyle}; 12 | use std::collections::hash_map::DefaultHasher; 13 | use std::fs; 14 | use std::fs::{create_dir_all, File, OpenOptions}; 15 | use std::hash::{Hash, Hasher}; 16 | use std::io::{stdin, BufReader, Write}; 17 | use std::path::Path; 18 | 19 | 20 | fn main() { 21 | if std::env::args().len() > 1 { 22 | work(Params::try_parse().unwrap()); 23 | } else { 24 | Params::print_help(); 25 | looped_work(); 26 | } 27 | } 28 | 29 | fn looped_work() { 30 | print!("input parameters or press Enter to exit: ").flush(); 31 | let mut line = String::new(); 32 | stdin().read_line(&mut line).unwrap(); 33 | if line.trim().len() > 1 { 34 | match Params::try_parse_from(line) { 35 | Ok(o) => { 36 | work(o); 37 | println!("it's done! another one?") 38 | }, 39 | Err(msg) => println!("{msg}"), 40 | }; 41 | looped_work() 42 | } 43 | } 44 | 45 | fn work(params: Params) { 46 | match params.file_type { 47 | FileType::Picture => from_picture(¶ms), 48 | FileType::Gif => from_gif(¶ms), 49 | } 50 | } 51 | 52 | fn from_picture(params: &Params) { 53 | let image = image::open(params.path_src.clone()).unwrap().to_rgba8(); 54 | let bitmap = img2bm(&image, ¶ms); 55 | 56 | if !params.only_preview { 57 | let mut file_dst = File::create(params.picture_path_bm.clone()).unwrap(); 58 | file_dst.write_all(bitmap.bytes.as_slice()).unwrap(); 59 | } 60 | if params.preview { 61 | let preview = bm2preview(&bitmap, params.preview_scale); 62 | save_preview(&preview, params.preview_picture_path.as_str()); 63 | } 64 | } 65 | 66 | fn new_progress(length: usize, prefix: &str) -> ProgressBar { 67 | let progressbar = ProgressBar::new(length as u64); 68 | progressbar.set_prefix(String::from(prefix)); 69 | progressbar.set_message("done"); 70 | let style = ProgressStyle::with_template("{spinner:.white} {prefix} {msg} [{bar:.white/white}] {pos}/{len}") 71 | .unwrap() 72 | .progress_chars("#>-") 73 | .tick_chars("-\\|/#"); 74 | progressbar.set_style(style); 75 | return progressbar; 76 | } 77 | 78 | fn from_gif(params: &Params) { 79 | let mut preview_frames = Vec::::new(); 80 | if !params.only_preview { 81 | create_dir_all(params.dolphin_anim_path.as_str()).unwrap(); 82 | } 83 | let file = File::open(params.path_src.clone()).unwrap(); 84 | let reader = BufReader::new(file); 85 | let decoder = GifDecoder::new(reader).unwrap(); 86 | let mut hashes = Vec::::new(); 87 | let mut data = Vec::::new(); 88 | let mut min_duration = -1f32; 89 | 90 | let frames = decoder.into_frames().collect_frames().unwrap(); 91 | let min_index = params.cut.start; 92 | let max_index = frames.len() - 1 - params.cut.end; 93 | let bar = new_progress(max_index + 1 - min_index, "Converting..."); 94 | let frames_iter = frames.into_iter() 95 | .enumerate() 96 | .filter(|&(i, _)| { i >= min_index && i <= max_index }) 97 | .map(|(_, it)| it); 98 | for frame in frames_iter { 99 | // todo use rayon 100 | let image = frame.buffer().to_owned(); 101 | let bitmap = img2bm(&image, ¶ms); 102 | 103 | let mut hasher = DefaultHasher::new(); 104 | bitmap.hash(&mut hasher); 105 | let hash = hasher.finish(); 106 | let index = hashes.iter().position(|&it| it == hash).unwrap_or_else(|| { 107 | let index = hashes.len(); 108 | if !params.only_preview { 109 | let mut file_dst = File::create(params.path_bm(index)).unwrap(); 110 | file_dst.write_all(bitmap.bytes.as_slice()).unwrap(); 111 | } 112 | hashes.push(hash); 113 | if params.preview { 114 | preview_frames.push(bm2preview(&bitmap, params.preview_scale)); 115 | } 116 | index 117 | }); 118 | let f_data = FrameData::from(index, &frame.delay()); 119 | if min_duration < 0.0 || f_data.duration < min_duration { 120 | min_duration = f_data.duration; 121 | } 122 | min_duration /= params.speed; 123 | data.push(f_data); 124 | bar.inc(1); 125 | } 126 | bar.finish(); 127 | 128 | for f_data in data.iter_mut() { 129 | f_data.duration = (f_data.duration / min_duration).round() * min_duration; 130 | } 131 | if !params.only_preview { 132 | let meta = get_meta(params.height, &data); 133 | fs::write(params.meta_path.clone(), meta).unwrap(); 134 | if params.with_manifest { 135 | write_manifest(¶ms); 136 | } 137 | } 138 | if params.preview { 139 | bm2preview_gif(¶ms, &data, &preview_frames) 140 | } 141 | } 142 | 143 | fn write_manifest(params: &Params) { 144 | let manifest_path = Path::new(params.manifest_path.as_str()); 145 | let with_header = params.replace_manifest || !manifest_path.exists(); 146 | let manifest_part = get_manifest(with_header, params.dolphin_anim_name.clone()); 147 | let mut manifest_file = OpenOptions::new() 148 | .create(true).write(true).append(!params.replace_manifest) 149 | .open(manifest_path) 150 | .unwrap(); 151 | manifest_file.write(manifest_part.as_bytes()).unwrap(); 152 | } 153 | 154 | fn bm2preview_gif(params: &Params, data: &Vec::, preview_frames: &Vec::) { 155 | let bar = new_progress(preview_frames.len(), "Generating preview..."); 156 | let mut frames = Vec::::new(); 157 | for fd in data { 158 | let image = preview_frames.get(fd.index).unwrap(); 159 | let dynamic = DynamicImage::from(image.clone()); 160 | let duration = (fd.duration / params.speed) as u32; 161 | let delay = Delay::from_numer_denom_ms(duration, 1); 162 | let frame = Frame::from_parts(dynamic.to_rgba8(), 0, 0, delay); 163 | frames.push(frame); 164 | bar.inc(1); 165 | } 166 | let preview_file = File::create(params.preview_gif_path.clone()).unwrap(); 167 | let mut encoder = GifEncoder::new(preview_file); 168 | encoder.set_repeat(Repeat::Infinite).unwrap(); 169 | encoder.encode_frames(frames.into_iter()).unwrap(); 170 | bar.finish(); 171 | } 172 | 173 | fn bm2preview(bitmap: &Bitmap, scale: u32) -> GrayImage { 174 | let width = bitmap.width as u32; 175 | let height = bitmap.height as u32; 176 | let mut image = GrayImage::new(width * scale, height * scale); 177 | for y in 0..height { 178 | for x in 0..width { 179 | // +1 because of the first byte is extra 0x00 180 | let bit = bitmap.get(x, y); 181 | if !bit { 182 | for x in (x * scale)..(x * scale + scale) { 183 | for y in (y * scale)..(y * scale + scale) { 184 | image.put_pixel(x, y, Luma([255u8])); 185 | } 186 | } 187 | } 188 | } 189 | } 190 | return image; 191 | } 192 | 193 | fn save_preview(img: &GrayImage, name: &str) { 194 | image::save_buffer_with_format( 195 | name, 196 | img, 197 | img.width(), 198 | img.height(), 199 | ColorType::L8, 200 | ImageFormat::Png, 201 | ).unwrap(); 202 | } 203 | --------------------------------------------------------------------------------