@watermarker/react-native
The React Native binding: a TurboModule over the shared native packages —
WatermarkerKit on iOS, the io.github.alexdonh:watermarker AAR on Android.
Everything crossing the bridge is a short string: two file paths, the options JSON, and a small assets array. The photo itself never crosses — the engine reads and writes it by path. That is why this is a TurboModule and not a JSI binding: JSI exists to avoid serializing large payloads, and there are none.
New architecture only.
Architecture
bindings/react-native/
src/
index.tsx the API: watermarkFile / watermarkFiles / watermarkBase64
NativeWatermarker.ts the TurboModule spec, which codegen reads
options.ts typed options schema (Layer/Layout/Style/...)
ios/Watermarker.mm ObjC++ module over the C ABI, via WatermarkerKit
android/src/
main/java/.../WatermarkerModuleImpl.kt the shared implementation
newarch/, oldarch/ the two module shells
watermarker.podspec depends on the WatermarkerKit pod
The JSON options schema is identical on every platform.
Install
npm install @watermarker/react-native
The package autolinks. On iOS, the engine ships separately as the prebuilt
WatermarkerKit pod; add one line to your ios/Podfile pointing at the matching
release so CocoaPods can resolve it:
pod 'WatermarkerKit', :podspec =>
'https://github.com/alexdonh/watermarker/releases/download/v<version>/WatermarkerKit.podspec'
cd ios && pod install
On Android the engine resolves from Maven Central, so just keep
mavenCentral() in your app's repositories; no credentials or other setup is
needed.
Usage
import {
watermarkFile, watermarkFiles, watermarkBase64,
presetNames, presetOptions, abiVersion,
} from '@watermarker/react-native';
const options = { layers: [{ type: 'text', value: '© Alex Do', layout: 'corner' }] };
// By path: the photo never crosses the bridge.
await watermarkFile(photo.uri, outPath, options);
// One prepared mark for a whole roll. Every photo after the first is only a
// blend, which is what makes this worth using over a loop.
await watermarkFiles(photos.map(p => ({ inputPath: p.uri, outputPath: out(p) })), options);
// Base64 in, base64 out, for a live preview with no file churn.
const preview = await watermarkBase64(base64, options);
// A preset is never merged into an object you wrote; this is how to start from one.
const layout = { ...(await presetOptions('layout', 'mesh')), gap: '8%' };
Inline logo and font bytes are source / font strings in the options; they are
split out and uploaded for you, so asset ids stay internal.
Paths
A file:// URL — what react-native-image-picker hands back on iOS — is turned
into a filesystem path for you. An Android content:// URI is not a path at all
and cannot be made into one; copy it to the cache directory first.
See the demo in examples/react-native.