feat: add native image module — decode, encode, and resize via Rust image crate
Port image processing from Oh My Pi's pi-natives crate, adapted for napi-rs v2. Exposes NativeImage class with async parse/encode/resize methods backed by the Rust `image` crate (PNG, JPEG, WebP, GIF support). Includes: - task.rs: lightweight async task scheduling for libuv thread pool - image.rs: NativeImage class with SamplingFilter enum - TypeScript types and wrapper (parseImage, ImageFormat, SamplingFilter) - 8 passing tests covering decode, encode, resize, round-trip, error cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e05292f772
commit
75fe5d3319
11 changed files with 485 additions and 5 deletions
32
native/Cargo.lock
generated
32
native/Cargo.lock
generated
|
|
@ -124,6 +124,12 @@ dependencies = [
|
|||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
|
|
@ -300,6 +306,16 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
|
|
@ -405,6 +421,7 @@ name = "gsd-engine"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"gsd-ast",
|
||||
"gsd-grep",
|
||||
"image",
|
||||
"napi",
|
||||
|
|
@ -464,10 +481,25 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
|||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ crate-type = ["cdylib"]
|
|||
gsd-ast = { path = "../ast" }
|
||||
gsd-grep = { path = "../grep" }
|
||||
arboard = "3"
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25", default-features = false, features = [
|
||||
"png",
|
||||
"jpeg",
|
||||
"gif",
|
||||
"webp",
|
||||
] }
|
||||
napi = { version = "2", features = ["napi8"] }
|
||||
napi-derive = "2"
|
||||
|
||||
|
|
|
|||
137
native/crates/engine/src/image.rs
Normal file
137
native/crates/engine/src/image.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
//! Image decode, encode, and resize via N-API.
|
||||
//!
|
||||
//! Provides:
|
||||
//! - Load image from bytes (PNG, JPEG, WebP, GIF)
|
||||
//! - Get dimensions
|
||||
//! - Resize with configurable sampling filter
|
||||
//! - Export as PNG, JPEG, WebP, or GIF
|
||||
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
|
||||
use image::{
|
||||
DynamicImage, ImageFormat, ImageReader,
|
||||
codecs::{jpeg::JpegEncoder, webp::WebPEncoder},
|
||||
imageops::FilterType,
|
||||
};
|
||||
use napi::bindgen_prelude::*;
|
||||
use napi_derive::napi;
|
||||
|
||||
use crate::task;
|
||||
|
||||
/// Sampling filter for resize operations.
|
||||
#[napi]
|
||||
pub enum SamplingFilter {
|
||||
/// Nearest-neighbor sampling (fast, low quality).
|
||||
Nearest = 1,
|
||||
/// Triangle filter (linear interpolation).
|
||||
Triangle = 2,
|
||||
/// Catmull-Rom filter with sharper edges.
|
||||
CatmullRom = 3,
|
||||
/// Gaussian filter for smoother results.
|
||||
Gaussian = 4,
|
||||
/// Lanczos3 filter for high-quality downscaling.
|
||||
Lanczos3 = 5,
|
||||
}
|
||||
|
||||
impl From<SamplingFilter> for FilterType {
|
||||
fn from(filter: SamplingFilter) -> Self {
|
||||
match filter {
|
||||
SamplingFilter::Nearest => Self::Nearest,
|
||||
SamplingFilter::Triangle => Self::Triangle,
|
||||
SamplingFilter::CatmullRom => Self::CatmullRom,
|
||||
SamplingFilter::Gaussian => Self::Gaussian,
|
||||
SamplingFilter::Lanczos3 => Self::Lanczos3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Image container for native interop.
|
||||
#[napi]
|
||||
pub struct NativeImage {
|
||||
img: Arc<DynamicImage>,
|
||||
}
|
||||
|
||||
type ImageTask = task::Async<NativeImage>;
|
||||
|
||||
#[napi]
|
||||
impl NativeImage {
|
||||
/// Decode encoded image bytes (PNG, JPEG, WebP, GIF) into a NativeImage.
|
||||
#[napi(js_name = "parse")]
|
||||
pub fn parse(bytes: Uint8Array) -> ImageTask {
|
||||
let bytes = bytes.as_ref().to_vec();
|
||||
task::blocking("image.decode", (), move |_| -> Result<Self> {
|
||||
let img = decode_image_from_bytes(&bytes)?;
|
||||
Ok(Self { img: Arc::new(img) })
|
||||
})
|
||||
}
|
||||
|
||||
/// Image width in pixels.
|
||||
#[napi(getter, js_name = "width")]
|
||||
pub fn get_width(&self) -> u32 {
|
||||
self.img.width()
|
||||
}
|
||||
|
||||
/// Image height in pixels.
|
||||
#[napi(getter, js_name = "height")]
|
||||
pub fn get_height(&self) -> u32 {
|
||||
self.img.height()
|
||||
}
|
||||
|
||||
/// Encode to bytes. Format: 0=PNG, 1=JPEG, 2=WebP, 3=GIF.
|
||||
#[napi(js_name = "encode")]
|
||||
pub fn encode(&self, format: u8, quality: u8) -> task::Async<Vec<u8>> {
|
||||
let img = Arc::clone(&self.img);
|
||||
task::blocking("image.encode", (), move |_| encode_image(&img, format, quality))
|
||||
}
|
||||
|
||||
/// Resize to exact dimensions. Returns a new NativeImage.
|
||||
#[napi(js_name = "resize")]
|
||||
pub fn resize(&self, width: u32, height: u32, filter: SamplingFilter) -> ImageTask {
|
||||
let img = Arc::clone(&self.img);
|
||||
task::blocking("image.resize", (), move |_| {
|
||||
Ok(Self { img: Arc::new(img.resize_exact(width, height, filter.into())) })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_image_from_bytes(bytes: &[u8]) -> Result<DynamicImage> {
|
||||
let reader = ImageReader::new(Cursor::new(bytes))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| Error::from_reason(format!("Failed to detect image format: {e}")))?;
|
||||
reader
|
||||
.decode()
|
||||
.map_err(|e| Error::from_reason(format!("Failed to decode image: {e}")))
|
||||
}
|
||||
|
||||
fn encode_image(img: &DynamicImage, format: u8, quality: u8) -> Result<Vec<u8>> {
|
||||
let (w, h) = (img.width(), img.height());
|
||||
match format {
|
||||
0 => {
|
||||
let mut buffer = Vec::with_capacity((w * h * 4) as usize);
|
||||
img.write_to(&mut Cursor::new(&mut buffer), ImageFormat::Png)
|
||||
.map_err(|e| Error::from_reason(format!("Failed to encode PNG: {e}")))?;
|
||||
Ok(buffer)
|
||||
},
|
||||
1 => {
|
||||
let mut buffer = Vec::with_capacity((w * h * 3) as usize);
|
||||
let encoder = JpegEncoder::new_with_quality(&mut buffer, quality);
|
||||
img.write_with_encoder(encoder)
|
||||
.map_err(|e| Error::from_reason(format!("Failed to encode JPEG: {e}")))?;
|
||||
Ok(buffer)
|
||||
},
|
||||
2 => {
|
||||
let mut buffer = Vec::with_capacity((w * h * 4) as usize);
|
||||
let encoder = WebPEncoder::new_lossless(&mut buffer);
|
||||
img.write_with_encoder(encoder)
|
||||
.map_err(|e| Error::from_reason(format!("Failed to encode WebP: {e}")))?;
|
||||
Ok(buffer)
|
||||
},
|
||||
3 => {
|
||||
let mut buffer = Vec::with_capacity((w * h) as usize);
|
||||
img.write_to(&mut Cursor::new(&mut buffer), ImageFormat::Gif)
|
||||
.map_err(|e| Error::from_reason(format!("Failed to encode GIF: {e}")))?;
|
||||
Ok(buffer)
|
||||
},
|
||||
_ => Err(Error::from_reason(format!("Invalid image format: {format}"))),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
//! N-API addon for GSD.
|
||||
//!
|
||||
//! Exposes high-performance Rust modules to Node.js via napi-rs.
|
||||
//! Architecture mirrors Oh My Pi's pi-natives crate:
|
||||
//! ```text
|
||||
//! JS (packages/native) -> N-API -> Rust modules (grep, ...)
|
||||
//! JS (packages/native) -> N-API -> Rust modules (ast, clipboard, grep, image, ...)
|
||||
//! ```
|
||||
|
||||
#![allow(clippy::needless_pass_by_value)]
|
||||
|
|
@ -11,3 +10,5 @@
|
|||
mod ast;
|
||||
mod clipboard;
|
||||
mod grep;
|
||||
mod image;
|
||||
mod task;
|
||||
|
|
|
|||
89
native/crates/engine/src/task.rs
Normal file
89
native/crates/engine/src/task.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
//! Blocking work scheduling for N-API exports.
|
||||
//!
|
||||
//! Runs CPU-bound or blocking Rust work on libuv's thread pool via napi's
|
||||
//! `Task` trait, keeping the main JS thread free.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use napi::{Env, Error, Result, Task, bindgen_prelude::*};
|
||||
|
||||
/// Token for cooperative cancellation of blocking work.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CancelToken {
|
||||
deadline: Option<Instant>,
|
||||
}
|
||||
|
||||
impl From<()> for CancelToken {
|
||||
fn from((): ()) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl CancelToken {
|
||||
/// Create a new cancel token from an optional timeout in milliseconds.
|
||||
#[allow(dead_code)]
|
||||
pub fn new(timeout_ms: Option<u32>) -> Self {
|
||||
Self {
|
||||
deadline: timeout_ms
|
||||
.map(|ms| Instant::now() + Duration::from_millis(ms as u64)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if cancellation has been requested.
|
||||
#[allow(dead_code)]
|
||||
pub fn heartbeat(&self) -> Result<()> {
|
||||
if let Some(deadline) = self.deadline {
|
||||
if deadline < Instant::now() {
|
||||
return Err(Error::from_reason("Aborted: Timeout"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Task that runs blocking work on libuv's thread pool.
|
||||
pub struct Blocking<T>
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
cancel_token: CancelToken,
|
||||
work: Option<Box<dyn FnOnce(CancelToken) -> Result<T> + Send>>,
|
||||
}
|
||||
|
||||
impl<T> Task for Blocking<T>
|
||||
where
|
||||
T: ToNapiValue + TypeName + Send + 'static,
|
||||
{
|
||||
type JsValue = T;
|
||||
type Output = T;
|
||||
|
||||
fn compute(&mut self) -> Result<Self::Output> {
|
||||
let work = self
|
||||
.work
|
||||
.take()
|
||||
.ok_or_else(|| Error::from_reason("BlockingTask: work already consumed"))?;
|
||||
work(self.cancel_token.clone())
|
||||
}
|
||||
|
||||
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Async<T> = AsyncTask<Blocking<T>>;
|
||||
|
||||
/// Create an `AsyncTask` that runs blocking work on libuv's thread pool.
|
||||
pub fn blocking<T, F>(
|
||||
_tag: &'static str,
|
||||
cancel_token: impl Into<CancelToken>,
|
||||
work: F,
|
||||
) -> AsyncTask<Blocking<T>>
|
||||
where
|
||||
F: FnOnce(CancelToken) -> Result<T> + Send + 'static,
|
||||
T: ToNapiValue + TypeName + Send + 'static,
|
||||
{
|
||||
AsyncTask::new(Blocking {
|
||||
cancel_token: cancel_token.into(),
|
||||
work: Some(Box::new(work)),
|
||||
})
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"name": "@gsd/native",
|
||||
"version": "0.1.0",
|
||||
"description": "Native Rust bindings for GSD — high-performance grep and clipboard via N-API",
|
||||
"description": "Native Rust bindings for GSD — high-performance native modules via N-API",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build:native": "node ../../native/scripts/build.js",
|
||||
"build:native:dev": "node ../../native/scripts/build.js --dev",
|
||||
"test": "node --test src/__tests__/grep.test.mjs src/__tests__/clipboard.test.mjs"
|
||||
"test": "node --test src/__tests__/grep.test.mjs src/__tests__/clipboard.test.mjs src/__tests__/image.test.mjs"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
|
|
@ -26,6 +26,10 @@
|
|||
"./ast": {
|
||||
"types": "./src/ast/index.ts",
|
||||
"import": "./src/ast/index.ts"
|
||||
},
|
||||
"./image": {
|
||||
"types": "./src/image/index.ts",
|
||||
"import": "./src/image/index.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
|
|
|||
137
packages/native/src/__tests__/image.test.mjs
Normal file
137
packages/native/src/__tests__/image.test.mjs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { test, describe } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createRequire } from "node:module";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { deflateSync } from "node:zlib";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const addonDir = path.resolve(__dirname, "..", "..", "..", "..", "native", "addon");
|
||||
const platformTag = `${process.platform}-${process.arch}`;
|
||||
const candidates = [
|
||||
path.join(addonDir, `gsd_engine.${platformTag}.node`),
|
||||
path.join(addonDir, "gsd_engine.dev.node"),
|
||||
];
|
||||
|
||||
let native;
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
native = require(candidate);
|
||||
break;
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
}
|
||||
|
||||
if (!native) {
|
||||
console.error("Native addon not found. Run 'npm run build:native -w @gsd/native' first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function crc32(buf) {
|
||||
let crc = 0xffffffff;
|
||||
const table = [];
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
table[n] = c;
|
||||
}
|
||||
for (let i = 0; i < buf.length; i++) crc = table[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function createTestPng() {
|
||||
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
const ihdrData = Buffer.alloc(13);
|
||||
ihdrData.writeUInt32BE(2, 0);
|
||||
ihdrData.writeUInt32BE(2, 4);
|
||||
ihdrData[8] = 8;
|
||||
ihdrData[9] = 2;
|
||||
const ihdrType = Buffer.from("IHDR");
|
||||
const ihdrCrc = Buffer.alloc(4);
|
||||
ihdrCrc.writeUInt32BE(crc32(Buffer.concat([ihdrType, ihdrData])));
|
||||
const ihdr = Buffer.concat([Buffer.from([0, 0, 0, 13]), ihdrType, ihdrData, ihdrCrc]);
|
||||
|
||||
const raw = Buffer.from([
|
||||
0, 255, 0, 0, 255, 0, 0,
|
||||
0, 255, 0, 0, 255, 0, 0,
|
||||
]);
|
||||
const compressed = deflateSync(raw);
|
||||
const idatType = Buffer.from("IDAT");
|
||||
const idatLen = Buffer.alloc(4);
|
||||
idatLen.writeUInt32BE(compressed.length);
|
||||
const idatCrc = Buffer.alloc(4);
|
||||
idatCrc.writeUInt32BE(crc32(Buffer.concat([idatType, compressed])));
|
||||
const idat = Buffer.concat([idatLen, idatType, compressed, idatCrc]);
|
||||
|
||||
const iendType = Buffer.from("IEND");
|
||||
const iendCrc = Buffer.alloc(4);
|
||||
iendCrc.writeUInt32BE(crc32(iendType));
|
||||
const iend = Buffer.concat([Buffer.from([0, 0, 0, 0]), iendType, iendCrc]);
|
||||
|
||||
return Buffer.concat([signature, ihdr, idat, iend]);
|
||||
}
|
||||
|
||||
const NativeImage = native.NativeImage;
|
||||
|
||||
describe("native image: NativeImage", () => {
|
||||
test("NativeImage class exists with parse method", () => {
|
||||
assert.ok(NativeImage, "NativeImage should be exported");
|
||||
assert.equal(typeof NativeImage.parse, "function");
|
||||
});
|
||||
|
||||
test("parse decodes PNG with correct dimensions", async () => {
|
||||
const img = await NativeImage.parse(createTestPng());
|
||||
assert.equal(img.width, 2);
|
||||
assert.equal(img.height, 2);
|
||||
});
|
||||
|
||||
test("encode to PNG produces valid PNG", async () => {
|
||||
const img = await NativeImage.parse(createTestPng());
|
||||
const encoded = await img.encode(0, 100);
|
||||
assert.ok(encoded.length > 0);
|
||||
assert.equal(encoded[0], 0x89);
|
||||
assert.equal(encoded[1], 0x50);
|
||||
assert.equal(encoded[2], 0x4e);
|
||||
assert.equal(encoded[3], 0x47);
|
||||
});
|
||||
|
||||
test("encode to JPEG produces valid JPEG", async () => {
|
||||
const img = await NativeImage.parse(createTestPng());
|
||||
const encoded = await img.encode(1, 80);
|
||||
assert.ok(encoded.length > 0);
|
||||
assert.equal(encoded[0], 0xff);
|
||||
assert.equal(encoded[1], 0xd8);
|
||||
});
|
||||
|
||||
test("resize returns correct dimensions", async () => {
|
||||
const img = await NativeImage.parse(createTestPng());
|
||||
const resized = await img.resize(10, 20, 5);
|
||||
assert.equal(resized.width, 10);
|
||||
assert.equal(resized.height, 20);
|
||||
});
|
||||
|
||||
test("resize + encode round-trip", async () => {
|
||||
const img = await NativeImage.parse(createTestPng());
|
||||
const resized = await img.resize(4, 4, 1);
|
||||
const encoded = await resized.encode(0, 100);
|
||||
assert.ok(encoded.length > 0);
|
||||
const reparsed = await NativeImage.parse(new Uint8Array(encoded));
|
||||
assert.equal(reparsed.width, 4);
|
||||
assert.equal(reparsed.height, 4);
|
||||
});
|
||||
|
||||
test("rejects invalid image data", async () => {
|
||||
await assert.rejects(
|
||||
() => NativeImage.parse(new Uint8Array([0, 1, 2, 3, 4, 5])),
|
||||
/Failed to (detect|decode) image/,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid format number", async () => {
|
||||
const img = await NativeImage.parse(createTestPng());
|
||||
await assert.rejects(() => img.encode(99, 100), /Invalid image format/);
|
||||
});
|
||||
});
|
||||
28
packages/native/src/image/index.ts
Normal file
28
packages/native/src/image/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Native image processing module using N-API.
|
||||
*
|
||||
* High-performance image decode/encode/resize backed by the Rust `image` crate.
|
||||
*/
|
||||
|
||||
import { native } from "../native.js";
|
||||
import type { NativeImageHandle } from "./types.js";
|
||||
import { ImageFormat, SamplingFilter } from "./types.js";
|
||||
|
||||
export { ImageFormat, SamplingFilter };
|
||||
export type { NativeImageHandle };
|
||||
|
||||
const NativeImageClass = (native as Record<string, unknown>)
|
||||
.NativeImage as NativeImageConstructor;
|
||||
|
||||
interface NativeImageConstructor {
|
||||
parse(bytes: Uint8Array): Promise<NativeImageHandle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode image bytes (PNG, JPEG, WebP, GIF) into a NativeImage handle.
|
||||
*
|
||||
* Format is auto-detected from the byte content.
|
||||
*/
|
||||
export function parseImage(bytes: Uint8Array): Promise<NativeImageHandle> {
|
||||
return NativeImageClass.parse(bytes);
|
||||
}
|
||||
41
packages/native/src/image/types.ts
Normal file
41
packages/native/src/image/types.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/** Sampling filter for resize operations. */
|
||||
export enum SamplingFilter {
|
||||
/** Nearest-neighbor sampling (fast, low quality). */
|
||||
Nearest = 1,
|
||||
/** Triangle filter (linear interpolation). */
|
||||
Triangle = 2,
|
||||
/** Catmull-Rom filter with sharper edges. */
|
||||
CatmullRom = 3,
|
||||
/** Gaussian filter for smoother results. */
|
||||
Gaussian = 4,
|
||||
/** Lanczos3 filter for high-quality downscaling. */
|
||||
Lanczos3 = 5,
|
||||
}
|
||||
|
||||
/** Output image format for encoding. */
|
||||
export enum ImageFormat {
|
||||
/** PNG (lossless, quality ignored). */
|
||||
PNG = 0,
|
||||
/** JPEG (lossy, quality 0-100). */
|
||||
JPEG = 1,
|
||||
/** WebP (lossless, quality ignored). */
|
||||
WebP = 2,
|
||||
/** GIF (quality ignored). */
|
||||
GIF = 3,
|
||||
}
|
||||
|
||||
/** Native image handle returned from parse(). */
|
||||
export interface NativeImageHandle {
|
||||
/** Image width in pixels. */
|
||||
readonly width: number;
|
||||
/** Image height in pixels. */
|
||||
readonly height: number;
|
||||
/** Encode to bytes in the specified format. Returns a Promise. */
|
||||
encode(format: number, quality: number): Promise<number[]>;
|
||||
/** Resize to the specified dimensions. Returns a new NativeImage Promise. */
|
||||
resize(
|
||||
width: number,
|
||||
height: number,
|
||||
filter: SamplingFilter,
|
||||
): Promise<NativeImageHandle>;
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@
|
|||
* @gsd/native — High-performance Rust modules exposed via N-API.
|
||||
*
|
||||
* Modules:
|
||||
* - ast: AST-aware structural search and rewrite
|
||||
* - clipboard: native clipboard access (text + image)
|
||||
* - grep: ripgrep-backed regex search (content + filesystem)
|
||||
* - image: decode, encode, and resize images
|
||||
*/
|
||||
|
||||
export {
|
||||
|
|
@ -34,3 +36,6 @@ export type {
|
|||
AstReplaceOptions,
|
||||
AstReplaceResult,
|
||||
} from "./ast/index.js";
|
||||
|
||||
export { parseImage, ImageFormat, SamplingFilter } from "./image/index.js";
|
||||
export type { NativeImageHandle } from "./image/index.js";
|
||||
|
|
|
|||
|
|
@ -48,4 +48,5 @@ export const native = loadNative() as {
|
|||
readImageFromClipboard: () => Promise<unknown>;
|
||||
astGrep: (options: unknown) => unknown;
|
||||
astEdit: (options: unknown) => unknown;
|
||||
NativeImage: unknown;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue