The official website for the open-source compatibility layer fpPS4
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implemented github parsing and images re-encoding and resizing

MrSn0wy 575e606e 599c997d

+423
backend/image.png

This is a binary file and will not be displayed.

backend/src/config.rs

This is a binary file and will not be displayed.

+138
backend/src/images.rs
··· 1 + use fast_image_resize::images::Image; 2 + use fast_image_resize::{FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer, SrcCropping}; 3 + use load_image::export::imgref::ImgVec; 4 + use ravif::{AlphaColorMode, ColorSpace, EncodedImage, Encoder, Img, RGBA8}; 5 + use load_image::ImageData; 6 + 7 + const RESIZE_OPTIONS: ResizeOptions = ResizeOptions { 8 + algorithm: ResizeAlg::Convolution(FilterType::Lanczos3), 9 + cropping: SrcCropping::None, 10 + mul_div_alpha: true, 11 + }; 12 + 13 + pub(crate) fn convert_image(image_data: Vec<u8>) -> anyhow::Result<Vec<u8>> { 14 + 15 + let quality: f32 = 60f32; 16 + let alpha_quality = ((quality + 100.)/2.).min(quality + quality/4. + 2.); 17 + let speed: u8 = 1; 18 + let color_space = ColorSpace::YCbCr; 19 + let depth = Some(10); 20 + 21 + let img = load_rgba(image_data)?; 22 + 23 + let cropped_image = crop_image(img)?; 24 + 25 + // let img: Img<Vec<u8>> = Img::new(cropped_image, 128,128); 26 + 27 + let rgba_pixels: Vec<RGBA8> = cropped_image 28 + .chunks(4) 29 + .map(|chunk| RGBA8 { 30 + r: chunk[0], 31 + g: chunk[1], 32 + b: chunk[2], 33 + a: chunk[3], 34 + }) 35 + .collect(); 36 + 37 + let img = Img::new(rgba_pixels, 128,128); 38 + // let img = load_rgba(cropped_image)?; 39 + 40 + // let img_vec = bytemuck::cast_slice(&img.clone().into_buf()); 41 + // fs::write("./cropped.png", dst_image.buffer())?; 42 + // let resized_img = load_rgba(dst_image.buffer())?; 43 + 44 + 45 + let enc = Encoder::new() 46 + .with_quality(quality) 47 + .with_depth(depth) 48 + .with_speed(speed) 49 + .with_alpha_quality(alpha_quality) 50 + .with_internal_color_space(color_space) 51 + .with_alpha_color_mode(AlphaColorMode::UnassociatedClean) 52 + .with_num_threads(None); 53 + 54 + let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref())?; 55 + 56 + Ok(avif_file) 57 + } 58 + 59 + fn load_rgba(data: Vec<u8>) -> anyhow::Result<ImgVec<RGBA8>> { 60 + use rgb::prelude::*; 61 + 62 + let img = load_image::load_data(&data)?.into_imgvec(); 63 + 64 + let img = match img { 65 + load_image::export::imgref::ImgVecKind::RGB8(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.with_alpha(255)).collect()), 66 + load_image::export::imgref::ImgVecKind::RGBA8(img) => img, 67 + load_image::export::imgref::ImgVecKind::RGB16(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.map(|c| (c >> 8) as u8).with_alpha(255)).collect()), 68 + load_image::export::imgref::ImgVecKind::RGBA16(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.map(|c| (c >> 8) as u8)).collect()), 69 + load_image::export::imgref::ImgVecKind::GRAY8(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = g.0; RGBA8::new(c,c,c,255) }).collect()), 70 + load_image::export::imgref::ImgVecKind::GRAY16(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = (g.0>>8) as u8; RGBA8::new(c,c,c,255) }).collect()), 71 + load_image::export::imgref::ImgVecKind::GRAYA8(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = g.0; RGBA8::new(c,c,c,g.1) }).collect()), 72 + load_image::export::imgref::ImgVecKind::GRAYA16(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = (g.0>>8) as u8; RGBA8::new(c,c,c,(g.1>>8) as u8) }).collect()), 73 + }; 74 + 75 + Ok(img) 76 + } 77 + fn load_rgba_test() -> anyhow::Result<(Vec<RGBA8>, usize, usize)> { 78 + let image = load_image::load_path("./image.png")?; 79 + 80 + let width = image.width; 81 + let height = image.height; 82 + 83 + 84 + let img = match image.bitmap { 85 + ImageData::RGBA8(img) => img, 86 + ImageData::RGB8(img) => img.iter().map(|buf| buf.with_alpha(255)).collect(), 87 + _ => todo!() 88 + }; 89 + 90 + Ok((img,width,height)) 91 + } 92 + 93 + 94 + fn crop_image(image_data: ImgVec<RGBA8>) -> anyhow::Result<Vec<u8>> { 95 + 96 + let width: u32 = image_data.width() as u32; 97 + let height: u32 = image_data.height() as u32; 98 + 99 + let img_bytes: Vec<u8> = image_data.into_buf().iter().flat_map(|pixel| vec![pixel.r, pixel.g, pixel.b, pixel.a]).collect(); 100 + 101 + let src_image = Image::from_vec_u8( 102 + width, 103 + height, 104 + img_bytes, 105 + PixelType::U8x4, // RGBA8 106 + )?; 107 + 108 + 109 + let mut cropped_image = Image::new( 110 + 128, 111 + 128, 112 + PixelType::U8x4, // RGBA8 113 + ); 114 + 115 + let mut resizer = Resizer::new(); 116 + resizer.resize(&src_image, &mut cropped_image, &RESIZE_OPTIONS)?; 117 + 118 + // let file = File::create("./cropped.png")?; 119 + // let ref mut w = BufWriter::new(file); 120 + // let mut encoder = PngEncoder::new(w, 128u32, 128u32); 121 + // encoder.set_color(png::ColorType::Rgba); 122 + // encoder.set_depth(png::BitDepth::Eight); 123 + // 124 + // let mut writer = encoder.write_header()?; 125 + // 126 + // // Write the image data (RGBA) 127 + // writer.write_image_data(cropped_image.buffer())?; 128 + 129 + 130 + // let (image, width, height) = load_rgba_test().unwrap(); 131 + // 132 + // let raw_pixels: &[u8] = bytemuck::cast_slice(&image); 133 + // 134 + // assert_eq!(raw_pixels.len(), (width * height * 4) as usize, "Invalid buffer size!"); 135 + // 136 + 137 + Ok(cropped_image.into_vec()) 138 + }
+29
backend/src/macros.rs
··· 1 + // purely cosmetic 2 + #[macro_export] 3 + macro_rules! panic_red { 4 + ($($arg:tt)*) => {{ 5 + eprintln!("\x1b[31;1m{}\x1b[0m", format!($($arg)*)); 6 + std::process::exit(1); 7 + }}; 8 + } 9 + 10 + #[macro_export] 11 + macro_rules! eprintln_red { 12 + ($($arg:tt)*) => {{ 13 + eprintln!("\x1b[31;1m{}\x1b[0m", format!($($arg)*)); 14 + }}; 15 + } 16 + 17 + #[macro_export] 18 + macro_rules! println_green { 19 + ($($arg:tt)*) => {{ 20 + println!("\x1b[32m{}\x1b[0m", format!($($arg)*)); 21 + }}; 22 + } 23 + 24 + #[macro_export] 25 + macro_rules! println_cyan { 26 + ($($arg:tt)*) => {{ 27 + println!("\x1b[36;1m{}\x1b[0m", format!($($arg)*)); 28 + }}; 29 + }
+26
backend/src/networking.rs
··· 1 + use serde_json::Value; 2 + use crate::panic_red; 3 + 4 + fn github_request(url: &str, token: &str) -> Value { 5 + match ureq::get(url) 6 + .set("User-Agent", "fpps4.net") 7 + .set("Accept", "application/vnd.github+json") 8 + .set("Authorization", &format!("Bearer {}", token)) 9 + .call() 10 + { 11 + Ok(response) => response.into_json().expect("Failed to parse JSON"), 12 + Err(response) => panic_red!("Github request failed: {}", response), 13 + } 14 + } 15 + 16 + // fn image_downloader(url: String, user_ffagent: &str, location: String) -> Result<(), anyhow::Error> { 17 + // let response = ureq::get(&url).set("User-Agent", user_agent).call()?; 18 + // let mut reader = response.into_reader(); 19 + // let mut buffer = Vec::new(); 20 + // reader.read_to_end(&mut buffer)?; 21 + // 22 + // let img = image::load_from_memory(&buffer)?; 23 + // let resized_img = img.resize_exact(256, 256, image::imageops::Lanczos3); 24 + // resized_img.save_with_format(location, image::ImageFormat::Avif)?; 25 + // Ok(()) 26 + // }
+230
backend/src/parsing.rs
··· 1 + use anyhow::{bail, Context}; 2 + use regex::Regex; 3 + use serde::{Deserialize, Serialize}; 4 + use serde_json::Value; 5 + 6 + struct Issues { 7 + issues: Vec<Issue>, 8 + } 9 + 10 + // #[derive(Serialize, Deserialize)] 11 + struct Issue { 12 + id: u64, 13 + code: String, 14 + title: String, 15 + labels: Vec<String>, 16 + status: Status, 17 + issue_type: GameType, 18 + created: String, 19 + updated: String, 20 + // image: bool, 21 + } 22 + 23 + // #[derive(Serialize, Deserialize)] 24 + enum GameType { 25 + Game, 26 + Homebrew, 27 + Ps2game, 28 + SystemFw505, 29 + SystemFwUnknown, 30 + } 31 + 32 + // #[derive(Serialize, Deserialize)] 33 + enum Status { 34 + Playable, 35 + Ingame, 36 + Menus, 37 + Boots, 38 + Nothing, 39 + } 40 + 41 + fn parse_github_issue(issue: Value, code_regex: Regex) -> anyhow::Result<(Issue, Vec<String>)> { 42 + let mut warning: Vec<String> = vec![]; 43 + 44 + let id: u64 = issue 45 + .get("number") 46 + .and_then(Value::as_u64) 47 + .context("Failed to parse id")?; 48 + 49 + let code: String = { 50 + let title: &str = issue 51 + .get("title") 52 + .and_then(Value::as_str) 53 + .context("Failed to parse title")?; 54 + 55 + if code_regex.captures(&title).iter().len() != 1 { 56 + warning.push(String::from("More than one title id has been matched")); // add a warning 57 + } 58 + 59 + code_regex 60 + .find(&title) 61 + .map(|x| x.as_str().to_uppercase()) 62 + .context("Failed to get code using regex")? 63 + }; 64 + 65 + let title: String = { 66 + let mut title: String = issue 67 + .get("title") 68 + .and_then(Value::as_str) 69 + .context("Failed to parse title")? 70 + .to_string(); 71 + 72 + title = title.replace(&code, ""); 73 + 74 + if title.contains("(Homebrew)") 75 + || title.contains("- HOMEBREW") 76 + || title.contains("Homebrew") 77 + || title.contains("[]") 78 + { 79 + warning.push(String::from("Title not correctly formatted")); // add a warning 80 + 81 + title = title.replace("(Homebrew)", ""); 82 + title = title.replace("- HOMEBREW", ""); 83 + title = title.replace("Homebrew", ""); 84 + title = title.replace("[]", ""); 85 + } 86 + 87 + title 88 + }; 89 + 90 + let mut labels: Vec<&str> = issue 91 + .get("labels") 92 + .and_then(Value::as_array) 93 + .context("Failed to get labels")? 94 + .iter() 95 + .map(|label| label 96 + .get("name") 97 + .and_then(Value::as_str) 98 + .unwrap_or_default()) 99 + .collect::<Vec<&str>>(); 100 + 101 + 102 + let status: Status = { 103 + 104 + // I agree that the .retain is a bit silly, but I want to be able to give a warning when an issue has more than one "status-" label 105 + let status = match labels.iter().find(|label| label.starts_with("status-")) { 106 + Some(label) => match *label { 107 + "status-playable" => { 108 + labels.retain(|label| label != &"status-playable"); 109 + Status::Playable 110 + } 111 + "status-ingame" => { 112 + labels.retain(|label| label != &"status-ingame"); 113 + Status::Ingame 114 + } 115 + "status-menus" => { 116 + labels.retain(|label| label != &"status-menus"); 117 + Status::Menus 118 + } 119 + "status-boots" => { 120 + labels.retain(|label| label != &"status-boots"); 121 + Status::Boots 122 + } 123 + "status-nothing" => { 124 + labels.retain(|label| label != &"status-nothing"); 125 + Status::Nothing 126 + } 127 + _ => bail!("No status label found or recognized"), 128 + } 129 + None => bail!("No status label found or recognized"), 130 + }; 131 + 132 + // clean labels 133 + if labels.contains(&"status-") 134 + { 135 + warning.push(String::from("More than one status label")); // add a warning 136 + labels = labels.into_iter().filter(|x| !x.starts_with("status-")).collect(); 137 + } 138 + 139 + status 140 + }; 141 + 142 + 143 + let issue_type: GameType = { 144 + let issue_type = match labels.iter().find(|label| label.starts_with("app-")) { 145 + Some(label) => match *label { 146 + "app-system-fw505" => { 147 + labels.retain(|label| *label != "app-system-fw505"); 148 + GameType::SystemFw505 149 + } 150 + "app-system-fw_unknown" => { 151 + labels.retain(|label| *label != "app-system-fw_unknown"); 152 + GameType::SystemFwUnknown 153 + } 154 + "app-ps2game" => { 155 + labels.retain(|label| *label != "app-ps2game"); 156 + GameType::Ps2game 157 + } 158 + "app-homebrew" => { 159 + labels.retain(|label| *label != "app-homebrew"); 160 + GameType::Homebrew 161 + } 162 + 163 + _ => 164 + if code.starts_with("CUSA") 165 + || code.starts_with("PCJS") 166 + || code.starts_with("PLJM") 167 + || code.starts_with("PLJS") 168 + { 169 + GameType::Game 170 + } else { 171 + bail!("couldn't determine the game-type") 172 + } 173 + 174 + } 175 + 176 + None => 177 + if code.starts_with("CUSA") 178 + || code.starts_with("PCJS") 179 + || code.starts_with("PLJM") 180 + || code.starts_with("PLJS") 181 + { 182 + GameType::Game 183 + } else { 184 + bail!("couldn't determine the game-type") 185 + } 186 + 187 + }; 188 + 189 + // clean labels 190 + if labels.contains(&"app-system-fw505") 191 + || labels.contains(&"app-system-fw_unknown") 192 + || labels.contains(&"app-ps2game") 193 + || labels.contains(&"app-homebrew") 194 + { 195 + warning.push(String::from("More than one app label")); // add a warning 196 + labels = labels.into_iter().filter(|x| !x.starts_with("app-")).collect(); 197 + } 198 + 199 + issue_type 200 + }; 201 + 202 + // RFC3339 String 203 + let created: String = issue 204 + .get("created_at") 205 + .and_then(Value::as_str) 206 + .context("Failed to parse updated_at")? 207 + .to_string(); 208 + 209 + let updated: String = issue 210 + .get("updated_at") 211 + .and_then(Value::as_str) 212 + .context("Failed to parse updated_at")? 213 + .to_string(); 214 + 215 + let new_issue: Issue = Issue { 216 + id, 217 + code, 218 + title, 219 + labels: labels 220 + .iter_mut() 221 + .map(|label| label.to_string()) 222 + .collect(), 223 + status, 224 + issue_type, 225 + created, 226 + updated, 227 + }; 228 + 229 + Ok((new_issue, warning)) 230 + }