Java课设:数字水印处理与解析器开发
前言
想养成写日记的习惯真不容易。最近比较懒散,复习不想复,项目又做完了,处于一种能干些什么,但是不太想干,但是不干些什么又浑身难受的处境。其实完全就不是匀不出来时间的问题,只是自己太懒了而已。想要获得什么,就肯定会牺牲什么,珍惜一点自己的时间!这几天会做一做Java的课设,基于学到的新东西,不时更新几篇blog。
日程
6.4
今天坚定了一下信念,开始着手课设的事情。被一个bug卡了不少时间,太依赖AI了,应该多多自己思考的。干到11点,不想滥用AI,进度比较慢,只完成了图片部分的注入和解析。
6.5
感觉没什么内容啊,不到两天就能做好。
学习内容
省流
- Java课设初步预设
- LSB的原理及简单实例
- 解析LSB和简单的BitStream实现
- 文件选择对话框与基于
<a>
超链接标签文件下载 - 细节:
@Controller
和@RestController
的区别
Java课设初步预设
这次的题材相对宽松,可以自己决定题材。我打算做一个轻量化的数字水印处理和解析器,并基于Nodejs实现UI界面。
基本需求
- 基于LSB为PNG格式图片生成和解析水印。
- 水印可以包括mp3,mp4,png,文本信息等内容,并且通过标记信息来区分。
- 通过java-node实现前后端的进程级通信。
进阶需求
- 提高数字水印的鲁棒性。
- 批量,工作流式处理数字水印(线程池模式,为每个任务分配一个线程)。
- 可以对视频,音频添加和解析数字水印。
超级进阶!
- 使用对抗神经网络来超级提高数字水印的鲁棒性。
LSB的原理及简单实例
RGB图像上的每个像素点都由多个二进制位组成。例如,RGB(150, 200, 100)的二进制表示:
- R: 10010110
- G: 11001000
- B: 01100100
LSB(最低有效位)就是用要隐藏的信息位替换像素值的最低有效位。低有效位的变化对整体颜色/亮度影响很小,人眼通常难以察觉。通常可以使用的像素位是1-2位。
简单实例
// 嵌入水印到图像的最低有效位
/**
在Java中,BufferedImage.getRGB()返回的是一个32位的int值,包含4个8位通道(ARGB):A(Alpha,透明度):bits 24-31R(Red,红色):bits 16-23G(Green,绿色):bits 8-15B(Blue,蓝色):bits 0-7
*/
public static void embedLSBImage(BufferedImage image, byte[] secretData) {int width = image.getWidth();int height = image.getHeight();int dataIndex = 0;int bitIndex = 0;for (int y = 0; y < height; y++) {for (int x = 0; x < width; x++) {if (dataIndex >= secretData.length) return;int rgb = image.getRGB(x, y);int r = (rgb >> 16) & 0xFF; //&0xFF保留最低8位int g = (rgb >> 8) & 0xFF;int b = rgb & 0xFF;// 嵌入到R通道if (bitIndex < 8) {int bit = (secretData[dataIndex] >> (7 - bitIndex)) & 1;r = (r & 0xFE) | bit; //& 0xFE 最低位置0bitIndex++;}// 嵌入到G通道if (bitIndex < 8) {int bit = (secretData[dataIndex] >> (7 - bitIndex)) & 1;g = (g & 0xFE) | bit;bitIndex++;}// 嵌入到B通道if (bitIndex < 8) {int bit = (secretData[dataIndex] >> (7 - bitIndex)) & 1;b = (b & 0xFE) | bit;bitIndex++;}if (bitIndex >= 8) {bitIndex = 0;dataIndex++;}image.setRGB(x, y, (r << 16) | (g << 8) | b);}}
}
为了能够正确地解析水印,设置信息头部:4位长度+1位类型标记+数据。
private static byte[] generateHeader(Object obj) {byte[] type = new byte[0];if(obj instanceof String){type = MarkingType.s.toString().getBytes();}else if(obj instanceof File){if(((File) obj).getName().endsWith(".png")){type = MarkingType.p.toString().getBytes();}else if(((File) obj).getName().endsWith(".mp3")){type = MarkingType.n.toString().getBytes();}else if(((File) obj).getName().endsWith(".mp4")){type = MarkingType.m.toString().getBytes();}}else{throw new RuntimeException("不支持的数据类型");}byte[] data = obj.toString().getBytes(CHARSET);int length = data.length;byte[] header = new byte[4];header[0] = (byte) (length >>> 24);header[1] = (byte) (length >>> 16);header[2] = (byte) (length >>> 8);header[3] = (byte) (length);return ByteBuffer.allocate(header.length + type.length + data.length).put(header).put(type).put(data).array();
}
解析LSB和简单的BitStream实现
为了方便读取长度,标记等信息,设计一个BitStream包装类,实现类似于一个一个bit读取BufferedImage的效果。
public class BitStream{private final BufferedImage image;private int x = 0,y = 0;private int channelIndex = 0;public BitStream(BufferedImage image){this.image = image;}public int readBit(){if(y >= image.getHeight()) return 0;RGB rgb = new RGB(image.getRGB(x, y));int bit = rgb.getLowBit(channelIndex);//next bitchannelIndex++;if(channelIndex >= 3){channelIndex = 0;x++;if(x >= image.getWidth()){x = 0;y++;}}return bit;}public byte readByte(){byte b = 0;for(int i = 0; i < 8; i++){b |= (byte)(readBit() << (7 - i));}return b;}public int readInt(){return (readByte() & 0xFF) << 24 |(readByte() & 0xFF) << 16 |(readByte() & 0xFF) << 8 |(readByte() & 0xFF);}
}
之后的提取方法就比较简单了。
public static <T> T extractLSBImage(File imageFile, Class<T> expectedType) throws IOException {BufferedImage image = ImageIO.read(imageFile);BitStream bitStream = new BitStream(image);//读取长度int length = bitStream.readInt();//读取类型MarkingType type = fromByte(bitStream.readByte());//读取实际数据byte[] secretData = new byte[length];for (int i = 0; i < length; i++) {secretData[i] = bitStream.readByte();}switch (type){case s:if(expectedType == String.class)return expectedType.cast(new String(secretData, CHARSET));else throw new RuntimeException("类型不匹配");// 其他类型处理...}return null;
}
文件选择对话框与基于<a>
超链接标签文件下载
文件选择对话框
原始html提供了<input type="file">
标签。点击它将调用操作系统提供的文件选择对话框,这是由浏览器提供的默认行为。可以添加以下属性来控制行为:
属性/配置 | 作用 | 示例 |
---|---|---|
accept | 限制可选文件类型 | accept="image/png" 只显示PNG文件 |
multiple | 是否允许多选 | <input type="file" multiple> |
ElementPlus提供了它的封装组件el-upload
。
<el-upload:auto-upload="false":on-change="handleGenerateImageUpload"accept="image/png"
><el-button>选择PNG图片</el-button>
</el-upload>
基于<a>
超链接标签文件下载
浏览器的<a>
标签是HTML中用于创建超链接的标签,通过href
属性,可以指定目标URL(可以是网页地址、文件路径、锚点等)。这里基于它实现了文件的下载功能。创建download
下载标签,该标签不会触发网页跳转,而是直接下载资源。
const a = document.createElement('a'); //创建一个隐藏的<a>标签
a.href = generatedWatermarkImage.value; // 设置下载链接(这里应该是URL属性)
a.download = 'watermarked.png'; // 设置下载文件名
document.body.appendChild(a); // 临时添加到DOM,只有在DOM中才能点击
a.click(); // 模拟点击 -即触发用户的下载行为
document.body.removeChild(a); // 移除元素
细节:@Controller
和@RestController
的区别
-
@Controller(传统 Spring MVC 控制器)
- 默认返回的是视图名称(即跳转到某个页面)。
- 如果要返回 JSON/XML 数据,必须加
@ResponseBody
。
-
@RestController(RESTful API 专用控制器)
- 默认所有方法都带
@ResponseBody
:直接返回 JSON/XML 数据,而不是视图。
- 默认所有方法都带
结语
因为再塞内容的话就太臃肿了,所以专门把内容分割出去下一篇blog,时间上连续的。