extract static method

String? extract({
  1. required Uint8List imageBytes,
  2. required String key,
  3. ForensicConfig config = const ForensicConfig(),
})

Extracts a previously embedded payload from imageBytes using key.

Returns the payload string, or null if the key is wrong, no watermark is found, or the data is corrupted.

Implementation

static String? extract({
  required Uint8List imageBytes,
  required String key,
  ForensicConfig config = const ForensicConfig(),
}) {
  if (key.isEmpty) return null;

  final image = img.decodePng(imageBytes);
  if (image == null) return null;

  final pixelCount = image.width * image.height;
  final seed = _djb2(key);

  // Phase 1: read header (16-bit magic + 32-bit length = 48 bits) per copy.
  const headerBits = 48;
  final headerTotalBits = headerBits * config.redundancy;

  if (headerTotalBits > (pixelCount * _kMaxCapacityFraction).floor()) {
    return null;
  }

  final rng = Random(seed);
  final used = <int>{};
  final headerPositions =
      _generatePositions(rng, pixelCount, headerTotalBits, used);

  final headerBitValues = <int>[];
  for (final pixelIndex in headerPositions) {
    final x = pixelIndex % image.width;
    final y = pixelIndex ~/ image.width;
    final pixel = image.getPixel(x, y);
    headerBitValues.add(pixel.b.toInt() & 1);
  }

  final headerVoted =
      _majorityVote(headerBitValues, headerBits, config.redundancy);

  // Verify magic number.
  var magic = 0;
  for (var i = 0; i < 16; i++) {
    magic = (magic << 1) | headerVoted[i];
  }
  if (magic != _kMagic) return null;

  // Read byte count.
  var byteCount = 0;
  for (var i = 16; i < 48; i++) {
    byteCount = (byteCount << 1) | headerVoted[i];
  }

  if (byteCount <= 0 || byteCount > 10 * 1024 * 1024) return null;

  // Phase 2: read payload bits.
  final payloadBitCount = byteCount * 8;
  final payloadTotalBits = payloadBitCount * config.redundancy;
  final totalBitsNeeded = headerTotalBits + payloadTotalBits;

  if (totalBitsNeeded > (pixelCount * _kMaxCapacityFraction).floor()) {
    return null;
  }

  final payloadPositions =
      _generatePositions(rng, pixelCount, payloadTotalBits, used);

  final payloadBitValues = <int>[];
  for (final pixelIndex in payloadPositions) {
    final x = pixelIndex % image.width;
    final y = pixelIndex ~/ image.width;
    final pixel = image.getPixel(x, y);
    payloadBitValues.add(pixel.b.toInt() & 1);
  }

  final payloadVoted =
      _majorityVote(payloadBitValues, payloadBitCount, config.redundancy);

  // Decode bits → bytes → UTF-8.
  final bytes = Uint8List(byteCount);
  for (var i = 0; i < byteCount; i++) {
    var byte = 0;
    for (var b = 0; b < 8; b++) {
      byte = (byte << 1) | payloadVoted[i * 8 + b];
    }
    bytes[i] = byte;
  }

  try {
    return utf8.decode(bytes);
  } catch (_) {
    return null;
  }
}