concord 2.2.7

A terminal user interface client for Discord
Documentation
use std::{
    io::Cursor,
    sync::{Arc, OnceLock},
};

use image::{DynamicImage, ImageReader, Limits};
use tokio::{sync::mpsc, task};

use super::preview::ImagePreviewKey;

const MAX_CONCURRENT_MEDIA_IMAGE_DECODES: usize = 2;
pub(super) const MAX_DECODED_IMAGE_WIDTH: u32 = 4096;
pub(super) const MAX_DECODED_IMAGE_HEIGHT: u32 = 4096;
const MAX_DECODED_IMAGE_BYTES: u64 = 64 * 1024 * 1024;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::tui) enum MediaImageDecodeKey {
    Preview(ImagePreviewKey),
    Avatar(String),
    Emoji(String),
}

pub(in crate::tui) struct MediaImageDecodeJob {
    pub(super) key: MediaImageDecodeKey,
    pub(super) generation: u64,
    pub(super) bytes: Arc<[u8]>,
}

pub(in crate::tui) struct MediaImageDecodeResult {
    pub(in crate::tui) key: MediaImageDecodeKey,
    pub(in crate::tui) generation: u64,
    pub(in crate::tui) result: std::result::Result<DynamicImage, String>,
}

pub(in crate::tui) fn spawn_media_image_decode(
    job: MediaImageDecodeJob,
    tx: mpsc::UnboundedSender<MediaImageDecodeResult>,
) {
    let decode_permits = media_image_decode_permits().clone();
    task::spawn(async move {
        let Ok(_permit) = decode_permits.acquire_owned().await else {
            return;
        };
        if let Ok(result) = task::spawn_blocking(move || decode_media_image(job)).await {
            let _ = tx.send(result);
        }
    });
}

fn decode_media_image(job: MediaImageDecodeJob) -> MediaImageDecodeResult {
    let result = decode_image_bytes(&job.bytes);
    MediaImageDecodeResult {
        key: job.key,
        generation: job.generation,
        result,
    }
}

fn media_image_decode_permits() -> &'static Arc<tokio::sync::Semaphore> {
    static PERMITS: OnceLock<Arc<tokio::sync::Semaphore>> = OnceLock::new();
    PERMITS.get_or_init(|| {
        Arc::new(tokio::sync::Semaphore::new(
            MAX_CONCURRENT_MEDIA_IMAGE_DECODES,
        ))
    })
}

pub(in crate::tui) fn decode_image_bytes(
    bytes: &[u8],
) -> std::result::Result<DynamicImage, String> {
    let mut limits = Limits::default();
    limits.max_image_width = Some(MAX_DECODED_IMAGE_WIDTH);
    limits.max_image_height = Some(MAX_DECODED_IMAGE_HEIGHT);
    limits.max_alloc = Some(MAX_DECODED_IMAGE_BYTES);

    let mut reader = ImageReader::new(Cursor::new(bytes))
        .with_guessed_format()
        .map_err(|error| format!("decode failed: {error}"))?;
    reader.limits(limits);
    reader
        .decode()
        .map_err(|error| format!("decode failed: {error}"))
}