embed static method
Uint8List
embed({
- required Uint8List imageBytes,
- required String payload,
- required String key,
- ForensicConfig config = const ForensicConfig(),
Embeds payload invisibly into imageBytes using key.
Returns the watermarked image as PNG-encoded bytes.
Throws ArgumentError if:
payloadis emptykeyis emptyimageBytescannot be decoded as an image- The image has insufficient pixel capacity for the payload
Implementation
static Uint8List embed({
required Uint8List imageBytes,
required String payload,
required String key,
ForensicConfig config = const ForensicConfig(),
}) {
if (payload.isEmpty) {
throw ArgumentError.value(payload, 'payload', 'must not be empty');
}
if (key.isEmpty) {
throw ArgumentError.value(key, 'key', 'must not be empty');
}
final image = img.decodePng(imageBytes);
if (image == null) {
throw ArgumentError.value(
imageBytes,
'imageBytes',
'could not decode PNG image',
);
}
final payloadBytes = utf8.encode(payload);
final bits = _buildBitStream(payloadBytes);
final totalBits = bits.length * config.redundancy;
final pixelCount = image.width * image.height;
if (totalBits > (pixelCount * _kMaxCapacityFraction).floor()) {
throw ArgumentError(
'Payload too large: needs $totalBits positions but image only '
'supports ${(pixelCount * _kMaxCapacityFraction).floor()} '
'(${image.width}x${image.height} pixels at '
'${(_kMaxCapacityFraction * 100).toInt()}% capacity)',
);
}
final seed = _djb2(key);
final rng = Random(seed);
final used = <int>{};
// Two-phase position generation (matches extract's two-phase read).
const headerBits = 48; // 16-bit magic + 32-bit length
final headerTotalBits = headerBits * config.redundancy;
final payloadBitCount = payloadBytes.length * 8;
final payloadTotalBits = payloadBitCount * config.redundancy;
final headerPositions =
_generatePositions(rng, pixelCount, headerTotalBits, used);
final payloadPositions =
_generatePositions(rng, pixelCount, payloadTotalBits, used);
// Write header bits at header positions.
final headerBitStream = bits.sublist(0, headerBits);
var posIndex = 0;
for (var copy = 0; copy < config.redundancy; copy++) {
for (final bit in headerBitStream) {
final pixelIndex = headerPositions[posIndex++];
final x = pixelIndex % image.width;
final y = pixelIndex ~/ image.width;
final pixel = image.getPixel(x, y);
pixel.b = (pixel.b.toInt() & ~1) | bit;
}
}
// Write payload bits at payload positions.
final payloadBitStream = bits.sublist(headerBits);
posIndex = 0;
for (var copy = 0; copy < config.redundancy; copy++) {
for (final bit in payloadBitStream) {
final pixelIndex = payloadPositions[posIndex++];
final x = pixelIndex % image.width;
final y = pixelIndex ~/ image.width;
final pixel = image.getPixel(x, y);
pixel.b = (pixel.b.toInt() & ~1) | bit;
}
}
return Uint8List.fromList(img.encodePng(image));
}