aboutsummaryrefslogtreecommitdiff
path: root/transcoder/src/image.rs
blob: fda312915bd170aa228bcd41e193c4be0d5a6fdf (plain)
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
/*
    This file is part of jellything (https://codeberg.org/metamuffin/jellything)
    which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
    Copyright (C) 2024 metamuffin <metamuffin.org>
*/
use crate::LOCAL_IMAGE_TRANSCODING_TASKS;
use anyhow::Context;
use image::imageops::FilterType;
use jellybase::cache::{async_cache_file, CachePath};
use log::{debug, info};
use rgb::FromSlice;
use std::{
    fs::File,
    io::{BufReader, Read, Seek, SeekFrom},
    path::PathBuf,
};
use tokio::io::AsyncWriteExt;

pub async fn transcode(
    path: PathBuf,
    quality: f32,
    speed: u8,
    width: usize,
) -> anyhow::Result<CachePath> {
    async_cache_file(
        &[
            "image-tc",
            path.clone().as_os_str().to_str().unwrap(),
            &format!("{width} {quality} {speed}"),
        ],
        move |mut output| async move {
            let _permit = LOCAL_IMAGE_TRANSCODING_TASKS.acquire().await?;
            info!("encoding {path:?} (speed={speed}, quality={quality}, width={width})");
            let encoded = tokio::task::spawn_blocking(move || {
                let mut file = BufReader::new(File::open(&path).context("opening source")?);

                // TODO: use better image library that supports AVIF
                let is_avif = {
                    let mut magic = [0u8; 12];
                    file.read_exact(&mut magic).context("reading magic")?;
                    file.seek(SeekFrom::Start(0))
                        .context("seeking back to start")?;
                    // TODO: magic experimentally found and probably not working in all cases but fine as long as our avif enc uses that
                    matches!(
                        magic,
                        [
                            0x00,
                            0x00,
                            0x00,
                            _,
                            b'f',
                            b't',
                            b'y',
                            b'p',
                            b'a',
                            b'v',
                            b'i',
                            b'f',
                        ]
                    )
                };
                let original = if is_avif {
                    let mut buf = Vec::new();
                    file.read_to_end(&mut buf).context("reading image")?;
                    libavif_image::read(&buf).unwrap().to_rgba8()
                } else {
                    let reader = image::ImageReader::new(file);
                    let reader = reader.with_guessed_format().context("guessing format")?;
                    debug!("guessed format (or fallback): {:?}", reader.format());
                    reader.decode().context("decoding image")?.to_rgba8()
                };
                let image = image::imageops::resize(
                    &original,
                    width as u32,
                    width as u32 * original.height() / original.width(),
                    FilterType::Lanczos3,
                );
                let pixels = image.to_vec();
                let encoded = ravif::Encoder::new()
                    .with_speed(speed.clamp(1, 10))
                    .with_quality(quality.clamp(1., 100.))
                    .encode_rgba(imgref::Img::new(
                        pixels.as_rgba(),
                        image.width() as usize,
                        image.height() as usize,
                    ))
                    .context("avif encoding")?;
                info!("transcode finished");
                Ok::<_, anyhow::Error>(encoded)
            })
            .await??;
            output
                .write_all(&encoded.avif_file)
                .await
                .context("writing encoded image")?;
            Ok(())
        },
    )
    .await
}