aboutsummaryrefslogtreecommitdiffstats
path: root/src/commands/qr.rs
blob: 611f43f42f50a8480cba5c0defe334bd025d2ea6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
use poise::serenity_prelude as serenity;

use crate::Data;
use crate::error::{Error, Result};
use crate::lang;

type Context<'a> = poise::Context<'a, Data, Error>;

/// QR group
#[poise::command(slash_command, subcommands("dekodowanie", "generowanie",))]
pub async fn qr(_ctx: Context<'_>) -> Result<()> {
    Ok(())
}

/// Generate a QR code from text
#[poise::command(slash_command)]
pub async fn generowanie(
    ctx: Context<'_>,
    #[description = "Zawartość kodu QR"] text: String,
) -> Result<()> {
    ctx.defer().await?;

    let code = qrcode::QrCode::new(text.as_bytes())?;
    let image = code.render::<image::Luma<u8>>().build();

    let scaled = image::imageops::resize(
        &image,
        image.width() * 10,
        image.height() * 10,
        image::imageops::Nearest,
    );

    let mut buf = std::io::Cursor::new(Vec::new());
    scaled.write_to(&mut buf, image::ImageFormat::Png)?;

    ctx.send(
        poise::CreateReply::default()
            .content(lang::L_QR_GENERATED)
            .attachment(serenity::CreateAttachment::bytes(
                buf.into_inner(),
                "qrcode.png",
            )),
    )
    .await?;
    Ok(())
}

/// Decode a QR code from an uploaded image
#[poise::command(slash_command)]
pub async fn dekodowanie(
    ctx: Context<'_>,
    #[description = "Obraz zawierający kod QR"] image: serenity::Attachment,
) -> Result<()> {
    ctx.defer().await?;

    let img_data = ctx
        .data()
        .http_client
        .get(&image.url)
        .send()
        .await?
        .bytes()
        .await?;
    // Decoding is CPU-heavy: keep it off the async runtime, and cap the
    // dimensions/allocation so a small compressed image cannot expand into
    // gigabytes of pixels.
    let img_data = img_data.to_vec();
    let decoded = tokio::task::spawn_blocking(move || {
        let mut reader = image::ImageReader::new(std::io::Cursor::new(&img_data))
            .with_guessed_format()
            .ok()?;
        let mut limits = image::Limits::default();
        limits.max_image_width = Some(5_000);
        limits.max_image_height = Some(5_000);
        limits.max_alloc = Some(128 * 1024 * 1024);
        reader.limits(limits);
        let img = reader.decode().ok()?;
        let gray = img.to_luma8();
        decode_qr_simple(&gray)
    })
    .await?;
    match decoded {
        Some(text) => {
            ctx.say(lang::fmt_qr_decoded(&text)).await?;
        }
        None => {
            ctx.say(lang::L_QR_DECODE_FAILED).await?;
        }
    }
    Ok(())
}

fn decode_qr_simple(img: &image::ImageBuffer<image::Luma<u8>, Vec<u8>>) -> Option<String> {
    rxing::helpers::detect_in_luma(img.as_raw().to_vec(), img.width(), img.height(), None)
        .ok()
        .map(|r| r.getText().to_owned())
}

pub fn commands() -> Vec<poise::Command<Data, Error>> {
    vec![qr()]
}