A Flutter package for visible & invisible watermarking. Part of the FlutterPlaza Security Suite.
- Visible watermarks deter screenshots by tiling user-identifying text over content
- Forensic (invisible) watermarks embed traceable data directly into image pixels using spread-spectrum LSB modulation — enabling post-leak identification even when visible watermarks are cropped out
Tile user-identifying text (email, user ID, timestamp) across any widget with configurable opacity, rotation, font size, and staggered brick pattern.
Embed traceable data into image pixels. The watermarked image looks identical to the original — but the hidden payload can be extracted with the correct secret key.
Visible watermark (Watermark widget):
- Diagonal tiled text overlay with configurable opacity, rotation, font size, and spacing
- Staggered brick pattern (alternate rows offset) for crop resistance
IgnorePointer— watermark does not intercept touch eventsRepaintBoundary— watermark repaints are isolated from child contentenabledflag to toggle visibility without removing from widget tree
Forensic (invisible) watermark (ForensicWatermark + ForensicWatermarkImage):
- Spread-spectrum LSB embedding in blue channel at PRNG-selected pixel positions
- Majority voting with configurable redundancy for error correction
- Magic number validation for wrong-key detection
ForensicWatermarkImagewidget with background isolate processing- CLI tools for embedding and extraction (
dart run secure_watermark:embed/extract)
Pure Dart/Flutter — works on all platforms (no plugin, no method channels).
dependencies:
secure_watermark: ^0.3.1import 'package:secure_watermark/secure_watermark.dart';
Watermark(
text: 'user@example.com 2026-02-15',
style: const WatermarkStyle(opacity: 0.2, rotate: -45),
enabled: true,
child: MyProtectedContent(),
)Watermark(
text: 'user@example.com',
enabled: isWatermarkVisible, // controlled by setState, provider, etc.
child: MyProtectedContent(),
)const style = WatermarkStyle(
opacity: 0.2, // 0.0–1.0 (default: 0.15)
rotate: -45, // degrees (default: -30)
fontSize: 14, // logical pixels (default: 16)
rowSpacing: 60, // vertical gap between rows (default: 80)
columnSpacing: 100, // horizontal gap between columns (default: 120)
fontWeight: FontWeight.bold,
staggered: true, // brick pattern offset (default: true)
);import 'package:secure_watermark/secure_watermark.dart';
// Embed a payload into an image
final watermarked = ForensicWatermark.embed(
imageBytes: pngBytes, // PNG-encoded Uint8List
payload: 'user@example.com', // data to hide
key: 'secret-key-123', // secret key for PRNG seeding
);
// Extract the payload later
final payload = ForensicWatermark.extract(
imageBytes: watermarked,
key: 'secret-key-123',
);
print(payload); // user@example.comForensicWatermarkImage(
imageBytes: pngBytes,
payload: 'user@example.com',
secretKey: 'key-123',
config: const ForensicConfig(redundancy: 5),
placeholder: const CircularProgressIndicator(),
errorBuilder: (ctx, error) => Text('Failed: $error'),
fit: BoxFit.contain,
)# Embed
dart run secure_watermark:embed -i photo.png -o watermarked.png -p "user@example.com" -k "secret"
# Extract
dart run secure_watermark:extract -i watermarked.png -k "secret"| Property | Type | Default | Description |
|---|---|---|---|
text |
String |
required | Text to tile as the watermark |
style |
WatermarkStyle |
WatermarkStyle() |
Visual configuration |
enabled |
bool |
true |
Whether the overlay is visible |
child |
Widget |
required | Content to display beneath the watermark |
| Property | Type | Default | Description |
|---|---|---|---|
opacity |
double |
0.15 |
Text opacity (0.0–1.0) |
rotate |
double |
-30 |
Rotation angle in degrees |
textColor |
Color |
Color(0xFF9E9E9E) |
Text color (opacity applied separately) |
fontSize |
double |
16 |
Font size in logical pixels |
rowSpacing |
double |
80 |
Vertical spacing between rows |
columnSpacing |
double |
120 |
Horizontal spacing between columns |
fontWeight |
FontWeight |
FontWeight.normal |
Font weight |
staggered |
bool |
true |
Offset alternate rows for brick pattern |
| Method | Returns | Description |
|---|---|---|
embed(imageBytes, payload, key, [config]) |
Uint8List |
Embeds payload into PNG, returns watermarked PNG bytes |
extract(imageBytes, key, [config]) |
String? |
Extracts payload, returns null on wrong key or no watermark |
| Property | Type | Default | Description |
|---|---|---|---|
redundancy |
int |
5 |
Odd positive int — copies for majority voting |
bitsPerChannel |
int |
1 |
LSBs per channel (only 1 supported) |
| Property | Type | Default | Description |
|---|---|---|---|
imageBytes |
Uint8List |
required | Source PNG image |
payload |
String |
required | Data to embed invisibly |
secretKey |
String |
required | Secret key for PRNG seeding |
config |
ForensicConfig |
ForensicConfig() |
Algorithm configuration |
placeholder |
Widget? |
null |
Shown during processing |
errorBuilder |
Widget Function(BuildContext, Object)? |
null |
Called on failure |
fit |
BoxFit |
BoxFit.contain |
Image fit mode |
alignment |
Alignment |
Alignment.center |
Image alignment |
The Watermark widget uses a Stack to layer a CustomPaint overlay on top of the child content. The WatermarkPainter:
- Builds a
ui.Paragraphwith the watermark text - Translates and rotates the canvas around its center
- Tiles the paragraph in a grid covering the full diagonal extent (ensuring no gaps after rotation)
- In staggered mode, offsets alternate rows by half the column spacing — creating a brick pattern that is harder to crop out
The overlay is wrapped in IgnorePointer (touches pass through) and RepaintBoundary (repaints are isolated from the child).
The ForensicWatermark uses spread-spectrum LSB (Least Significant Bit) modulation:
Embedding:
- Decode PNG to RGBA pixels
- Convert payload to UTF-8 bytes, prepend 16-bit magic number (
0x574D) + 32-bit length header - Seed a PRNG with a DJB2 hash of the secret key
- Generate unique random pixel positions (Set-based collision avoidance)
- Modify the LSB of the blue channel at each position to encode bits
- Repeat the payload
redundancytimes for error correction - Re-encode to PNG
Extraction:
- Decode PNG, seed PRNG with same key
- Read header bits across all redundancy copies, majority-vote each bit
- Verify magic number — return
nullif wrong (wrong key) - Read payload bits, majority-vote, decode UTF-8
The example/ directory contains a full demo app with two tabs:
- Visible — interactive controls for the
Watermarkwidget (opacity, angle, font size, staggered pattern) - Forensic — embed/extract invisible watermarks with configurable payload, key, and redundancy
cd example
flutter run- no_screenshot — Screenshot & recording prevention
- no_tapjack — Tapjacking & overlay attack detection
BSD 3-Clause License. See LICENSE for details.

