104 lines
3.0 KiB
Dart
104 lines
3.0 KiB
Dart
import 'dart:convert';
|
||
import 'dart:typed_data';
|
||
import 'package:image/image.dart' as img;
|
||
|
||
class SteganographyUtil {
|
||
static const String MAGIC_HEADER = 'SGWCMAID';
|
||
|
||
/// 图片隐写:写入数据
|
||
static Uint8List hideDataInImage(Uint8List imageBytes, String secretData) {
|
||
// 1. 解码图片
|
||
final image = img.decodeImage(imageBytes);
|
||
if (image == null) throw Exception('图片解码失败');
|
||
|
||
// 2. 准备数据:必须以 MAGIC_HEADER 开头,并以 \0 结尾
|
||
final dataBytes = utf8.encode(secretData);
|
||
final payload = Uint8List(dataBytes.length + 1);
|
||
payload.setRange(0, dataBytes.length, dataBytes);
|
||
payload[dataBytes.length] = 0; // 终止符
|
||
|
||
// 检查容量
|
||
if (payload.length * 8 > image.width * image.height) {
|
||
throw Exception('图片像素不足以容纳数据');
|
||
}
|
||
|
||
int payloadIndex = 0;
|
||
int bitIndex = 7; // MSB First (最高位优先)
|
||
|
||
// ✅ 修复:必须严格按照 Java 的 (y, x) 顺序遍历,确保坐标对齐
|
||
bool finished = false;
|
||
for (int y = 0; y < image.height; y++) {
|
||
for (int x = 0; x < image.width; x++) {
|
||
if (payloadIndex >= payload.length) {
|
||
finished = true;
|
||
break;
|
||
}
|
||
|
||
// 获取当前坐标的像素
|
||
final pixel = image.getPixel(x, y);
|
||
|
||
// 取出当前字节的特定位
|
||
int currentByte = payload[payloadIndex];
|
||
int bit = (currentByte >> bitIndex) & 1;
|
||
|
||
// ✅ 修复:通过 red 分量操作,屏蔽不同图片格式(RGBA/BGRA)的底层差异
|
||
int r = pixel.r.toInt();
|
||
int g = pixel.g.toInt();
|
||
int b = pixel.b.toInt();
|
||
int a = pixel.a.toInt();
|
||
|
||
// 修改 R 通道最低位
|
||
r = (r & 0xFE) | bit;
|
||
|
||
// 写回像素(保持其他通道不变)
|
||
image.setPixel(x, y, img.ColorRgba8(r, g, b, a));
|
||
|
||
bitIndex--;
|
||
if (bitIndex < 0) {
|
||
bitIndex = 7;
|
||
payloadIndex++;
|
||
}
|
||
}
|
||
if (finished) break;
|
||
}
|
||
|
||
// ✅ 必须导出为 PNG,防止 JPEG 压缩破坏 LSB 位
|
||
return Uint8List.fromList(img.encodePng(image));
|
||
}
|
||
|
||
/// 提取数据(本地调试用,逻辑需与写入完全对称)
|
||
static String? extractDataFromImage(Uint8List imageBytes) {
|
||
final image = img.decodeImage(imageBytes);
|
||
if (image == null) return null;
|
||
|
||
List<int> extractedBytes = [];
|
||
int currentByte = 0;
|
||
int bitCount = 0;
|
||
|
||
outer:
|
||
for (int y = 0; y < image.height; y++) {
|
||
for (int x = 0; x < image.width; x++) {
|
||
final pixel = image.getPixel(x, y);
|
||
int bit = pixel.r.toInt() & 1;
|
||
|
||
// MSB First 组装字节
|
||
currentByte = (currentByte << 1) | bit;
|
||
bitCount++;
|
||
|
||
if (bitCount == 8) {
|
||
if (currentByte == 0) break outer; // 结束符
|
||
extractedBytes.add(currentByte);
|
||
currentByte = 0;
|
||
bitCount = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (extractedBytes.isEmpty) return null;
|
||
try {
|
||
return utf8.decode(extractedBytes);
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
} |