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,時間上連續的。