diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ba9a238..fb17eb4 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -33,7 +33,7 @@ jobs: strip -x packages/*/*.node - host: windows-latest build: | - choco install meson -y + python -m pip install meson yarn workspace @napi-rs/image build --features with_simd target: x86_64-pc-windows-msvc - host: ubuntu-latest diff --git a/README.md b/README.md index 8883536..5e7d9f9 100644 --- a/README.md +++ b/README.md @@ -166,4 +166,11 @@ writeFileSync( ) console.info(chalk.green('Encoding webp from JPEG with EXIF done')) + +writeFileSync( + 'output-overlay-png.png', + await new Transformer(PNG).overlay(PNG, 200, 200).png() +) + +console.info(chalk.green('Overlay an image done')) ``` diff --git a/example.mjs b/example.mjs index 463be97..1b88b2d 100644 --- a/example.mjs +++ b/example.mjs @@ -64,3 +64,10 @@ writeFileSync( ) console.info(chalk.green('Encoding webp from JPEG with EXIF done')) + +writeFileSync( + 'output-overlay-png.png', + await new Transformer(PNG).overlay(PNG, 200, 200).png() +) + +console.info(chalk.green('Overlay an image done')) \ No newline at end of file diff --git a/packages/binding/README.md b/packages/binding/README.md index 9e1390c..758e8fa 100644 --- a/packages/binding/README.md +++ b/packages/binding/README.md @@ -419,6 +419,28 @@ export const enum ResizeFilterType { } ``` +#### `overlay` + +```ts +/** + * Overlay an image at a given coordinate (x, y) + */ +overlay(onTop: Buffer, x: number, y: number): this +``` +```ts +import { writeFileSync } from 'fs' + +import { Transformer } from '@napi-rs/image' + + +const imageOutputPng = await new Transformer(PNG).overlay(PNG, 200, 200).png() + +writeFileSync( + 'output-overlay-png.png', + imageOutputPng +) +``` + **ResizeFilterType**: To test the different sampling filters on a real example, you can find two diff --git a/packages/binding/index.d.ts b/packages/binding/index.d.ts index 9cd3ed3..5832aea 100644 --- a/packages/binding/index.d.ts +++ b/packages/binding/index.d.ts @@ -300,6 +300,8 @@ export interface Metadata { } export class Transformer { constructor(input: Buffer) + /** Overlay an image at a given coordinate (x, y) */ + overlay(onTop: Buffer, x: number, y: number): this static fromRgbaPixels(input: Buffer | Uint8ClampedArray, width: number, height: number): Transformer metadata(withExif?: boolean | undefined | null, signal?: AbortSignal | undefined | null): Promise /** diff --git a/packages/binding/src/transformer.rs b/packages/binding/src/transformer.rs index dd42346..1304d33 100644 --- a/packages/binding/src/transformer.rs +++ b/packages/binding/src/transformer.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::io::Cursor; use std::sync::Arc; +use image::imageops::overlay; use image::{ imageops::FilterType, ColorType, DynamicImage, ImageBuffer, ImageEncoder, ImageFormat, }; @@ -70,7 +71,7 @@ impl TryFrom for Orientation { 8 => Ok(Orientation::Rotate270Cw), _ => Err(Error::new( Status::InvalidArg, - format!("Invalid orientation {}", value), + format!("Invalid orientation {value}"), )), } } @@ -214,7 +215,7 @@ impl ThreadSafeDynamicImage { let image_format = image::guess_format(input_buf).map_err(|err| { Error::new( Status::InvalidArg, - format!("Guess format from input image failed {}", err), + format!("Guess format from input image failed {err}"), ) })?; if with_exif { @@ -227,7 +228,7 @@ impl ThreadSafeDynamicImage { let avif = libavif::decode_rgb(input_buf).map_err(|err| { Error::new( Status::InvalidArg, - format!("Decode avif image failed {}", err), + format!("Decode avif image failed {err}"), ) })?; let decoded_rgb = avif.to_vec(); @@ -249,7 +250,7 @@ impl ThreadSafeDynamicImage { } } else { image::load_from_memory_with_format(input_buf, image_format) - .map_err(|err| Error::new(Status::InvalidArg, format!("Decode image failed {}", err)))? + .map_err(|err| Error::new(Status::InvalidArg, format!("Decode image failed {err}")))? }; let color_type = dynamic_image.color(); image.replace(ImageMetaData { @@ -476,6 +477,7 @@ impl Task for EncodeTask { if let Some((x, y, width, height)) = self.image_transform_args.crop { meta.image = meta.image.crop_imm(x, y, width, height); } + let dynamic_image = &mut meta.image; let color_type = &meta.color_type; let width = dynamic_image.width(); @@ -521,7 +523,7 @@ impl Task for EncodeTask { .map_err(|err| { Error::new( Status::GenericFailure, - format!("Encode output png failed {}", err), + format!("Encode output png failed {err}"), ) })?; return Ok(EncodeOutput::Buffer(output.into_inner())); @@ -535,7 +537,7 @@ impl Task for EncodeTask { encoder.encode_image(dynamic_image).map_err(|err| { Error::new( Status::GenericFailure, - format!("Encode output jpeg failed {}", err), + format!("Encode output jpeg failed {err}"), ) })?; return Ok(EncodeOutput::Buffer(output.into_inner())); @@ -600,6 +602,16 @@ impl Transformer { } } + #[napi] + /// Overlay an image at a given coordinate (x, y) + pub fn overlay(&self, on_top: Buffer, x: i64, y: i64) -> Result<&Self> { + let bottom = self.dynamic_image.get(true)?; + let top = ThreadSafeDynamicImage::new(on_top); + let top_image_meta = top.get(true)?; + overlay(&mut bottom.image, &top_image_meta.image, x, y); + Ok(self) + } + #[napi] pub fn from_rgba_pixels( input: Either,