diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6c6bea8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# AGENTS.md + +## Project Overview +`img-toolkit` is a Rust + WebAssembly image processing library published to npm. +The Rust core handles decode/transform/encode, and the TypeScript wrapper exposes the browser-facing API. + +## Key Files +- `src/lib.rs`: Rust/WASM core pipeline (option parsing, resize/brightness, format encode/decode, Rust tests). +- `ts-wrapper/resizeImage.ts`: TypeScript public API wrapper used by app code. +- `example/imageWorker.js`: Worker-side integration example for async image processing. +- `Cargo.toml`: Rust crate config and dependencies. +- `package.json`: npm package metadata, scripts, and build entrypoints. +- `README.MD`: user-facing API docs and usage examples; update when behavior/API changes. + +## Common Commands +- `cargo test`: run Rust unit tests for core behavior. +- `npm run build:wasm`: build wasm artifacts from Rust (`wasm-pack build --target web`). +- `npm run build:ts`: compile TypeScript wrapper/types. +- `npm run build`: full package build (`build:wasm` + `build:ts`). + +Run the smallest relevant command first, then run the full build before final handoff. + +## Working Rules +- Preserve 2.x API compatibility unless a breaking change is explicitly requested. +- Keep user-facing errors safe and generic; avoid exposing low-level internals. +- Validate/sanitize options (clamp ranges, handle non-finite values). +- Do not remove `jpg/jpeg`, `png`, or `webp` support unless explicitly requested. +- Keep changes focused; avoid unrelated refactors. +- If API/default behavior changes, update `README.MD` and relevant examples in `example/`. diff --git a/README.MD b/README.MD index b0d81b4..2d96515 100644 --- a/README.MD +++ b/README.MD @@ -11,6 +11,8 @@ Demo: https://2yh02.github.io/img-toolkit - [Usage](#usage) - [Functions](#functions) - [Options](#options) +- [Defaults and Input Validation](#defaults-and-input-validation) +- [Error Handling Policy](#error-handling-policy) - [Quality Behavior](#quality-behavior) - [Quality Comparison](#quality-comparison) - [Vite Setup for WASM](#vite-setup-for-wasm) @@ -95,7 +97,7 @@ adjustBrightness(file: File, options: BrightnessOptions): Promise | ------------ | ------ | ------------------------------------------------------------------------------------------------- | | `width` | number | (Optional) Target width in pixels. If omitted, width is auto-adjusted. | | `height` | number | (Optional) Target height in pixels. If omitted, height is auto-adjusted. | -| `quality` | number | (Optional) 0.0 to 1.0. Effective for JPEG and WebP output. | +| `quality` | number | (Optional) 0.0 to 1.0. Effective for JPEG and WebP output. Defaults to `0.7`. Non-finite values (e.g. `NaN`) are sanitized to the default. | | `format` | string | Output format (`"jpg"`, `"png"`, `"webp"`). | | `brightness` | number | (Optional) 0.0 to 1.0. Defaults to 0.5. | | `resampling` | number | (Optional) 0 to 10. Defaults to 4. | @@ -107,22 +109,39 @@ adjustBrightness(file: File, options: BrightnessOptions): Promise | `width` | number | (Optional) Target width in pixels. | | `height` | number | (Optional) Target height in pixels. | | `resampling` | number | (Optional) 0 to 10. Defaults to 4. | -| `quality` | number | (Optional) 0.0 to 1.0. Effective if source is JPEG. | +| `quality` | number | (Optional) 0.0 to 1.0. Effective if source is JPEG. Defaults to `0.7`. Non-finite values are sanitized to the default. | ### `ConvertFormatOptions` | Option | Type | Description | | --------- | ------ | ------------------------------------------------------ | | `format` | string | Output format (`"jpg"`, `"png"`, `"webp"`). | -| `quality` | number | (Optional) 0.0 to 1.0. Effective for JPEG and WebP output. | +| `quality` | number | (Optional) 0.0 to 1.0. Effective for JPEG and WebP output. Defaults to `0.7`. Non-finite values are sanitized to the default. | ### `BrightnessOptions` | Option | Type | Description | | ------------ | ------ | --------------------------------------------------- | -| `brightness` | number | 0.0 to 1.0 | +| `brightness` | number | 0.0 to 1.0. Defaults to `0.5`. Non-finite values are sanitized to the default. | | `quality` | number | (Optional) 0.0 to 1.0. Effective if source is JPEG. | +## Defaults and Input Validation + +- `quality` defaults to `0.7` when omitted. +- `brightness` defaults to `0.5` when omitted. +- `resampling` defaults to `4` when omitted. +- `quality` and `brightness` are clamped to valid ranges (`0.0..1.0`), and non-finite values such as `NaN` / `Infinity` are sanitized to safe defaults. +- `resampling` is clamped to `0..10`, and non-finite values are sanitized to default. + +## Error Handling Policy + +- User-facing errors are intentionally generic and safe for client contexts. +- Internal low-level encoder/decoder details are logged internally and are not returned directly to API consumers. +- Typical user-facing messages include: + - `Invalid options` + - `Unsupported format` + - `Image processing failed` + ## Quality Behavior - `jpg` output: `quality` is applied. diff --git a/src/lib.rs b/src/lib.rs index ead3714..912f493 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -356,6 +356,65 @@ mod tests { assert!(matches!(err, ToolkitError::UnsupportedFormat)); } + #[test] + fn resize_image_without_dimensions_keeps_original_size() { + let input = make_test_png(120, 80); + let options = ResizeOptions { + width: None, + height: None, + quality: None, + format: "png".to_string(), + brightness: 0.5, + resampling: 4, + }; + + let output = resize_image_with_options(&input, options).unwrap(); + let (format, decoded) = decode_image(&output); + + assert_eq!(format, ImageFormat::Png); + assert_eq!(decoded.dimensions(), (120, 80)); + } + + #[test] + fn resize_image_returns_decode_error_for_invalid_input_bytes() { + let input = vec![0x00, 0x11, 0x22, 0x33]; + let options = ResizeOptions { + width: Some(32), + height: Some(32), + quality: None, + format: "jpg".to_string(), + brightness: 0.5, + resampling: 4, + }; + + let err = resize_image_with_options(&input, options).unwrap_err(); + assert!( + matches!( + err, + ToolkitError::FormatGuessFailed(_) | ToolkitError::DecodeFailed(_) + ) + ); + } + + #[test] + fn resize_image_encodes_as_webp() { + let input = make_test_png(96, 64); + let options = ResizeOptions { + width: Some(48), + height: Some(32), + quality: Some(0.7), + format: "webp".to_string(), + brightness: 0.5, + resampling: 4, + }; + + let output = resize_image_with_options(&input, options).unwrap(); + let (format, decoded) = decode_image(&output); + + assert_eq!(format, ImageFormat::WebP); + assert_eq!(decoded.dimensions(), (48, 32)); + } + #[test] fn toolkit_error_user_messages_follow_exposure_policy() { assert_eq!(ToolkitError::InvalidOptions("x".to_string()).user_message(), "Invalid options"); diff --git a/ts-wrapper/resizeImage.ts b/ts-wrapper/resizeImage.ts index 59c7f75..87175f3 100644 --- a/ts-wrapper/resizeImage.ts +++ b/ts-wrapper/resizeImage.ts @@ -35,7 +35,7 @@ export type ConvertFormatOptions = { }; export type BrightnessOptions = { - brightness: number; + brightness?: number; /** * 0.0 to 1.0. Effective when the source image is JPEG. */