// Copyright (C) 2022-2022 Fuwn // SPDX-License-Identifier: MIT #![allow(clippy::cast_sign_loss)] use { byteorder::{LittleEndian, ReadBytesExt}, chrono::{DateTime, NaiveDateTime, TimeZone, Utc}, std::{ collections::HashMap, fmt::Write, fs, io::{Cursor, Read}, ops::Coroutine, sync::OnceLock, }, }; /// Flipnote speed -> frames per second fn framerates() -> &'static HashMap { static FRAMERATES: OnceLock> = OnceLock::new(); FRAMERATES.get_or_init(|| { let mut hashmap = HashMap::new(); hashmap.insert(1, 0.5); hashmap.insert(2, 1.0); hashmap.insert(3, 2.0); hashmap.insert(4, 4.0); hashmap.insert(5, 6.0); hashmap.insert(6, 12.0); hashmap.insert(7, 20.0); hashmap.insert(8, 30.0); hashmap }) } /// Thumbnail bitmap RGB colours // const THUMBNAIL_PALETTE: &'static [(u64, u64, u64)] = &[ // (0xFF, 0xFF, 0xFF), // (0x52, 0x52, 0x52), // (0xFF, 0xFF, 0xFF), // (0x9C, 0x9C, 0x9C), // (0xFF, 0x48, 0x44), // (0xC8, 0x51, 0x4F), // (0xFF, 0xAD, 0xAC), // (0x00, 0xFF, 0x00), // (0x48, 0x40, 0xFF), // (0x51, 0x4F, 0xB8), // (0xAD, 0xAB, 0xFF), // (0x00, 0xFF, 0x00), // (0xB6, 0x57, 0xB7), // (0x00, 0xFF, 0x00), // (0x00, 0xFF, 0x00), // (0x00, 0xFF, 0x00), // ]; // Frame RGB colours const BLACK: (u8, u8, u8) = (0x0E, 0x0E, 0x0E); const WHITE: (u8, u8, u8) = (0xFF, 0xFF, 0xFF); const BLUE: (u8, u8, u8) = (0x0A, 0x39, 0xFF); const RED: (u8, u8, u8) = (0xFF, 0x2A, 0x2A); macro read_n_to_as_utf8_from_stream($n:expr, $from:ident) { String::from_utf8({ let mut buffer = vec![0; $n]; $from.stream.read_exact(&mut buffer).unwrap(); buffer }) .unwrap() } macro read_n_of_size_from_to_vec($n:expr, $from:tt, $size:ty) {{ let mut buffer = vec![0 as $size; $n]; $from.stream.read_exact(&mut buffer).unwrap(); buffer }} fn strip_null(string: &str) -> String { string.replace(char::from(0), "") } fn read_n_to_vec(stream: &mut Cursor>, n: usize) -> Vec { let mut buffer = vec![0; n]; stream.read_exact(&mut buffer).unwrap(); buffer } fn vec_u8_to_string(vec: &[u8]) -> String { vec.iter().rev().fold(String::new(), |mut output, m| { let _ = write!(output, "{m:02X}"); output }) } pub struct PPMParser { stream: Cursor>, layers: Vec>>, prev_layers: Vec>>, prev_frame_index: usize, animation_data_size: u32, sound_data_size: u32, frame_count: u16, lock: u16, thumb_index: u16, root_author_name: String, parent_author_name: String, current_author_name: String, parent_author_id: String, current_author_id: String, parent_filename: String, current_filename: String, root_author_id: String, partial_filename: String, timestamp: DateTime, layer_1_visible: bool, layer_2_visible: bool, loop_: bool, frame_speed: u8, bgm_speed: u8, framerate: f64, bgm_framerate: f64, offset_table: Vec, } impl PPMParser { #[allow(unused)] pub fn new(stream: Vec) -> Self { Self { stream: Cursor::new(stream), ..Self::default() } } pub fn new_from_file(file: &str) -> Self { Self { stream: Cursor::new(std::fs::read(file).unwrap()), ..Self::default() } } pub fn load(&mut self) { self.read_header(); self.read_meta(); self.read_animation_header(); self.read_sound_header(); self.layers = vec![vec![vec![0; 256]; 192]; 2]; self.prev_layers = vec![vec![vec![0; 256]; 192]; 2]; self.prev_frame_index = isize::MAX as usize; // -1 } /// Decode header /// /// fn read_header(&mut self) { self.stream.set_position(0); let _magic = read_n_to_as_utf8_from_stream!(4, self); let animation_data_size = self.stream.read_u32::().unwrap(); let sound_data_size = self.stream.read_u32::().unwrap(); let frame_count = self.stream.read_u16::().unwrap(); let _version = self.stream.read_u16::().unwrap(); self.animation_data_size = animation_data_size; self.sound_data_size = sound_data_size; self.frame_count = frame_count + 1; } fn read_filename(&mut self) -> String { // Parent and current filenames are stored as: // // - three bytes representing the last six digits of the console's MAC // address // - thirteen-character `String` // - `u16` edit counter let mac = read_n_to_vec(&mut self.stream, 3); let ident = read_n_to_vec(&mut self.stream, 13) .into_iter() .map(|c| c as char) .collect::(); let edits = self.stream.read_u16::().unwrap(); // Filenames are formatted as // __ // // Example: F78DA8_14768882B56B8_030 format!( "{}_{}_{:#03}", mac.iter().fold(String::new(), |mut output, m| { let _ = write!(output, "{m:02X}"); output }), String::from_utf8(ident.as_bytes().to_vec()).unwrap(), edits, ) } /// Decode metadata /// /// fn read_meta(&mut self) { self.stream.set_position(0x10); self.lock = self.stream.read_u16::().unwrap(); self.thumb_index = self.stream.read_u16::().unwrap(); self.root_author_name = strip_null(&read_n_to_as_utf8_from_stream!(22, self)); self.parent_author_name = strip_null(&read_n_to_as_utf8_from_stream!(22, self)); self.current_author_name = strip_null(&read_n_to_as_utf8_from_stream!(22, self)); self.parent_author_id = vec_u8_to_string(read_n_to_vec(&mut self.stream, 8).as_mut_slice()); self.current_author_id = vec_u8_to_string(read_n_to_vec(&mut self.stream, 8).as_mut_slice()); self.parent_filename = self.read_filename(); self.current_filename = self.read_filename(); self.root_author_id = vec_u8_to_string(read_n_to_vec(&mut self.stream, 8).as_mut_slice()); self.partial_filename = vec_u8_to_string(read_n_to_vec(&mut self.stream, 8).as_slice()); // Not really useful for anything // Timestamp is stored as the number of seconds since 2000, January, 1st let timestamp = self.stream.read_u32::().unwrap(); self.timestamp = TimeZone::from_utc_datetime( // We add 946684800 to convert this to a more common Unix timestamp, // which starts on 1970, January, 1st &Utc, #[allow(deprecated)] &NaiveDateTime::from_timestamp(i64::from(timestamp) + 946_684_800, 0), ); } #[allow(unused)] fn read_thumbnail(&mut self) -> Vec> { self.stream.set_position(0xA0); let mut bitmap = vec![vec![0; 64]; 48]; for tile_index in 0..48 { let tile_x = tile_index % 8 * 8; let tile_y = tile_index / 8 * 8; for line in 0..8 { // [This](https://linuxtut.com/en/ff1ac20b39137f1ccdb9/) can be used, // but let's do it in Rust. for pixel in (0..8).step_by(2) { let byte = self.stream.read_uint::(1).unwrap(); let x = tile_x + pixel; let y = tile_y + line; bitmap[y][x] = byte & 0x0F; bitmap[y][x + 1] = (byte >> 4) & 0x0F; } } } bitmap } fn read_animation_header(&mut self) { self.stream.set_position(0x06A0); let table_size = self.stream.read_u16::().unwrap(); let _unknown = self.stream.read_u16::().unwrap(); let flags = self.stream.read_u32::().unwrap(); // Unpack animation flags self.layer_1_visible = (flags >> 11) & 0x01 != 0; self.layer_2_visible = (flags >> 10) & 0x01 != 0; self.loop_ = (flags >> 1) & 0x01 != 0; // Read offset table into an array let offset_table = { let from_buffer = read_n_to_vec(&mut self.stream, table_size.into()); let mut buffer = Vec::with_capacity((table_size / 4).into()); // I'm very glad that I got this working. It took way longer than it // should have... // // 2022. 02. 25. 03:58., Fuwn for index in (0..usize::from(table_size)).step_by(4) { buffer.push( (u32::from(from_buffer[index])) | (u32::from(from_buffer[index + 1]) << 8) | (u32::from(from_buffer[index + 2]) << 16) | (u32::from(from_buffer[index + 3]) << 24), ); } buffer }; self.offset_table = offset_table .into_iter() .map(|m| m + 0x06A0 + 8 + u32::from(table_size)) .collect(); } fn read_sound_header(&mut self) { // offset = frame data offset + frame data length + sound effect flags // // let mut offset = 0x06A0 + self.animation_data_size + u32::from(self.frame_count); if offset % 2 != 0 { // Account for multiple-of-four padding offset += 4 - (offset % 4); } self.stream.set_position(u64::from(offset)); let _bgm_size = self.stream.read_u32::().unwrap(); let _se1_size = self.stream.read_u32::().unwrap(); let _se2_size = self.stream.read_u32::().unwrap(); let _se3_size = self.stream.read_u32::().unwrap(); let frame_speed = self.stream.read_u8().unwrap(); let bgm_speed = self.stream.read_u8().unwrap(); self.frame_speed = 8 - frame_speed; self.bgm_speed = 8 - bgm_speed; self.framerate = *framerates().get(&self.frame_speed).unwrap(); self.bgm_framerate = *framerates().get(&self.bgm_speed).unwrap(); } fn frame_is_new(&mut self, index: usize) -> bool { self.stream.set_position(u64::from(*self.offset_table.get(index).unwrap())); self.stream.read_uint::(1).unwrap() >> 7 & 0x1 != 0 } fn read_line_types( line_types: &[u8], ) -> impl Coroutine + '_ { #[coroutine] move || { for index in 0..192 { let line_type = line_types.get(index / 4).unwrap() >> ((index % 4) * 2) & 0x03; yield (index, line_type); } } } fn read_frame(&mut self, index: usize) -> &Vec>> { // Decode the previous frames if needed if index != 0 && self.prev_frame_index != index - 1 && !self.frame_is_new(index) { self.read_frame(index - 1); } // Copy the current layer buffers to the previous ones self.prev_layers.clone_from_slice(&self.layers); self.prev_frame_index = index; // Clear the current layer buffers by resetting them to zero self.layers.fill(vec![vec![0u8; 256]; 192]); // Seek to the frame offset so we can start reading self.stream.set_position(u64::from(*self.offset_table.get(index).unwrap())); // Unpack frame header flags let header = self.stream.read_uint::(1).unwrap(); let is_new_frame = (header >> 7) & 0x01 != 0; let is_translated = (header >> 5) & 0x03 != 0; // If the frame is translated, we need to unpack the x and y values let translation_x = if is_translated { self.stream.read_i8().unwrap() } else { 0 }; let translation_y = if is_translated { self.stream.read_i8().unwrap() } else { 0 }; // Read line encoding bytes let line_types = [ read_n_of_size_from_to_vec!(48, self, u8), read_n_of_size_from_to_vec!(48, self, u8), ]; // Loop through layers #[allow(clippy::needless_range_loop)] for layer in 0..2 { let bitmap = &mut self.layers[layer]; { let mut generator = Self::read_line_types(&line_types[layer]); while let std::ops::CoroutineState::Yielded((line, line_type)) = std::pin::Pin::new(&mut generator).resume(()) { let mut pixel = 0; // No data stored for this line if line_type == 0 { // pass; } else if line_type == 1 || line_type == 2 { // Compressed line // If `line_type == 2`, the line starts off with all the pixels set // to one if line_type == 2 { for i in 0..256 { bitmap[line][i] = 1; } } // Unpack chunk usage let mut chunk_usage = self.stream.read_u32::().unwrap(); // Unpack pixel chunks while pixel < 256 { if chunk_usage & 0x8000_0000 == 0 { pixel += 8; } else { let chunk = self.stream.read_uint::(1).unwrap(); for bit in 0..8 { bitmap[line][pixel] = (chunk >> bit & 0x1) as u8; pixel += 1; } } chunk_usage <<= 1; } // Raw line } else if line_type == 3 { // Unpack pixel chunks while pixel < 256 { let chunk = self.stream.read_uint::(1).unwrap(); for bit in 0..8 { bitmap[line][pixel] = (chunk >> bit & 0x1) as u8; pixel += 1; } } } } } } // Frame diffing // // If the current frame is based on the previous one, merge them by XOR-ing // their pixels. This is a big performance bottleneck... if !is_new_frame { // Loop through lines for y in 0..192 { // Skip to next line if this one falls off the top edge of the screen // if y - (translation_y as usize) < 0 { // continue; // } // Stop once the bottom screen edge has been reached if y - translation_y as usize >= 192 { break; } for x in 0..256 { // Skip to the next pixel if this one falls off the left edge of the // screen if x - (translation_x as usize) < 0 { // continue; // } // Stop diffing this line once the right screen edge has been reached if x - translation_x as usize >= 256 { break; } // Diff pixels with a binary XOR self.layers[0][y][x] ^= self.prev_layers[0] [y - translation_y as usize][x - translation_x as usize]; self.layers[1][y][x] ^= self.prev_layers[1] [y - translation_y as usize][x - translation_x as usize]; } } } &self.layers } pub fn get_frame_palette(&mut self, index: usize) -> Vec<(u8, u8, u8)> { self.stream.set_position(self.offset_table[index].into()); let header = self.stream.read_uint::(1).unwrap(); let paper_colour = header & 0x1; let pen = [ None, Some(if paper_colour == 1 { BLACK } else { WHITE }), Some(RED), Some(BLUE), ]; vec![ if paper_colour == 1 { WHITE } else { BLACK }, pen.get(((header >> 1) & 0x3) as usize).unwrap().unwrap(), /* Layer one colour */ pen.get(((header >> 3) & 0x3) as usize).unwrap().unwrap(), /* Layer two colour */ ] } pub fn get_frame_pixels(&mut self, index: usize) -> Vec> { let layers = self.read_frame(index); let mut pixels = vec![vec![0u8; 256]; 192]; #[allow(clippy::needless_range_loop)] for y in 0..192 { for x in 0..256 { if layers[0][y][x] > 0 { pixels[y][x] = 1; } else if layers[1][y][x] > 0 { pixels[y][x] = 2; } } } pixels } pub const fn get_frame_count(&self) -> u16 { self.frame_count } pub const fn get_thumb_index(&self) -> u16 { self.thumb_index } pub const fn get_framerate(&self) -> f64 { self.framerate } pub fn dump_to_json(&self, filename: &str) { let writer = std::io::BufWriter::new(fs::File::create(filename).unwrap()); serde_json::to_writer_pretty( writer, &serde_json::json!({ "animation_data_size": self.animation_data_size, "sound_data_size": self.sound_data_size, "frame_count": self.frame_count, "lock": self.lock, "thumb_index": self.thumb_index, "root_author_name": self.root_author_name, "parent_author_name": self.parent_author_name, "current_author_name": self.current_author_name, "root_author_id": self.root_author_id, "parent_author_id": self.parent_author_id, "current_author_id": self.current_author_id, "parent_filename": self.parent_filename, "current_filename": self.current_filename, "partial_filename": self.partial_filename, "timestamp": self.timestamp.to_string(), "layer_1_visible": self.layer_1_visible, "layer_2_visible": self.layer_2_visible, "loop": self.loop_, "frame_speed": self.frame_speed, "bgm_speed": self.bgm_speed, "framerate": self.framerate, "bgm_framerate": self.bgm_framerate, }), ) .unwrap(); } } impl Default for PPMParser { fn default() -> Self { Self { stream: Cursor::default(), layers: Vec::new(), prev_layers: Vec::new(), prev_frame_index: Default::default(), animation_data_size: Default::default(), sound_data_size: Default::default(), frame_count: Default::default(), lock: Default::default(), thumb_index: Default::default(), root_author_name: String::default(), parent_author_name: String::default(), current_author_name: String::default(), parent_author_id: String::default(), current_author_id: String::default(), parent_filename: String::default(), current_filename: String::default(), root_author_id: String::default(), partial_filename: String::default(), #[allow(deprecated)] timestamp: TimeZone::from_utc_datetime( &Utc, &NaiveDateTime::from_timestamp(0, 0), ), layer_1_visible: Default::default(), layer_2_visible: Default::default(), loop_: Default::default(), frame_speed: Default::default(), bgm_speed: Default::default(), framerate: Default::default(), bgm_framerate: Default::default(), offset_table: Vec::default(), } } }