diff --git a/cvm-rs/Cargo.lock b/cvm-rs/Cargo.lock index 844f876..02e6b96 100644 --- a/cvm-rs/Cargo.lock +++ b/cvm-rs/Cargo.lock @@ -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" diff --git a/cvm-rs/Cargo.toml b/cvm-rs/Cargo.toml index f7b13f9..404b7a1 100644 --- a/cvm-rs/Cargo.toml +++ b/cvm-rs/Cargo.toml @@ -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" diff --git a/cvm-rs/index.d.ts b/cvm-rs/index.d.ts index af6e202..98d5667 100644 --- a/cvm-rs/index.d.ts +++ b/cvm-rs/index.d.ts @@ -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; -// TODO: Version that can downscale? +/// Performs JPEG encoding with resizing. +export function jpegResizeEncode(input: JpegResizeInputArgs): Promise; + /* remoting API? diff --git a/cvm-rs/index.js b/cvm-rs/index.js index 3c3b5bd..cf43b65 100644 --- a/cvm-rs/index.js +++ b/cvm-rs/index.js @@ -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) { diff --git a/cvm-rs/src/jpeg_js.rs b/cvm-rs/src/jpeg_js.rs index 6f1004c..de98202 100644 --- a/cvm-rs/src/jpeg_js.rs +++ b/cvm-rs/src/jpeg_js.rs @@ -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 = OnceCell::new(); @@ -69,9 +75,76 @@ pub fn jpeg_encode(env: Env, input: JpegInputArgs) -> napi::Result napi::Result { + let (deferred_resolver, promise) = env.create_deferred::()?; + 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 = + 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()) diff --git a/cvmts/package.json b/cvmts/package.json index e38615d..c559cff 100644 --- a/cvmts/package.json +++ b/cvmts/package.json @@ -22,7 +22,6 @@ "mnemonist": "^0.39.5", "msgpackr": "^1.10.2", "pino": "^9.3.1", - "sharp": "^0.33.3", "toml": "^3.0.0", "ws": "^8.17.1" }, diff --git a/cvmts/src/JPEGEncoder.ts b/cvmts/src/JPEGEncoder.ts index 8c182a0..97e699c 100644 --- a/cvmts/src/JPEGEncoder.ts +++ b/cvmts/src/JPEGEncoder.ts @@ -1,5 +1,4 @@ import { Size, Rect } from './Utilities'; -import sharp from 'sharp'; import * as cvm from '@cvmts/cvm-rs'; // A good balance. TODO: Configurable? @@ -10,17 +9,6 @@ const kThumbnailSize: Size = { height: 300 }; -// this returns appropiate Sharp options to deal with CVMTS raw framebuffers -// (which are RGBA bitmaps, essentially. We probably should abstract that out but -// that'd mean having to introduce that to rfb and oihwekjtgferklds;./tghnredsltg;erhds) -function GetRawSharpOptions(size: Size): sharp.CreateRaw { - return { - width: size.width, - height: size.height, - channels: 4 - }; -} - export class JPEGEncoder { static SetQuality(quality: number) { gJpegQuality = quality; @@ -37,16 +25,12 @@ export class JPEGEncoder { } static async EncodeThumbnail(buffer: Buffer, size: Size): Promise { - let { data, info } = await sharp(buffer, { raw: GetRawSharpOptions(size) }) - .resize(kThumbnailSize.width, kThumbnailSize.height, { fit: 'fill' }) - .raw() - .toBuffer({ resolveWithObject: true }); - - return cvm.jpegEncode({ - width: kThumbnailSize.width, - height: kThumbnailSize.height, - stride: kThumbnailSize.width, - buffer: data + return cvm.jpegResizeEncode({ + width: size.width, + height: size.height, + desiredWidth: kThumbnailSize.width, + desiredHeight: kThumbnailSize.height, + buffer: buffer }); } }