cvm-rs: Add jpegResizeEncode(), remove usage of sharp

sharp sucks.
This commit is contained in:
modeco80
2025-06-05 16:40:42 -04:00
parent d9f9f0d07f
commit 23c57dbb3b
7 changed files with 124 additions and 29 deletions

27
cvm-rs/Cargo.lock generated
View File

@@ -53,6 +53,12 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "bytemuck"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
[[package]]
name = "cc"
version = "1.2.23"
@@ -132,6 +138,8 @@ dependencies = [
"napi-derive",
"once_cell",
"rayon",
"resize",
"rgb",
"turbojpeg-sys",
]
@@ -331,6 +339,25 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "resize"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87a103d0b47e783f4579149402f7499397ab25540c7a57b2f70487a5d2d20ef0"
dependencies = [
"rayon",
"rgb",
]
[[package]]
name = "rgb"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
dependencies = [
"bytemuck",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"

View File

@@ -20,6 +20,8 @@ rayon = "1.10.0"
napi = { version = "2.16.9", features = [ "async", "napi8", "error_anyhow" ] }
napi-derive = "2.16.11"
anyhow = "1.0.86"
resize = "0.8.8"
rgb = "0.8.50"
[build-dependencies]
napi-build = "2.1.3"

12
cvm-rs/index.d.ts vendored
View File

@@ -14,10 +14,20 @@ interface JpegInputArgs {
// (i.e: new JpegEncoder(FORMAT_xxx)).
}
interface JpegResizeInputArgs {
width: number; // source width
height: number; // source height
desiredWidth: number; // dest width
desiredHeight: number; // dest height
buffer: Buffer; // source raw pixel buffer
}
/// Performs JPEG encoding.
export function jpegEncode(input: JpegInputArgs): Promise<Buffer>;
// TODO: Version that can downscale?
/// Performs JPEG encoding with resizing.
export function jpegResizeEncode(input: JpegResizeInputArgs): Promise<Buffer>;
/* remoting API?

View File

@@ -2,9 +2,9 @@
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
let {guacDecode, guacEncodeImpl, jpegEncode} = require('./index.node');
let {guacDecode, guacEncodeImpl, jpegEncode, jpegResizeEncode} = require('./index.node');
export { guacDecode, jpegEncode };
export { guacDecode, jpegEncode, jpegResizeEncode };
// shim for js->rust interop, because napi-rs kind of blows in this regard
export function guacEncode(...args) {

View File

@@ -9,6 +9,12 @@ use rayon::{ThreadPool, ThreadPoolBuilder};
use crate::jpeg_compressor::*;
use resize::Pixel::RGBA8;
use resize::Type::Triangle;
use rgb::FromSlice;
/// Gives a Rayon thread pool we use for parallelism
fn rayon_pool() -> &'static ThreadPool {
static RUNTIME: OnceCell<ThreadPool> = OnceCell::new();
@@ -69,9 +75,76 @@ pub fn jpeg_encode(env: Env, input: JpegInputArgs) -> napi::Result<napi::JsObjec
});
deferred_resolver.resolve(move |env| {
let buffer = env
.create_buffer_with_data(vec)
.expect("Couldn't create node Buffer, things are probably very broken by this point");
let buffer = env.create_buffer_with_data(vec).expect(
"Couldn't create node Buffer, things are probably very broken by this point",
);
// no longer need the input buffer
buf.unref(env)?;
Ok(buffer.into_raw())
});
});
Ok(promise)
}
#[napi(object)]
pub struct JpegResizeInputArgs {
pub width: u32,
pub height: u32,
pub desired_width: u32,
pub desired_height: u32,
pub buffer: napi::JsBuffer,
}
#[napi(js_name = "jpegResizeEncode")]
#[allow(unused)]
pub fn jpeg_resize_and_encode(
env: Env,
input: JpegResizeInputArgs,
) -> napi::Result<napi::JsObject> {
let (deferred_resolver, promise) = env.create_deferred::<napi::JsBuffer, _>()?;
let mut buf = input.buffer.into_ref()?;
// Spawn a task on the rayon pool that encodes the JPEG and fufills the promise
// once it is done encoding.
rayon_pool().spawn_fifo(move || {
let mut new_data: Vec<u8> =
vec![0; (input.desired_width * input.desired_height) as usize * 4];
let mut resizer = resize::new(
input.width as usize,
input.height as usize,
input.desired_width as usize,
input.desired_height as usize,
RGBA8,
Triangle,
)
.expect("Could not create resizer");
resizer
.resize(&buf.as_rgba(), new_data.as_rgba_mut())
.expect("Resize operation failed");
// then just jpeg encode. Ideally this would be shared :(
let image = Image {
buffer: &new_data,
width: input.desired_width as u32,
height: input.desired_height as u32,
stride: (input.desired_width as u64 * 4u64) as u32,
format: turbojpeg_sys::TJPF_TJPF_RGBA,
};
let vec = COMPRESSOR.with(|lazy| {
let mut b = lazy.borrow_mut();
b.set_quality(35);
b.set_subsamp(turbojpeg_sys::TJSAMP_TJSAMP_420);
b.compress_buffer(&image)
});
deferred_resolver.resolve(move |env| {
let buffer = env.create_buffer_with_data(vec).expect(
"Couldn't create node Buffer, things are probably very broken by this point",
);
// no longer need the input buffer
buf.unref(env)?;
Ok(buffer.into_raw())