2024-01-07 23:52:49 +07:00
|
|
|
use crate::helper;
|
2024-01-07 19:19:40 +07:00
|
|
|
use crate::tesseract::{libtesseract, subprocess};
|
2024-01-05 22:41:44 +07:00
|
|
|
use crate::CONFIG;
|
2024-01-05 00:40:57 +07:00
|
|
|
use image::imageops::colorops::contrast_in_place;
|
2023-12-30 21:39:44 +07:00
|
|
|
use image::io::Reader as ImageReader;
|
2024-01-10 01:24:22 +07:00
|
|
|
use image::{DynamicImage, GenericImage, GenericImageView, ImageBuffer, ImageFormat, Rgba};
|
2024-01-07 23:52:49 +07:00
|
|
|
use serenity::all::Context;
|
2023-12-30 21:39:44 +07:00
|
|
|
use serenity::model::channel::Message;
|
|
|
|
|
use std::io::Cursor;
|
2024-01-06 14:34:45 +07:00
|
|
|
use swordfish_common::database::katana as db;
|
2024-01-08 20:56:02 +07:00
|
|
|
use swordfish_common::structs::{Character, DroppedCard};
|
2024-01-07 23:52:49 +07:00
|
|
|
use swordfish_common::{error, trace, warn};
|
2024-01-05 22:41:44 +07:00
|
|
|
use tokio::task;
|
2024-01-07 23:52:49 +07:00
|
|
|
use tokio::time::Instant;
|
2023-12-30 21:39:44 +07:00
|
|
|
|
2024-01-12 18:14:50 +07:00
|
|
|
const ALLOWED_CHARS: [char; 13] = [
|
|
|
|
|
' ', '-', '.', '!', ':', '(', ')', '\'', '/', '\'', '@', '&', '_',
|
|
|
|
|
];
|
2024-01-07 01:05:01 +07:00
|
|
|
const CARD_NAME_X_OFFSET: u32 = 22;
|
|
|
|
|
const CARD_NAME_Y_OFFSET: u32 = 28;
|
|
|
|
|
const CARD_NAME_WIDTH: u32 = 202 - CARD_NAME_X_OFFSET;
|
|
|
|
|
const CARD_NAME_HEIGHT: u32 = 70 - CARD_NAME_Y_OFFSET;
|
|
|
|
|
const CARD_SERIES_X_OFFSET: u32 = 22;
|
2024-01-10 06:30:37 +07:00
|
|
|
const CARD_SERIES_Y_OFFSET: u32 = 278;
|
2024-01-12 18:14:50 +07:00
|
|
|
const CARD_SERIES_WIDTH: u32 = 206 - CARD_SERIES_X_OFFSET;
|
2024-01-07 01:05:01 +07:00
|
|
|
const CARD_SERIES_HEIGHT: u32 = 330 - CARD_SERIES_Y_OFFSET;
|
2024-01-01 20:15:51 +07:00
|
|
|
|
2024-01-05 00:40:57 +07:00
|
|
|
fn replace_string(text: &mut String, from: &str, to: &str) -> bool {
|
|
|
|
|
match text.find(from) {
|
|
|
|
|
Some(i) => {
|
|
|
|
|
text.replace_range(i..i + from.len(), to);
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
None => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fix_tesseract_string(text: &mut String) {
|
|
|
|
|
// Remove the \n
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
if text.ends_with("\n") {
|
|
|
|
|
text.pop();
|
|
|
|
|
}
|
|
|
|
|
// Workaround for a bug the text
|
|
|
|
|
// e.g. "We Never Learn\nN" -> "We Never Learn"
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
if text.ends_with("\nN") {
|
2024-01-10 01:24:22 +07:00
|
|
|
text.truncate(text.len() - 2);
|
2024-01-05 00:40:57 +07:00
|
|
|
}
|
|
|
|
|
// Replace first (to prevent "byte index 13 is not a char boundary; it is inside '—' (bytes 11..14)")
|
|
|
|
|
while replace_string(text, "—", "-") {
|
|
|
|
|
trace!("Replacing '—' with '-'");
|
|
|
|
|
}
|
|
|
|
|
// Workaround for a bug the text
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
if text.starts_with("- ") || text.starts_with("-.") {
|
2024-01-10 01:24:22 +07:00
|
|
|
text.drain(0..2);
|
2024-01-05 00:40:57 +07:00
|
|
|
}
|
2024-01-07 18:15:51 +07:00
|
|
|
// Remove the first character if it is not alphanumeric
|
2024-01-10 01:24:22 +07:00
|
|
|
if !text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
2024-01-07 18:15:51 +07:00
|
|
|
text.remove(0);
|
|
|
|
|
}
|
2024-01-05 00:40:57 +07:00
|
|
|
// Workaround IR -> Ik
|
|
|
|
|
// Maybe it only occurs if Ik is in the start of the string?
|
|
|
|
|
// e.g. "IReda" -> "Ikeda"
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
replace_string(text, "IR", "Ik");
|
|
|
|
|
// Workaround for "A\n"
|
|
|
|
|
// This is usually the corner of the card
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
replace_string(text, "A\n", "");
|
2024-01-05 22:07:04 +07:00
|
|
|
// Workaround for '“NO'
|
|
|
|
|
// This is usually the left bottom corner of the card
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
if text.ends_with(r##"“NO"##) {
|
2024-01-10 12:31:29 +07:00
|
|
|
text.drain(text.len() - 4..text.len());
|
2024-01-05 22:07:04 +07:00
|
|
|
}
|
2024-01-05 00:40:57 +07:00
|
|
|
// Workaround for "\n." (and others in the future)
|
2024-01-10 01:24:22 +07:00
|
|
|
let text_clone = text.clone();
|
|
|
|
|
let mut clone_chars = text_clone.chars();
|
|
|
|
|
for (i, c) in clone_chars.clone().enumerate() {
|
2024-01-05 00:40:57 +07:00
|
|
|
if c != '\n' {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2024-01-10 01:24:22 +07:00
|
|
|
let prev_char = match clone_chars.nth(i - 1) {
|
2024-01-05 00:40:57 +07:00
|
|
|
Some(c) => c,
|
|
|
|
|
None => continue,
|
|
|
|
|
};
|
2024-01-05 20:18:54 +07:00
|
|
|
let mut rm_prev: i8 = 0;
|
2024-01-05 00:40:57 +07:00
|
|
|
trace!("Prev char: {}", prev_char);
|
|
|
|
|
if ['-'].contains(&prev_char) {
|
2024-01-05 20:18:54 +07:00
|
|
|
rm_prev = 1;
|
2024-01-05 00:40:57 +07:00
|
|
|
text.remove(i - 1);
|
|
|
|
|
}
|
2024-01-05 20:18:54 +07:00
|
|
|
// Fix for "Asobi ni Iku lo Asobi ni Oide" -> "Asobi ni Iku yo! Asobi ni Oide"
|
|
|
|
|
if prev_char == 'l' {
|
2024-01-10 01:24:22 +07:00
|
|
|
let prev_prev_char = match clone_chars.nth(i - 2) {
|
2024-01-05 20:18:54 +07:00
|
|
|
Some(c) => c,
|
|
|
|
|
None => continue,
|
|
|
|
|
};
|
|
|
|
|
trace!("Prev prev char: {}", prev_prev_char);
|
|
|
|
|
if prev_prev_char == 'o' {
|
|
|
|
|
rm_prev = -1;
|
2024-01-10 12:31:29 +07:00
|
|
|
text.drain(i - 3..i - 1);
|
2024-01-05 20:18:54 +07:00
|
|
|
text.insert_str(i - 2, "yo!")
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-10 01:24:22 +07:00
|
|
|
let next_char = match clone_chars.nth(i + 1) {
|
2024-01-05 20:18:54 +07:00
|
|
|
Some(c) => c,
|
|
|
|
|
None => break,
|
|
|
|
|
};
|
2024-01-05 00:40:57 +07:00
|
|
|
trace!("Next char: {}", next_char);
|
|
|
|
|
if ['.'].contains(&next_char) {
|
2024-01-05 20:18:54 +07:00
|
|
|
text.remove((i as i8 + 1 - rm_prev) as usize);
|
2024-01-05 00:40:57 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Replace "\n" with " "
|
|
|
|
|
trace!("Text: {}", text);
|
2024-01-07 20:59:49 +07:00
|
|
|
while replace_string(text, "\n", " ") {
|
|
|
|
|
trace!("Replacing '\\n' with ' '");
|
|
|
|
|
}
|
2024-01-05 00:40:57 +07:00
|
|
|
// Remove all non-alphanumeric characters
|
|
|
|
|
trace!("Text: {}", text);
|
2024-01-10 01:24:22 +07:00
|
|
|
text.retain(|c| ALLOWED_CHARS.contains(&c) || c.is_ascii_alphanumeric());
|
2024-01-05 00:40:57 +07:00
|
|
|
// Fix "mn" -> "III"
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
if text.ends_with("mn") {
|
|
|
|
|
text.pop();
|
|
|
|
|
text.pop();
|
|
|
|
|
text.push_str("III");
|
|
|
|
|
}
|
|
|
|
|
// Fix "1ll" -> "III"
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
replace_string(text, "1ll", "III");
|
2024-01-05 22:07:04 +07:00
|
|
|
// Fix "lll" -> "!!!"
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
replace_string(text, "lll", "!!!");
|
2024-01-08 00:00:56 +07:00
|
|
|
// Fix "Il" -> "II" in the end of the string
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
if text.ends_with("Il") {
|
|
|
|
|
text.pop();
|
|
|
|
|
text.pop();
|
|
|
|
|
text.push_str("II");
|
|
|
|
|
}
|
2024-01-05 00:40:57 +07:00
|
|
|
// Replace multiple spaces with one space
|
|
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
while replace_string(text, " ", " ") {
|
|
|
|
|
trace!("Removing multiple spaces");
|
|
|
|
|
}
|
2024-01-06 21:16:56 +07:00
|
|
|
// Remove the last character if it is a dash
|
|
|
|
|
if text.ends_with("-") {
|
|
|
|
|
text.pop();
|
|
|
|
|
}
|
2024-01-05 00:40:57 +07:00
|
|
|
// Workaround if the first character is a space
|
|
|
|
|
trace!("Text: {}", text);
|
2024-01-10 06:20:32 +07:00
|
|
|
while text.starts_with(|c: char| c.is_whitespace()) {
|
2024-01-05 00:40:57 +07:00
|
|
|
trace!("Removing leading space");
|
2024-01-10 06:20:32 +07:00
|
|
|
text.remove(0);
|
2024-01-05 00:40:57 +07:00
|
|
|
}
|
2024-01-05 22:41:44 +07:00
|
|
|
// Workaround if the last character is a space
|
2024-01-10 06:20:32 +07:00
|
|
|
trace!("Text: {}", text);
|
|
|
|
|
while text.ends_with(|c: char| c.is_whitespace()) {
|
2024-01-05 22:41:44 +07:00
|
|
|
trace!("Removing ending space");
|
2024-01-10 06:20:32 +07:00
|
|
|
text.pop();
|
2024-01-05 22:41:44 +07:00
|
|
|
}
|
2024-01-05 00:40:57 +07:00
|
|
|
trace!("Text (final): {}", text);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-10 12:31:29 +07:00
|
|
|
fn regexify_text(text: &String) -> String {
|
2024-01-12 18:14:50 +07:00
|
|
|
let partial_match: bool;
|
|
|
|
|
if text.len() > 23 {
|
|
|
|
|
partial_match = true;
|
|
|
|
|
} else {
|
|
|
|
|
partial_match = false;
|
|
|
|
|
}
|
2024-01-10 12:31:29 +07:00
|
|
|
let mut regex = String::new();
|
|
|
|
|
let mut ascii_text = String::new();
|
2024-01-10 22:59:34 +07:00
|
|
|
let mut prev_chars: Vec<char> = Vec::new();
|
2024-01-10 12:31:29 +07:00
|
|
|
for c in text.chars() {
|
|
|
|
|
// Here comes the workaround...
|
|
|
|
|
// The character "0" is sometimes used in place of "O" in names
|
|
|
|
|
if ['0', 'O'].contains(&c) {
|
|
|
|
|
ascii_text.push_str("[0O]");
|
2024-01-10 22:59:34 +07:00
|
|
|
} else if ['u', 'v'].contains(&c) && prev_chars.len() > 0 {
|
|
|
|
|
let prev_char = prev_chars[prev_chars.len() - 1];
|
|
|
|
|
if ['u', 'v'].contains(&prev_char) {
|
|
|
|
|
ascii_text.pop();
|
|
|
|
|
ascii_text.push_str("[uv][uv]");
|
|
|
|
|
} else {
|
|
|
|
|
ascii_text.push(c);
|
|
|
|
|
}
|
|
|
|
|
} else if ['t'].contains(&c) {
|
|
|
|
|
ascii_text.push_str("[ti]");
|
2024-01-12 18:14:50 +07:00
|
|
|
} else if ['I', 'l', '!', '1'].contains(&c) {
|
|
|
|
|
ascii_text.push_str("[Il!1i]");
|
2024-01-10 22:59:34 +07:00
|
|
|
} else if ['.'].contains(&c) {
|
2024-01-11 22:09:18 +07:00
|
|
|
if prev_chars.len() > 3 {
|
|
|
|
|
let prev_char = prev_chars[prev_chars.len() - 1];
|
|
|
|
|
let prev_prev_char = prev_chars[prev_chars.len() - 2];
|
|
|
|
|
if prev_char.is_numeric() && prev_prev_char.is_whitespace() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2024-01-10 22:59:34 +07:00
|
|
|
}
|
2024-01-12 18:14:50 +07:00
|
|
|
ascii_text.push(' ');
|
2024-01-10 23:05:32 +07:00
|
|
|
} else if ['R'].contains(&c) {
|
|
|
|
|
ascii_text.push_str("[Rk]");
|
2024-01-10 12:31:29 +07:00
|
|
|
} else if c.is_ascii_alphanumeric() {
|
|
|
|
|
ascii_text.push(c);
|
|
|
|
|
} else {
|
|
|
|
|
ascii_text.push(' ');
|
|
|
|
|
}
|
2024-01-10 22:59:34 +07:00
|
|
|
prev_chars.push(c);
|
|
|
|
|
}
|
|
|
|
|
let split = ascii_text.split_whitespace();
|
|
|
|
|
let len = split.clone().count();
|
2024-01-12 18:14:50 +07:00
|
|
|
trace!("Partial match: {}", partial_match);
|
2024-01-10 22:59:34 +07:00
|
|
|
for (i, word) in split.enumerate() {
|
|
|
|
|
if word.len() < 2 && i > 0 && i < len - 1
|
|
|
|
|
|| (word.len() == 1 && word.to_ascii_uppercase() == word)
|
|
|
|
|
{
|
2024-01-10 15:32:30 +07:00
|
|
|
continue;
|
|
|
|
|
}
|
2024-01-10 12:31:29 +07:00
|
|
|
regex.push_str("(?=.*\\b");
|
2024-01-12 18:14:50 +07:00
|
|
|
let processed_word = word.to_lowercase();
|
|
|
|
|
if partial_match && processed_word.len() > 2 {
|
|
|
|
|
regex.push_str(&processed_word[2..(word.len() - 2)]);
|
|
|
|
|
} else {
|
|
|
|
|
regex.push_str(&processed_word.as_str());
|
|
|
|
|
}
|
2024-01-10 12:31:29 +07:00
|
|
|
regex.push_str("\\b)");
|
2024-01-10 15:32:30 +07:00
|
|
|
}
|
2024-01-10 12:31:29 +07:00
|
|
|
regex.push_str(".+");
|
|
|
|
|
trace!("Regex: {}", regex);
|
|
|
|
|
regex
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-10 01:24:22 +07:00
|
|
|
fn save_image_if_trace(img: &DynamicImage, path: &str) {
|
2024-01-05 20:18:54 +07:00
|
|
|
let log_lvl = CONFIG.get().unwrap().log.level.as_str();
|
2024-01-05 00:40:57 +07:00
|
|
|
if log_lvl == "trace" {
|
|
|
|
|
match img.save(path) {
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
trace!("Saved image to {}", path);
|
|
|
|
|
}
|
|
|
|
|
Err(why) => {
|
|
|
|
|
warn!("{}", format!("Failed to save image: {:?}", why))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2024-01-01 20:15:51 +07:00
|
|
|
}
|
|
|
|
|
|
2024-01-10 01:24:22 +07:00
|
|
|
fn image_with_white_padding(im: DynamicImage) -> DynamicImage {
|
|
|
|
|
// Partially copied from https://github.com/PureSci/nori/blob/main/rust-workers/src/drop.rs#L102C1-L121C6
|
|
|
|
|
let mut new_im: DynamicImage =
|
|
|
|
|
ImageBuffer::<Rgba<u8>, Vec<u8>>::new(im.width() + 14, im.height() + 14).into();
|
|
|
|
|
let white = Rgba([255, 255, 255, 255]);
|
|
|
|
|
for y in 0..im.height() {
|
|
|
|
|
for x in 0..im.width() {
|
|
|
|
|
let p = im.get_pixel(x, y);
|
|
|
|
|
new_im.put_pixel(x + 7, y + 7, p.to_owned());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for y in 0..7 {
|
|
|
|
|
for x in 0..im.width() + 14 {
|
|
|
|
|
new_im.put_pixel(x, y, white);
|
|
|
|
|
new_im.put_pixel(x, y + im.height() + 7, white);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for x in 0..7 {
|
|
|
|
|
for y in 7..im.height() + 7 {
|
|
|
|
|
new_im.put_pixel(x, y, white);
|
|
|
|
|
new_im.put_pixel(x + im.width() + 7, y, white);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
new_im
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 22:09:18 +07:00
|
|
|
pub async fn analyze_card_libtesseract(
|
|
|
|
|
card: image::DynamicImage,
|
|
|
|
|
count: u32,
|
|
|
|
|
) -> Result<DroppedCard, String> {
|
2024-01-01 20:15:51 +07:00
|
|
|
trace!("Spawning threads for analyzing card...");
|
2023-12-31 22:55:32 +07:00
|
|
|
// Read the name and the series
|
2024-01-01 20:15:51 +07:00
|
|
|
let card_clone = card.clone();
|
2024-01-10 01:24:22 +07:00
|
|
|
let name_thread = task::spawn_blocking(move || {
|
2024-01-07 22:38:51 +07:00
|
|
|
// let mut leptess =
|
|
|
|
|
// libtesseract::init_tesseract(false).expect("Failed to initialize Tesseract");
|
2024-01-10 01:24:22 +07:00
|
|
|
let binding = unsafe { libtesseract::get_tesseract() };
|
2024-01-07 22:38:51 +07:00
|
|
|
let mut leptess = binding.lock().unwrap();
|
2024-01-10 01:24:22 +07:00
|
|
|
let name_img = image_with_white_padding(card_clone.crop_imm(
|
2024-01-07 01:05:01 +07:00
|
|
|
CARD_NAME_X_OFFSET,
|
|
|
|
|
CARD_NAME_Y_OFFSET,
|
|
|
|
|
CARD_NAME_WIDTH,
|
|
|
|
|
CARD_NAME_HEIGHT,
|
2024-01-10 01:24:22 +07:00
|
|
|
));
|
2024-01-05 00:40:57 +07:00
|
|
|
let mut buffer: Cursor<Vec<u8>> = Cursor::new(Vec::new());
|
|
|
|
|
match name_img.write_to(&mut buffer, ImageFormat::Png) {
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
Err(why) => {
|
|
|
|
|
panic!("{}", format!("Failed to write image: {:?}", why));
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-10 01:24:22 +07:00
|
|
|
save_image_if_trace(
|
|
|
|
|
&name_img,
|
|
|
|
|
format!("debug/4-libtesseract-{}-name.png", count).as_str(),
|
|
|
|
|
);
|
2024-01-05 00:40:57 +07:00
|
|
|
leptess.set_image_from_mem(&buffer.get_mut()).unwrap();
|
|
|
|
|
let mut name_str = leptess.get_utf8_text().expect("Failed to read name");
|
|
|
|
|
fix_tesseract_string(&mut name_str);
|
|
|
|
|
name_str
|
2024-01-01 20:15:51 +07:00
|
|
|
});
|
|
|
|
|
let card_clone = card.clone();
|
2024-01-10 01:24:22 +07:00
|
|
|
let series_thread = task::spawn_blocking(move || {
|
2024-01-07 22:38:51 +07:00
|
|
|
// let mut leptess =
|
|
|
|
|
// libtesseract::init_tesseract(false).expect("Failed to initialize Tesseract");
|
2024-01-10 01:24:22 +07:00
|
|
|
let binding = unsafe { libtesseract::get_tesseract() };
|
2024-01-07 22:38:51 +07:00
|
|
|
let mut leptess = binding.lock().unwrap();
|
2024-01-10 01:24:22 +07:00
|
|
|
let series_img = image_with_white_padding(card_clone.crop_imm(
|
2024-01-07 01:05:01 +07:00
|
|
|
CARD_SERIES_X_OFFSET,
|
|
|
|
|
CARD_SERIES_Y_OFFSET,
|
|
|
|
|
CARD_SERIES_WIDTH,
|
|
|
|
|
CARD_SERIES_HEIGHT,
|
2024-01-10 01:24:22 +07:00
|
|
|
));
|
2024-01-05 00:40:57 +07:00
|
|
|
let mut buffer: Cursor<Vec<u8>> = Cursor::new(Vec::new());
|
|
|
|
|
match series_img.write_to(&mut buffer, ImageFormat::Png) {
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
Err(why) => {
|
|
|
|
|
panic!("{}", format!("Failed to write image: {:?}", why));
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-05 01:28:58 +07:00
|
|
|
save_image_if_trace(
|
|
|
|
|
&series_img,
|
2024-01-10 01:24:22 +07:00
|
|
|
format!("debug/4-libtesseract-{}-series.png", count).as_str(),
|
2024-01-05 01:28:58 +07:00
|
|
|
);
|
2024-01-05 00:40:57 +07:00
|
|
|
leptess.set_image_from_mem(&buffer.get_mut()).unwrap();
|
2024-01-10 01:24:22 +07:00
|
|
|
let mut series_str = leptess.get_utf8_text().expect("Failed to read series");
|
2024-01-05 00:40:57 +07:00
|
|
|
fix_tesseract_string(&mut series_str);
|
|
|
|
|
series_str
|
2024-01-01 20:15:51 +07:00
|
|
|
});
|
2024-01-11 22:09:18 +07:00
|
|
|
let name = match name_thread.await {
|
|
|
|
|
Ok(name) => name,
|
|
|
|
|
Err(why) => {
|
|
|
|
|
return Err(format!("Failed to read name: {:?}", why));
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-01 20:15:51 +07:00
|
|
|
trace!("Name: {}", name);
|
2024-01-11 22:09:18 +07:00
|
|
|
let series = match series_thread.await {
|
|
|
|
|
Ok(series) => series,
|
|
|
|
|
Err(why) => {
|
|
|
|
|
return Err(format!("Failed to read series: {:?}", why));
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-05 00:40:57 +07:00
|
|
|
trace!("Series: {}", series);
|
|
|
|
|
// TODO: Read the print number
|
2024-01-08 20:56:02 +07:00
|
|
|
let mut character = Character {
|
2024-01-06 15:30:11 +07:00
|
|
|
wishlist: None,
|
2024-01-05 00:40:57 +07:00
|
|
|
name,
|
|
|
|
|
series,
|
2024-01-06 15:30:11 +07:00
|
|
|
last_update_ts: 0,
|
2024-01-05 00:40:57 +07:00
|
|
|
};
|
2024-01-06 15:30:11 +07:00
|
|
|
// Read the wishlist number
|
2024-01-08 20:56:02 +07:00
|
|
|
match db::query_character(&character.name, &character.series).await {
|
2024-01-06 15:30:11 +07:00
|
|
|
Some(c) => {
|
2024-01-08 20:56:02 +07:00
|
|
|
character = c;
|
2024-01-06 15:30:11 +07:00
|
|
|
}
|
2024-01-10 12:31:29 +07:00
|
|
|
None => match db::query_character_regex(
|
|
|
|
|
®exify_text(&character.name),
|
|
|
|
|
®exify_text(&character.series),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2024-01-08 19:42:32 +07:00
|
|
|
Some(c) => {
|
2024-01-08 20:56:02 +07:00
|
|
|
character = c;
|
2024-01-08 12:44:04 +07:00
|
|
|
}
|
2024-01-08 19:42:32 +07:00
|
|
|
None => {}
|
|
|
|
|
},
|
2024-01-06 15:30:11 +07:00
|
|
|
}
|
2024-01-11 22:09:18 +07:00
|
|
|
Ok(DroppedCard {
|
2024-01-08 20:56:02 +07:00
|
|
|
character,
|
|
|
|
|
print: 0,
|
|
|
|
|
edition: 0,
|
2024-01-11 22:09:18 +07:00
|
|
|
})
|
2023-12-31 22:55:32 +07:00
|
|
|
}
|
|
|
|
|
|
2024-01-11 22:09:18 +07:00
|
|
|
pub async fn analyze_card_subprocess(
|
|
|
|
|
card: image::DynamicImage,
|
|
|
|
|
count: u32,
|
|
|
|
|
) -> Result<DroppedCard, String> {
|
2024-01-05 20:18:54 +07:00
|
|
|
trace!("Spawning threads for analyzing card...");
|
|
|
|
|
// Read the name and the series
|
|
|
|
|
let card_clone = card.clone();
|
2024-01-05 22:41:44 +07:00
|
|
|
let name_thread = task::spawn_blocking(move || {
|
2024-01-10 01:24:22 +07:00
|
|
|
let name_img = image_with_white_padding(card_clone.crop_imm(
|
2024-01-07 01:05:01 +07:00
|
|
|
CARD_NAME_X_OFFSET,
|
|
|
|
|
CARD_NAME_Y_OFFSET,
|
|
|
|
|
CARD_NAME_WIDTH,
|
|
|
|
|
CARD_NAME_HEIGHT,
|
2024-01-10 01:24:22 +07:00
|
|
|
));
|
2024-01-05 20:18:54 +07:00
|
|
|
let img = subprocess::Image::from_dynamic_image(&name_img).unwrap();
|
|
|
|
|
save_image_if_trace(
|
|
|
|
|
&name_img,
|
|
|
|
|
format!("debug/4-subprocess-{}-name.png", count).as_str(),
|
|
|
|
|
);
|
|
|
|
|
let mut name_str = subprocess::image_to_string(&img).unwrap();
|
|
|
|
|
fix_tesseract_string(&mut name_str);
|
|
|
|
|
name_str
|
|
|
|
|
});
|
|
|
|
|
let card_clone = card.clone();
|
2024-01-05 22:41:44 +07:00
|
|
|
let series_thread = task::spawn_blocking(move || {
|
2024-01-10 01:24:22 +07:00
|
|
|
let series_img = image_with_white_padding(card_clone.crop_imm(
|
2024-01-07 01:05:01 +07:00
|
|
|
CARD_SERIES_X_OFFSET,
|
|
|
|
|
CARD_SERIES_Y_OFFSET,
|
|
|
|
|
CARD_SERIES_WIDTH,
|
|
|
|
|
CARD_SERIES_HEIGHT,
|
2024-01-10 01:24:22 +07:00
|
|
|
));
|
2024-01-05 20:18:54 +07:00
|
|
|
let img = subprocess::Image::from_dynamic_image(&series_img).unwrap();
|
|
|
|
|
save_image_if_trace(
|
|
|
|
|
&series_img,
|
|
|
|
|
format!("debug/4-subprocess-{}-series.png", count).as_str(),
|
|
|
|
|
);
|
|
|
|
|
let mut series_str = subprocess::image_to_string(&img).unwrap();
|
|
|
|
|
fix_tesseract_string(&mut series_str);
|
|
|
|
|
series_str
|
|
|
|
|
});
|
2024-01-11 22:09:18 +07:00
|
|
|
let name = match name_thread.await {
|
|
|
|
|
Ok(name) => name,
|
|
|
|
|
Err(why) => {
|
|
|
|
|
return Err(format!("Failed to read name: {:?}", why));
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-05 20:18:54 +07:00
|
|
|
trace!("Name: {}", name);
|
2024-01-11 22:09:18 +07:00
|
|
|
let series = match series_thread.await {
|
|
|
|
|
Ok(series) => series,
|
|
|
|
|
Err(why) => {
|
|
|
|
|
return Err(format!("Failed to read series: {:?}", why));
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-05 20:18:54 +07:00
|
|
|
// TODO: Read the print number
|
2024-01-08 20:56:02 +07:00
|
|
|
let mut character = Character {
|
2024-01-06 15:30:11 +07:00
|
|
|
wishlist: None,
|
2024-01-05 20:18:54 +07:00
|
|
|
name,
|
|
|
|
|
series,
|
2024-01-06 15:30:11 +07:00
|
|
|
last_update_ts: 0,
|
2024-01-05 20:18:54 +07:00
|
|
|
};
|
2024-01-06 15:30:11 +07:00
|
|
|
// Read the wishlist number
|
2024-01-08 20:56:02 +07:00
|
|
|
match db::query_character(&character.name, &character.series).await {
|
2024-01-06 15:30:11 +07:00
|
|
|
Some(c) => {
|
2024-01-08 20:56:02 +07:00
|
|
|
character = c;
|
2024-01-06 15:30:11 +07:00
|
|
|
}
|
2024-01-10 12:31:29 +07:00
|
|
|
None => match db::query_character_regex(
|
|
|
|
|
®exify_text(&character.name),
|
|
|
|
|
®exify_text(&character.series),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2024-01-08 19:42:32 +07:00
|
|
|
Some(c) => {
|
2024-01-08 20:56:02 +07:00
|
|
|
character = c;
|
2024-01-08 12:44:04 +07:00
|
|
|
}
|
2024-01-08 19:42:32 +07:00
|
|
|
None => {}
|
|
|
|
|
},
|
2024-01-06 15:30:11 +07:00
|
|
|
}
|
2024-01-11 22:09:18 +07:00
|
|
|
Ok(DroppedCard {
|
2024-01-08 20:56:02 +07:00
|
|
|
character,
|
|
|
|
|
print: 0,
|
|
|
|
|
edition: 0,
|
2024-01-11 22:09:18 +07:00
|
|
|
})
|
2024-01-05 20:18:54 +07:00
|
|
|
}
|
|
|
|
|
|
2024-01-11 22:09:18 +07:00
|
|
|
async fn execute_analyze_drop(image: DynamicImage, count: u32) -> Result<DroppedCard, String> {
|
2024-01-05 20:18:54 +07:00
|
|
|
let config = CONFIG.get().unwrap();
|
|
|
|
|
match config.tesseract.backend.as_str() {
|
2024-01-05 22:41:44 +07:00
|
|
|
"libtesseract" => analyze_card_libtesseract(image, count).await,
|
|
|
|
|
"subprocess" => analyze_card_subprocess(image, count).await,
|
2024-01-05 20:18:54 +07:00
|
|
|
_ => {
|
|
|
|
|
panic!("Invalid Tesseract backend: {}", config.tesseract.backend);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-08 20:56:02 +07:00
|
|
|
pub async fn analyze_drop_message(message: &Message) -> Result<Vec<DroppedCard>, String> {
|
2023-12-30 21:39:44 +07:00
|
|
|
if message.attachments.len() < 1 {
|
|
|
|
|
return Err("No attachments found".to_string());
|
|
|
|
|
};
|
|
|
|
|
// Get the image attachment
|
|
|
|
|
let attachment = &message.attachments[0];
|
|
|
|
|
let image_bytes = match attachment.download().await {
|
|
|
|
|
Ok(bytes) => bytes,
|
|
|
|
|
Err(why) => return Err(format!("Failed to download attachment: {:?}", why)),
|
|
|
|
|
};
|
|
|
|
|
// Pre-process the image
|
2024-01-05 00:40:57 +07:00
|
|
|
let mut img =
|
|
|
|
|
match ImageReader::with_format(Cursor::new(image_bytes), ImageFormat::Png).decode() {
|
2023-12-30 21:39:44 +07:00
|
|
|
Ok(img) => img,
|
|
|
|
|
Err(why) => return Err(format!("Failed to decode image: {:?}", why)),
|
2024-01-05 00:40:57 +07:00
|
|
|
};
|
2023-12-31 22:55:32 +07:00
|
|
|
trace!("Grayscaling image...");
|
2023-12-30 21:39:44 +07:00
|
|
|
img = img.grayscale();
|
2024-01-05 00:40:57 +07:00
|
|
|
save_image_if_trace(&img, "debug/1-grayscale.png");
|
2023-12-31 22:55:32 +07:00
|
|
|
trace!("Increasing contrast of the image...");
|
2024-01-10 01:24:22 +07:00
|
|
|
contrast_in_place(&mut img, 127.0 / 4.0);
|
2024-01-05 00:40:57 +07:00
|
|
|
save_image_if_trace(&img, "debug/2-contrast.png");
|
2023-12-31 22:55:32 +07:00
|
|
|
// Cropping cards
|
|
|
|
|
let distance = 257 - 29 + 305 - 259;
|
|
|
|
|
let cards_count = img.width() / distance;
|
|
|
|
|
trace!("Cropping {} cards...", cards_count);
|
|
|
|
|
let mut jobs: Vec<_> = Vec::new();
|
2024-01-08 20:56:02 +07:00
|
|
|
let mut cards: Vec<DroppedCard> = Vec::with_capacity(cards_count.try_into().unwrap());
|
2024-01-05 00:40:57 +07:00
|
|
|
for index in 0..cards_count {
|
|
|
|
|
let i = index.clone();
|
|
|
|
|
let x = 29 + distance * i;
|
|
|
|
|
let y = 34;
|
|
|
|
|
let width = 257 + distance * i - x;
|
|
|
|
|
let height = 387 - y;
|
|
|
|
|
trace!("Cropping card {} ({}, {}, {}, {})", i, x, y, width, height);
|
|
|
|
|
let card_img = img.crop_imm(x, y, width, height);
|
|
|
|
|
save_image_if_trace(&card_img, &format!("debug/3-cropped-{}.png", i));
|
2024-01-05 22:41:44 +07:00
|
|
|
jobs.push(async move {
|
2024-01-05 00:40:57 +07:00
|
|
|
trace!("Analyzing card {}", i);
|
2024-01-05 22:41:44 +07:00
|
|
|
Ok((i, execute_analyze_drop(card_img, i).await))
|
2024-01-05 20:18:54 +07:00
|
|
|
});
|
2023-12-31 22:55:32 +07:00
|
|
|
}
|
2024-01-12 18:14:50 +07:00
|
|
|
let mut handles: Vec<task::JoinHandle<Result<(u32, Result<DroppedCard, String>), String>>> =
|
|
|
|
|
Vec::new();
|
2023-12-31 22:55:32 +07:00
|
|
|
for job in jobs {
|
2024-01-05 22:41:44 +07:00
|
|
|
let handle = task::spawn(job);
|
|
|
|
|
handles.push(handle);
|
2023-12-31 22:55:32 +07:00
|
|
|
}
|
2024-01-05 22:41:44 +07:00
|
|
|
for handle in handles {
|
|
|
|
|
let result = handle.await;
|
2023-12-31 22:55:32 +07:00
|
|
|
match result {
|
2024-01-05 00:40:57 +07:00
|
|
|
Ok(result) => {
|
|
|
|
|
match result {
|
2024-01-11 22:18:39 +07:00
|
|
|
Ok((i, card_result)) => {
|
|
|
|
|
let card = match card_result {
|
|
|
|
|
Ok(card) => card,
|
|
|
|
|
Err(why) => return Err(format!("Failed to analyze card: {}", why)),
|
|
|
|
|
};
|
2024-01-05 00:40:57 +07:00
|
|
|
trace!("Finished analyzing card {}", i);
|
|
|
|
|
cards.push(card);
|
|
|
|
|
}
|
|
|
|
|
Err(why) => return Err(format!("Failed to analyze card: {}", why)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
Err(why) => return Err(format!("Failed to analyze card: {:?}", why)),
|
2023-12-31 22:55:32 +07:00
|
|
|
};
|
|
|
|
|
}
|
2024-01-05 00:40:57 +07:00
|
|
|
Ok(cards)
|
2023-12-30 21:39:44 +07:00
|
|
|
}
|
2024-01-07 23:52:49 +07:00
|
|
|
|
|
|
|
|
pub async fn handle_drop_message(ctx: &Context, msg: &Message) {
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
match analyze_drop_message(msg).await {
|
|
|
|
|
Ok(cards) => {
|
|
|
|
|
let duration = start.elapsed();
|
|
|
|
|
let mut reply_str = String::new();
|
|
|
|
|
for card in cards {
|
|
|
|
|
// reply_str.push_str(&format!("{:?}\n", card));
|
2024-01-08 20:56:02 +07:00
|
|
|
let wishlist_str: String = match card.character.wishlist {
|
2024-01-07 23:52:49 +07:00
|
|
|
Some(wishlist) => {
|
|
|
|
|
let mut out_str = wishlist.to_string();
|
|
|
|
|
while out_str.len() < 5 {
|
|
|
|
|
out_str.push(' ');
|
|
|
|
|
}
|
|
|
|
|
out_str
|
|
|
|
|
}
|
|
|
|
|
None => "None ".to_string(),
|
|
|
|
|
};
|
2024-01-08 20:56:02 +07:00
|
|
|
let last_update_ts_str = match card.character.last_update_ts {
|
2024-01-07 23:52:49 +07:00
|
|
|
0 => "`Never`".to_string(),
|
|
|
|
|
ts => {
|
|
|
|
|
format!("<t:{}:R>", ts.to_string())
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
reply_str.push_str(
|
|
|
|
|
format!(
|
|
|
|
|
":heart: `{}` • `{}` • **{}** • {} • {}\n",
|
2024-01-08 20:56:02 +07:00
|
|
|
wishlist_str,
|
|
|
|
|
card.print,
|
|
|
|
|
card.character.name,
|
|
|
|
|
card.character.series,
|
|
|
|
|
last_update_ts_str
|
2024-01-07 23:52:49 +07:00
|
|
|
)
|
|
|
|
|
.as_str(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
reply_str.push_str(&format!("Time taken (to analyze): `{:?}`", duration));
|
|
|
|
|
match msg.reply(ctx, reply_str).await {
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
Err(why) => {
|
|
|
|
|
error!("Failed to reply to message: {:?}", why);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
Err(why) => {
|
|
|
|
|
helper::error_message(
|
|
|
|
|
ctx,
|
|
|
|
|
msg,
|
|
|
|
|
format!("Failed to analyze drop: `{:?}`", why),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|