react-native-image-marker-editor@0.3 is the optional React Native and Web
interaction layer for react-native-image-marker@2.1. It owns Recipe state,
selection, gestures, history, panels, templates, and persistence. Core owns
image decoding, composition, preview rendering, and encoding.
npm install react-native-image-marker@^2.1 \
react-native-image-marker-editor@^0.3The Editor depends on the platform-neutral @image-marker/recipe package and
uses the same Recipe v2 document as Web, Node, and CLI. Import the Core adapter
only when previews or exports should run on the device:
import { createCoreEditorAdapter } from 'react-native-image-marker-editor/core-adapter';Applications can inject another ImageMarkerEditorRenderAdapter when rendering
belongs on a server.
Core 2.1 automatically reads the original dimensions and encoded orientation.
The application no longer needs to maintain a matching sourceSize.
import * as React from 'react';
import {
Button,
Image,
StyleSheet,
View,
useWindowDimensions,
} from 'react-native';
import {
ImageMarkerEditor,
ImageMarkerEditorAssetPanel,
ImageMarkerEditorController,
ImageMarkerEditorInspector,
ImageMarkerEditorLayerPanel,
ImageMarkerEditorToolbar,
} from 'react-native-image-marker-editor';
import { createCoreEditorAdapter } from 'react-native-image-marker-editor/core-adapter';
const background = require('../assets/background.jpg');
const logo = require('../assets/logo.png');
const adapter = createCoreEditorAdapter(1024);
export function WatermarkEditor() {
const window = useWindowDimensions();
const width = Math.min(Math.max(window.width - 32, 280), 720);
const height = Math.round((width * 9) / 16);
const controller = React.useMemo(
() =>
new ImageMarkerEditorController({
schemaVersion: 2,
layers: [
{
id: 'title',
name: 'Campaign title',
type: 'text',
text: 'IMAGE MARKER 2.1',
position: { X: 160, Y: 120 },
style: {
color: '#FFFFFF',
fontSize: 64,
bold: true,
},
},
{
id: 'logo',
name: 'Brand mark',
type: 'image',
src: logo,
position: { X: 1280, Y: 720 },
scale: 0.4,
},
],
output: { saveFormat: 'png' },
}),
[]
);
const [resultUri, setResultUri] = React.useState<string>();
const request = () => ({
recipe: controller.exportRecipe(),
input: { backgroundImage: { src: background } },
control: { timeoutMs: 20_000 },
});
const renderPreview = async () => {
const result = await adapter.renderPreview(request());
setResultUri(result.uri);
};
const exportOriginal = async () => {
const result = await adapter.exportOriginal(request());
setResultUri(result.final.uri);
};
return (
<View>
<ImageMarkerEditorToolbar controller={controller} />
<ImageMarkerEditor
adapter={adapter}
background={
<Image
resizeMode="contain"
source={background}
style={StyleSheet.absoluteFill}
/>
}
controller={controller}
height={height}
snapThreshold={8}
source={background}
width={width}
/>
<ImageMarkerEditorLayerPanel controller={controller} />
<ImageMarkerEditorInspector
brandKit={{
colors: ['#FFFFFF', '#5271FF', '#FF5B45'],
fonts: ['Inter', 'Courier'],
}}
controller={controller}
/>
<ImageMarkerEditorAssetPanel
assets={[{ id: 'logo', name: 'Logo', source: logo }]}
controller={controller}
/>
<Button title="Render Core preview" onPress={renderPreview} />
<Button title="Export original" onPress={exportOriginal} />
{resultUri && (
<Image
resizeMode="contain"
source={{ uri: resultUri }}
style={{ width, height }}
/>
)}
</View>
);
}adapter.renderPreview() creates a bounded render while preserving original
coordinates. adapter.exportOriginal() returns the visible render plus the
final result after optional invisible-watermark or Content Credentials work.
The controller provides atomic, undoable commands rather than exposing mutable state:
controller.selectLayers(['title', 'logo']);
controller.groupLayers();
controller.alignLayers('center', measuredBounds);
controller.distributeLayers('horizontal', measuredBounds);
controller.duplicateLayers();
controller.updateTextLayer('title', {
text: 'Launch',
alpha: 0.8,
blendMode: 'overlay',
style: {
fontName: 'Inter',
fontSize: 64,
color: '#FFFFFF',
strokeStyle: { color: '#0F172A', width: 2 },
},
});
controller.replaceImage('logo', replacementLogo);The built-in Toolbar, Inspector, Layer Panel, and Asset Panel cover:
- text, typography, color, opacity, stroke, and blend mode;
- image add and replacement;
- duplicate, rename, lock, visibility, delete, and ordering;
- multi-select, group, ungroup, align, distribute, copy, and paste;
- undo, redo, zoom, pan, fit, safe area, snapping, resize, and rotation.
Long-press a layer row to toggle it into a multi-selection. A copied selection can also cross document boundaries:
const clipboardJson = controller.copyLayers();
otherController.pasteLayers(clipboardJson, { x: 20, y: 20 });Cmd/Ctrl+Z,Shift+Cmd/Ctrl+Z, andCmd/Ctrl+Y: undo and redo.Cmd/Ctrl+A/C/X/V/D: select all, copy, cut, paste, and duplicate.Cmd/Ctrl+GandShift+Cmd/Ctrl+G: group and ungroup.- Arrow keys: move by one pixel; add Shift to move by ten.
[and]: move the primary layer through the z-order.Cmd/Ctrl+0,+, and-: fit and zoom.- Delete or Backspace: remove the editable selection.
const controller = new ImageMarkerEditorController({
document: initialRecipe,
autosave: {
key: 'campaign-draft',
debounceMs: 350,
storage: {
load: (key) => AsyncStorage.getItem(key),
save: (key, value) => AsyncStorage.setItem(key, value),
remove: (key) => AsyncStorage.removeItem(key),
},
onError: reportAutosaveError,
},
onChange: setEditorState,
});
await controller.restoreAutosave();
await controller.flushAutosave();
// Parent-controlled synchronization without another undo record.
controller.replaceState(editorState);ImageMarkerEditor, Inspector, Layer Panel, and Asset Panel also accept a
controlled state prop. Autosave intentionally excludes invisible-watermark
keys, signing adapters, and export credentials; applications should obtain
those from protected runtime storage at export time.
Template text uses the same variables and conditional-layer materializer as the shared Recipe package:
import {
createEditorTemplate,
materializeEditorTemplate,
} from 'react-native-image-marker-editor';
const template = createEditorTemplate({
id: 'social-card',
name: 'Social card',
recipe: {
schemaVersion: 2,
layers: [
{ id: 'title', type: 'text', text: 'Hello {{name}}' },
{
id: 'sale',
type: 'text',
text: 'Sale',
visibleWhen: { variable: 'showSale', equals: true },
},
],
output: { saveFormat: 'png' },
},
});
const recipe = materializeEditorTemplate(template, {
variables: { name: 'Ada', showSale: false },
});Use createEditorBrandKit, createBrandTextLayer, and
createBrandLogoLayer to share approved colors, fonts, and Logo assets.
- Canvas slots:
canvasOverlay,selectionOverlay, andsourceLoading. - Layer rendering:
renderLayerandgetLayerSize. - Panel slots: header, footer, empty state, layer label, and asset renderer.
- Plugin slots: toolbar actions, inspector sections, and canvas overlays.
Plugins are ordinary objects and do not receive global access:
const campaignPlugin = {
id: 'campaign-rules',
toolbarActions: [
{
id: 'approve',
label: 'Approve',
onPress: ({ state }) => approve(state.recipe),
},
],
renderInspectorSection: ({ state }) => (
<CampaignRules recipe={state.recipe} />
),
};- Numeric Recipe geometry remains in decoded original-image pixels.
- Core 2.1 obtains display dimensions after applying encoded orientation.
- The surface projects that coordinate space into the zoomed viewport.
- The adapter projects the same Recipe into bounded previews and preserves it for original export.
- Pass
sourceSizeonly to override automatic detection or when a custom adapter cannot inspect the source. - Fonts, antialiasing, decoding, and encoding may still differ slightly across platform renderers.
- Native Editor example
- Controller and component tests
- Browser Playground
- Integration guide
- Generated API reference
The native example exposes stable testID values for panels, layer controls,
canvas handles, toolbar actions, preview, export, status, result, and Recipe
assertions. CI covers unit, component, consumer, iOS, and Android workflows.