embed static method

Uint8List embed({
  1. required Uint8List imageBytes,
  2. required String payload,
  3. required String key,
  4. ForensicConfig config = const ForensicConfig(),
})

Embeds payload invisibly into imageBytes using key.

Returns the watermarked image as PNG-encoded bytes.

Throws ArgumentError if:

  • payload is empty
  • key is empty
  • imageBytes cannot 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));
}