Native C/C++ image processing for React Native. Lossless JPEG ops, full format support, MIT-licensed.
- Format conversion — JPEG, PNG, WebP, BMP, TIFF, HEIC, AVIF
- Lossless JPEG — rotate 90/180/270, crop at MCU boundaries, strip EXIF (zero quality loss)
- Pixel operations — resize (Lanczos/bilinear/nearest), crop, rotate, flip
- EXIF metadata — read and strip without re-encoding
- Platform HEIC — iOS ImageIO + Android HeifWriter (no LGPL dependencies)
- Synchronous JSI — all operations run on the JS thread via C++ (no async bridge)
npm install @toolsmithhq/imagecore-native @toolsmithhq/imagecore-filesimport { convertFile, resizeFile, getFileImageInfo } from '@toolsmithhq/imagecore-files';
// Convert HEIC to JPEG
const jpegUri = await convertFile('file:///path/to/photo.heic', 'jpeg', 0.85);
// Resize to thumbnail
const thumbUri = await resizeFile(jpegUri, 200, 200, 'jpeg');
// Get image dimensions
const info = await getFileImageInfo(jpegUri);
console.log(info.width, info.height, info.format);That's it. File in, file out. No ArrayBuffers, no manual memory management.
The recommended way to use imagecore in React Native. All functions take file URIs and return file URIs.
import { convertFile } from '@toolsmithhq/imagecore-files';
const pngUri = await convertFile(sourceUri, 'png'); // default quality: 0.85
const jpegUri = await convertFile(sourceUri, 'jpeg', 0.9); // custom quality
const webpUri = await convertFile(sourceUri, 'webp', 0.85);
const heicUri = await convertFile(sourceUri, 'heic', 0.8);import { resizeFile } from '@toolsmithhq/imagecore-files';
const resizedUri = await resizeFile(sourceUri, 800, 600, 'jpeg'); // default quality: 0.95
const thumbUri = await resizeFile(sourceUri, 200, 200, 'png', 0.8); // custom qualityimport { cropFile } from '@toolsmithhq/imagecore-files';
// cropFile(uri, x, y, width, height, outputFormat, quality?)
const croppedUri = await cropFile(sourceUri, 100, 50, 400, 300, 'jpeg'); // default quality: 0.95import { rotateFile } from '@toolsmithhq/imagecore-files';
const rotatedUri = await rotateFile(sourceUri, 90, 'jpeg'); // 90, 180, or 270. default quality: 0.95import { flipFile } from '@toolsmithhq/imagecore-files';
// flipFile(uri, horizontal, vertical, outputFormat, quality?). default quality: 0.95
const mirroredUri = await flipFile(sourceUri, true, false, 'png'); // horizontal flip
const flippedUri = await flipFile(sourceUri, false, true, 'png'); // vertical flip
const bothUri = await flipFile(sourceUri, true, true, 'png'); // bothimport { compressFile } from '@toolsmithhq/imagecore-files';
// Re-encode at lower quality
const compressedUri = await compressFile(sourceUri, 0.5, 'jpeg');Rotates JPEG without decoding pixels. Zero quality loss — rearranges DCT blocks directly.
import { losslessJpegRotate } from '@toolsmithhq/imagecore-files';
const rotatedUri = await losslessJpegRotate(sourceUri, 90); // 90, 180, or 270import { getFileImageInfo } from '@toolsmithhq/imagecore-files';
const info = await getFileImageInfo(sourceUri);
// { width: 4032, height: 3024, format: 'jpeg', hasExif: true, fileSize: 2048576 }import { readFileExif, stripFileExif } from '@toolsmithhq/imagecore-files';
// Read EXIF tags
const exif = await readFileExif(sourceUri);
// { Make: 'Apple', Model: 'iPhone 15', DateTime: '2024:01:15 10:30:00', ... }
// Strip all EXIF metadata (binary removal for JPEG — no re-encoding)
const strippedUri = await stripFileExif(sourceUri);import { readFileAsArrayBuffer, writeArrayBufferToFile } from '@toolsmithhq/imagecore-files';
// Read file to ArrayBuffer (for custom workflows)
const buffer = await readFileAsArrayBuffer(sourceUri);
// Write ArrayBuffer to cache file
const outputUri = writeArrayBufferToFile(buffer, 'png');For working with in-memory buffers, chaining multiple operations without intermediate files, or custom workflows.
npm install @toolsmithhq/imagecore-nativeimport { ImageCore } from '@toolsmithhq/imagecore-native';
// All methods are synchronous (JSI) and operate on ArrayBuffers.
// DecodedImage objects must be freed when done.
// Convert format (one-liner)
const pngBuffer = ImageCore.convert(inputBuffer, { format: 'png', quality: 0.9 });
// Lossless JPEG rotate (zero quality loss)
const rotated = ImageCore.jpegLosslessRotate(jpegBuffer, 90);
// Decode → resize → encode (manual pipeline)
const decoded = ImageCore.decode(inputBuffer);
const resized = ImageCore.resize(decoded, { width: 800, height: 600, filter: 'lanczos' });
const output = ImageCore.encode(resized, { format: 'webp', quality: 0.85 });
resized.free(); // release native memory
decoded.free();
// EXIF
const exif = ImageCore.readExif(jpegBuffer);
const stripped = ImageCore.stripExif(jpegBuffer);All methods are synchronous. data is ArrayBuffer, image is DecodedImage.
| Method | Description |
|---|---|
getImageInfo(data) |
Get dimensions, format, EXIF presence without full decode |
decode(data) |
Decode any format to RGBA pixels (returns DecodedImage) |
encode(image, opts) |
Encode pixels to any format (returns ArrayBuffer) |
convert(data, opts) |
Decode + encode in one call |
jpegLosslessRotate(data, 90|180|270) |
Lossless JPEG rotate via DCT block rearrangement |
jpegLosslessCrop(data, region) |
Lossless JPEG crop at MCU boundaries |
jpegStripExif(data) |
Binary EXIF removal from JPEG (no re-encode) |
resize(image, opts) |
Resize with Lanczos, bilinear, or nearest filter |
crop(image, region) |
Crop to { x, y, width, height } |
rotate(image, 90|180|270) |
Rotate decoded image |
flipHorizontal(image) |
Mirror horizontally |
flipVertical(image) |
Flip vertically |
readExif(data) |
Read EXIF metadata tags |
stripExif(data) |
Strip metadata from any format |
decode(), resize(), crop(), rotate(), flipHorizontal(), and flipVertical() return DecodedImage objects that hold native memory. Always call .free() when done:
const decoded = ImageCore.decode(buffer);
try {
const resized = ImageCore.resize(decoded, { width: 400, height: 300 });
try {
const output = ImageCore.encode(resized, { format: 'jpeg', quality: 0.85 });
// use output...
} finally {
resized.free();
}
} finally {
decoded.free();
}The file API (@toolsmithhq/imagecore-files) handles this automatically.
@toolsmithhq/imagecore-types ← shared TypeScript types
|
+---------+
| |
imagecore-native imagecore-web ← same API, different engines (JSI vs WASM)
|
imagecore-files ← high-level file URI convenience layer (React Native)
| Package | Install | Platform |
|---|---|---|
@toolsmithhq/imagecore-files |
npm install @toolsmithhq/imagecore-files |
React Native (iOS + Android) |
@toolsmithhq/imagecore-native |
npm install @toolsmithhq/imagecore-native |
React Native (iOS + Android) |
@toolsmithhq/imagecore-types |
npm install @toolsmithhq/imagecore-types |
All |
@toolsmithhq/imagecore-web |
npm install @toolsmithhq/imagecore-web |
Web (not yet available) |
packages/corecontains the C++ source. It is not published to npm — it gets compiled into prebuilt.afiles (iOS/Android) that ship inside@toolsmithhq/imagecore-native.
| Format | iOS | Android | macOS (tests) | WASM |
|---|---|---|---|---|
| JPEG | decode + encode | decode + encode | decode + encode | decode + encode |
| PNG | decode + encode | decode + encode | decode + encode | decode + encode |
| WebP | decode + encode | decode + encode | decode + encode | decode + encode |
| BMP | decode + encode | decode + encode | decode + encode | decode + encode |
| TIFF | decode + encode | decode + encode | decode + encode | decode + encode |
| HEIC | decode + encode | decode + encode | — | — |
| AVIF | — | — | decode + encode | decode + encode |
- HEIC uses platform APIs: iOS
ImageIO.framework, AndroidHeifWriter(API 30+). No LGPL dependencies. - AVIF uses libaom. Not cross-compiled for mobile yet — works on macOS and WASM only.
The web package (@toolsmithhq/imagecore-web) is not yet available. It will provide the same API backed by WASM:
import { createImageCore } from '@toolsmithhq/imagecore-web';
const core = await createImageCore(); // loads WASM module
const pngBuffer = core.convert(inputBuffer, { format: 'png', quality: 0.9 });
// Same API as @toolsmithhq/imagecore-nativeWhy no
imagecore-filesfor web? On React Native, reading a file URI into an ArrayBuffer requiresexpo-file-systemwith base64 encoding/decoding — that's the boilerplateimagecore-fileseliminates. On web, the browser has this built in:
// Web: reading a file is one line
const buffer = await file.arrayBuffer(); // File from <input type="file">
// Web: displaying the result is one line
const blob = new Blob([outputBuffer], { type: 'image/png' });
const url = URL.createObjectURL(blob); // use in <img src={url}>
// Web: downloading the result
const a = document.createElement('a');
a.href = url;
a.download = 'output.png';
a.click();No bridge package needed — the browser's File API handles it natively.
make test # 38 tests (no sample images)
make test-samples # 63 tests (with sample images)Or manually:
cd packages/core
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DIC_BUILD_TESTS=ON
cmake --build build --config Release -j$(sysctl -n hw.logicalcpu)
./build/test_imagecore # 38 tests
./build/test_imagecore "$(pwd)/tests/samples/" # 63 tests| Test file | What it tests |
|---|---|
test_format_detection.cpp |
Magic byte detection, getImageInfo, null handling |
test_codec_roundtrips.cpp |
Encode/decode round-trips, cross-format conversion |
test_lossless_jpeg.cpp |
Lossless JPEG rotate, EXIF strip |
test_pixel_ops.cpp |
Crop, resize, rotate, flip, integration |
test_sample_images.cpp |
Real sample file decode, convert, operations |
make build-ios # iOS device + simulator (arm64)
make build-android # Android arm64-v8a| Library | License | Purpose |
|---|---|---|
| libjpeg-turbo | BSD | JPEG codec + lossless transforms |
| libpng | libpng (MIT-like) | PNG codec |
| libwebp | BSD | WebP codec (lossy + lossless) |
| libtiff | BSD-like | TIFF codec |
| libavif | BSD | AVIF container (macOS/WASM only) |
| libaom | BSD | AV1 codec for AVIF (macOS/WASM only) |
| zlib | zlib | Compression (used by libpng, libtiff) |
HEIC uses platform APIs (iOS ImageIO, Android HeifWriter) — no LGPL dependencies on mobile.
MIT