Skip to content

Commit

Permalink
feat: add image-resizer servlet
Browse files Browse the repository at this point in the history
  • Loading branch information
G4Vi committed Jan 23, 2025
1 parent 8ca493a commit c3542cf
Show file tree
Hide file tree
Showing 7 changed files with 568 additions and 0 deletions.
2 changes: 2 additions & 0 deletions servlets/image-resizer/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
target = "wasm32-wasip1"
21 changes: 21 additions & 0 deletions servlets/image-resizer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
19 changes: 19 additions & 0 deletions servlets/image-resizer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "image-resizer"
version = "0.1.0"
edition = "2021"

[lib]
name = "plugin"
crate-type = ["cdylib"]

[dependencies]
extism-pdk = "1.1.0"
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
base64-serde = "0.7"
base64 = "0.21"
image = "0.25.5"

[workspace]
58 changes: 58 additions & 0 deletions servlets/image-resizer/prepare.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/bash

# Function to check if a command exists
command_exists () {
command -v "$1" >/dev/null 2>&1
}

missing_deps=0

# Check for Cargo
if ! (command_exists cargo); then
missing_deps=1
echo "❌ Cargo/rust is not installed."
echo ""
echo "To install Rust, visit the official download page:"
echo "👉 https://www.rust-lang.org/tools/install"
echo ""
echo "Or install it using a package manager:"
echo ""
echo "🔹 macOS (Homebrew):"
echo " brew install cargo"
echo ""
echo "🔹 Ubuntu/Debian:"
echo " sudo apt-get install -y cargo"
echo ""
echo "🔹 Arch Linux:"
echo " sudo pacman -S rust"
echo ""
fi

if ! (command_exists rustup); then
missing_deps=1
echo "❌ rustup is missing. Check your rust installation."
echo ""
fi

# Exit with a bad exit code if any dependencies are missing
if [ "$missing_deps" -ne 0 ]; then
echo "Install the missing dependencies and ensure they are on your path. Then run this command again."
# TODO: remove sleep when cli bug is fixed
sleep 2
exit 1
fi

if ! (rustup target list --installed | grep -q '^wasm32-wasip1$'); then
if ! (rustup target add wasm32-wasip1); then
echo "❌ error encountered while adding target \"wasm32-wasip1\""
echo ""
echo "Update rustup with:"
echo "👉 rustup update"
echo ""
exit 1
fi
fi

if ! (rustup target list --installed | grep -q '^wasm32-unknown-unknown$'); then
rustup target add wasm32-unknown-unknown
fi
112 changes: 112 additions & 0 deletions servlets/image-resizer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
mod pdk;

use crate::types::ListToolsResult;
use crate::types::ToolDescription;
use base64::{
engine::general_purpose::STANDARD, engine::general_purpose::URL_SAFE_NO_PAD, Engine as _,
};
use extism_pdk::*;
use image::GenericImageView;
use image::ImageReader;
use pdk::*;
use serde_json::{Map, Value};
use std::io::Cursor;

// Called when the tool is invoked.
// If you support multiple tools, you must switch on the input.params.name to detect which tool is being called.
// The name will match one of the tool names returned from "describe".
pub(crate) fn call(_input: types::CallToolRequest) -> Result<types::CallToolResult, Error> {
// load the params
let b64_image = _input
.params
.arguments
.as_ref()
.and_then(|args| args.get("data"))
.and_then(|data| data.as_str())
.ok_or_else(|| Error::msg("Argument `data` must be provided"))?;
let image_data = match URL_SAFE_NO_PAD.decode(b64_image) {
Ok(data) => data,
_ => STANDARD.decode(b64_image)?
};
let image = ImageReader::new(Cursor::new(image_data))
.with_guessed_format()?
.decode()?;
let scale = _input
.params
.arguments
.as_ref()
.and_then(|args| args.get("scale"))
.and_then(|scale| scale.as_number())
.ok_or_else(|| Error::msg("Argument `scale` must be provided"))?;
let scale = scale.as_f64().unwrap();

// scale
let (oldw, oldh) = image.dimensions();
let neww = ((oldw as f64) * scale) as u32;
let newh = ((oldh as f64) * scale) as u32;
let image = image.resize(neww, newh, image::imageops::FilterType::Nearest);

// return the result
let mut result_bytes: Vec<u8> = Vec::new();
image.write_to(&mut Cursor::new(&mut result_bytes), image::ImageFormat::Png)?;
let result_text = URL_SAFE_NO_PAD.encode(result_bytes.clone());
let result_image = STANDARD.encode(result_bytes);
Ok(types::CallToolResult {
content: vec![
types::Content {
r#type: types::ContentType::Image,
text: None,
annotations: None,
data: Some(result_image),
mime_type: Some("image/png".into()),
},
types::Content {
r#type: types::ContentType::Text,
text: Some(result_text),
annotations: None,
data: None,
mime_type: None,
},
],
is_error: None,
})
}

// Called by mcpx to understand how and why to use this tool.
// Note: Your servlet configs will not be set when this function is called,
// so do not rely on config in this function
pub(crate) fn describe() -> Result<types::ListToolsResult, Error> {
let mut data_prop: Map<String, Value> = Map::new();
data_prop.insert("type".into(), "string".into());
data_prop.insert(
"description".into(),
"base64url data of image file to resize".into(),
);

let mut scale_prop: Map<String, Value> = Map::new();
scale_prop.insert("type".into(), "number".into());
scale_prop.insert(
"description".into(),
"Amount to scale image, for example, 1 to keep the same, 2 to double the size, etc".into(),
);

let mut props: Map<String, Value> = Map::new();
props.insert("data".into(), data_prop.into());
props.insert("scale".into(), scale_prop.into());

let mut schema: Map<String, Value> = Map::new();
schema.insert("type".into(), "object".into());
schema.insert("properties".into(), Value::Object(props));
schema.insert(
"required".into(),
Value::Array(vec!["data".into(), "scale".into()]),
);

Ok(ListToolsResult {
tools: vec![ToolDescription {
name: "image-resizer".into(),
description: "Resize an image file".into(),
input_schema: schema,
}],
})
}
Loading

0 comments on commit c3542cf

Please sign in to comment.