当前位置: 首页 > java >正文

嵌入式JPEG图像加水印实战技巧

使用 libjpeg 和 FreeType 在 JPEG 图像中添加文字水印

    • 一、背景知识
      • libjpeg
      • FreeType
    • 二、项目功能概述
    • 三、关键宏定义
    • 四、结构体对齐小实验
    • 五、主要流程详解
      • 1. JPEG 解码
      • 2. 初始化 FreeType 并加载字体
      • 3. 绘制半透明矩形背景
      • 4. 渲染文字并绘制
      • 5. JPEG 编码并输出
    • 六、实际运行效果
    • 七、总结
    • 八、后续优化建议
    • 九、源码Demo

在嵌入式开发和图像处理领域中,为 JPEG 图片添加自定义文字水印是一个常见需求。本文将介绍如何使用 libjpegFreeType 两个经典的 C 语言图形处理库,在 JPEG 图片中右下角绘制一个带半透明黑色背景和白色文字的水印。

一、背景知识

libjpeg

libjpeg 是处理 JPEG 图像的标准库,可实现 JPEG 图片的解码和编码功能。

FreeType

FreeType 是一个开源的字体引擎,可用于将矢量字体渲染为位图,非常适合文字水印的绘制。

二、项目功能概述

本程序的功能包括:

  1. 从 JPEG 文件读取图像到内存;
  2. 使用 FreeType 加载字体并渲染文字;
  3. 在图像右下角绘制一个带有半透明背景的矩形;
  4. 在矩形中叠加白色文字水印;
  5. 将处理后的图像再次保存为 JPEG 文件。

三、关键宏定义

#define RECT_WIDTH 600      // 水印背景矩形的宽度
#define RECT_HEIGHT 100     // 水印背景矩形的高度
#define RECT_COLOR_R 0      // 背景颜色 R 分量
#define RECT_COLOR_G 0
#define RECT_COLOR_B 0
#define RECT_ALPHA 0.1      // 背景透明度#define TEXT_COLOR_R 255    // 文字颜色 R 分量
#define TEXT_COLOR_G 255
#define TEXT_COLOR_B 255
#define TEXT_ALPHA 1.0      // 文字不透明

四、结构体对齐小实验

程序中包含了一个结构体大小输出的小实验,用于说明结构体在内存中的对齐规则:

struct Example {char a;     // 1 字节int b;      // 4 字节double c;   // 8 字节char d[3];  // 3 字节
};

输出结果显示 sizeof(struct Example) 会因为填充而大于所有字段的总和。

五、主要流程详解

1. JPEG 解码

使用 libjpeg 将 JPEG 文件读入内存:

jpeg_mem_src(&cinfo, src_img_buf, buf_size);
jpeg_read_header(&cinfo, TRUE);
jpeg_start_decompress(&cinfo);

2. 初始化 FreeType 并加载字体

FT_Init_FreeType(&ft);
FT_New_Face(ft, "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 0, &face);
FT_Set_Pixel_Sizes(face, 0, 80);

3. 绘制半透明矩形背景

通过 alpha 混合实现:

image_data[img_pos] = RECT_COLOR_R * RECT_ALPHA + orig_r * (1 - RECT_ALPHA);

4. 渲染文字并绘制

使用 FT_Load_Char() 获取字符位图,再通过位图 alpha 值与原图像素进行混合。

5. JPEG 编码并输出

通过 jpeg_mem_dest() 将图像重新编码为 JPEG 格式,并输出为文件:

jpeg_start_compress(&cjpeg, TRUE);
jpeg_write_scanlines(...);

六、实际运行效果

运行命令如下:

./watermark input.jpg output.jpg

输出文件将包含右下角的“High: 126cm”文字水印。

七、总结

本示例展示了在 JPEG 图像中添加自定义水印的完整流程,涵盖了内存处理、字体渲染、图像混合等多个关键技术点,适用于嵌入式图像处理、工业视觉等场景。

八、后续优化建议

  • 支持多行文字排版;
  • 动态调整水印区域大小以适应文字长度;
  • 支持 PNG 等格式;
  • 使用硬件加速库提高性能。

九、源码Demo

add_watermark.c

#include <stdio.h>
#include <stdlib.h>
#include <jpeglib.h>
#include <string.h>
#include <ft2build.h>
#include FT_FREETYPE_H// 水印矩形设置
#define RECT_WIDTH 600      // 矩形宽度
#define RECT_HEIGHT 100      // 矩形高度
#define RECT_COLOR_R 0      // 矩形颜色的 R 值(黑色)
#define RECT_COLOR_G 0      // 矩形颜色的 G 值
#define RECT_COLOR_B 0      // 矩形颜色的 B 值
#define RECT_ALPHA 0.1      // 矩形透明度(0.0 完全透明 - 1.0 完全不透明)// 文字颜色设置
#define TEXT_COLOR_R 255    // 文字颜色的 R 值(白色)
#define TEXT_COLOR_G 255    // 文字颜色的 G 值
#define TEXT_COLOR_B 255    // 文字颜色的 B 值
#define TEXT_ALPHA 1.0      // 文字透明度// 函数声明
void add_watermark_with_text(const unsigned char *src_img_buf, size_t buf_size, unsigned char **dst_img_buf, size_t *dst_buf_size, const char *osd_text);struct Example {char a;     // 占用 1 个字节int b;      // 占用 4 个字节double c;   // 占用 8 个字节char d[3];  // 占用 3 个字节
};int main(int argc, char *argv[]) {if (argc != 3) {fprintf(stderr, "Usage: %s <input.jpg> <output.jpg>\n", argv[0]);return 1;}printf("结构体 Example 的大小: %zu 字节\n", sizeof(struct Example));// 读取图像文件到缓冲区FILE *input_file = fopen(argv[1], "rb");if (!input_file) {fprintf(stderr, "无法打开输入文件 %s\n", argv[1]);return 1;}// 获取文件大小fseek(input_file, 0, SEEK_END);size_t file_size = ftell(input_file);fseek(input_file, 0, SEEK_SET);// 分配缓冲区并读取文件数据unsigned char *src_img_buf = (unsigned char *)malloc(file_size);if (!src_img_buf) {fprintf(stderr, "内存分配失败\n");fclose(input_file);return 1;}fread(src_img_buf, 1, file_size, input_file);fclose(input_file);// 分配目标图像缓冲区unsigned char *dst_img_buf = NULL;size_t dst_buf_size = 0;// 水印文本const char *osd_text = "High: 126cm";// 调用水印函数处理内存中的图像数据add_watermark_with_text(src_img_buf, file_size, &dst_img_buf, &dst_buf_size, osd_text);// 保存结果到输出文件FILE *output_file = fopen(argv[2], "wb");if (!output_file) {fprintf(stderr, "无法创建输出文件 %s\n", argv[2]);free(src_img_buf);free(dst_img_buf);return 1;}fwrite(dst_img_buf, 1, dst_buf_size, output_file);fclose(output_file);free(src_img_buf);free(dst_img_buf);printf("水印已添加到图片右下角。\n");return 0;
}void add_watermark_with_text(const unsigned char *src_img_buf, size_t buf_size, unsigned char **dst_img_buf, size_t *dst_buf_size, const char *osd_text) {// 初始化JPEG解码struct jpeg_decompress_struct cinfo;struct jpeg_error_mgr jerr;cinfo.err = jpeg_std_error(&jerr);jpeg_create_decompress(&cinfo);// 使用内存缓冲区解码jpeg_mem_src(&cinfo, src_img_buf, buf_size);jpeg_read_header(&cinfo, TRUE);jpeg_start_decompress(&cinfo);// 分配内存存储图像数据int row_stride = cinfo.output_width * cinfo.output_components;unsigned long img_size = cinfo.output_width * cinfo.output_height * cinfo.output_components;unsigned char *image_data = (unsigned char *)malloc(img_size);if (!image_data) {fprintf(stderr, "内存分配失败\n");jpeg_destroy_decompress(&cinfo);return;}// 读取图像数据while (cinfo.output_scanline < cinfo.output_height) {unsigned char *rowptr = image_data + (cinfo.output_scanline) * row_stride;jpeg_read_scanlines(&cinfo, &rowptr, 1);}jpeg_finish_decompress(&cinfo);jpeg_destroy_decompress(&cinfo);// 初始化FreeTypeFT_Library ft;if (FT_Init_FreeType(&ft)) {fprintf(stderr, "无法初始化FreeType库\n");free(image_data);return;}FT_Face face;if (FT_New_Face(ft, "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 0, &face)) {fprintf(stderr, "无法加载字体\n");FT_Done_FreeType(ft);free(image_data);return;}FT_Set_Pixel_Sizes(face, 0, 80);// 计算水印矩形的位置(右下角,偏移10像素)int rect_x_start = cinfo.output_width - RECT_WIDTH - 10;int rect_y_start = cinfo.output_height - RECT_HEIGHT - 10;// 绘制半透明矩形for (int y = rect_y_start; y < rect_y_start + RECT_HEIGHT; y++) {for (int x = rect_x_start; x < rect_x_start + RECT_WIDTH; x++) {if (x < 0 || x >= cinfo.output_width || y < 0 || y >= cinfo.output_height)continue;int img_pos = (y * cinfo.output_width + x) * cinfo.output_components;// 原始像素颜色unsigned char orig_r = image_data[img_pos];unsigned char orig_g = image_data[img_pos + 1];unsigned char orig_b = image_data[img_pos + 2];// 混合颜色image_data[img_pos]     = (unsigned char)(RECT_COLOR_R * RECT_ALPHA + orig_r * (1 - RECT_ALPHA));image_data[img_pos + 1] = (unsigned char)(RECT_COLOR_G * RECT_ALPHA + orig_g * (1 - RECT_ALPHA));image_data[img_pos + 2] = (unsigned char)(RECT_COLOR_B * RECT_ALPHA + orig_b * (1 - RECT_ALPHA));}}// 渲染文字并绘制到图像int pen_x = rect_x_start + 10; // 左边距10像素int pen_y = rect_y_start + RECT_HEIGHT - 10; // 从底部偏移10像素for (const char *p = osd_text; *p; p++) {// 加载字符if (FT_Load_Char(face, *p, FT_LOAD_RENDER)) {fprintf(stderr, "无法加载字符 '%c'\n", *p);continue;}FT_GlyphSlot g = face->glyph;for (int row = 0; row < g->bitmap.rows; row++) {for (int col = 0; col < g->bitmap.width; col++) {int x = pen_x + g->bitmap_left + col;int y = pen_y - g->bitmap_top + row;if (x < 0 || x >= cinfo.output_width || y < 0 || y >= cinfo.output_height)continue;unsigned char mask = g->bitmap.buffer[row * g->bitmap.width + col];float alpha = (mask / 255.0) * TEXT_ALPHA;int img_pos = (y * cinfo.output_width + x) * cinfo.output_components;// 原始像素颜色unsigned char orig_r = image_data[img_pos];unsigned char orig_g = image_data[img_pos + 1];unsigned char orig_b = image_data[img_pos + 2];// 混合文字颜色image_data[img_pos]     = (unsigned char)(TEXT_COLOR_R * alpha + orig_r * (1 - alpha));image_data[img_pos + 1] = (unsigned char)(TEXT_COLOR_G * alpha + orig_g * (1 - alpha));image_data[img_pos + 2] = (unsigned char)(TEXT_COLOR_B * alpha + orig_b * (1 - alpha));}}pen_x += g->advance.x >> 6; // 更新X坐标}// 压缩并保存结果struct jpeg_compress_struct cjpeg;struct jpeg_error_mgr jerr2;cjpeg.err = jpeg_std_error(&jerr2);jpeg_create_compress(&cjpeg);jpeg_mem_dest(&cjpeg, dst_img_buf, dst_buf_size);cjpeg.image_width = cinfo.output_width;cjpeg.image_height = cinfo.output_height;cjpeg.input_components = 3;cjpeg.in_color_space = JCS_RGB;jpeg_set_defaults(&cjpeg);jpeg_start_compress(&cjpeg, TRUE);while (cjpeg.next_scanline < cjpeg.image_height) {unsigned char *rowptr = image_data + (cjpeg.next_scanline) * row_stride;jpeg_write_scanlines(&cjpeg, &rowptr, 1);}jpeg_finish_compress(&cjpeg);jpeg_destroy_compress(&cjpeg);FT_Done_Face(face);FT_Done_FreeType(ft);free(image_data);
}
http://www.xdnf.cn/news/9598.html

相关文章:

  • 自我觉察是成长的第一步,如何构建内心的平静
  • 仿真每日一练 | ABAQUS水滴入水分析
  • SWMM+HTWATER最新水文水动力模型应用
  • linux版本vmware修改ubuntu虚拟机为桥接模式
  • STM32:ESP8266 + MQTT 云端与报文全解析
  • 微信小程序关于截图、录屏拦截
  • 通义实验室开源针对RAG的预训练框架
  • P1923 【深基9.例4】求第 k 小的数
  • Sentinel限流熔断机制实战
  • 软件测试计划中时间与资源的估算
  • 探索Dify-LLM:构建自定义大模型应用的高效平台
  • IO进程(进程 Process)
  • COF材料前沿应用:多孔晶态材料的催化革新之旅 | 乐研试剂
  • 华南会议|AI驱动仿真未来 2025 Altair区域技术交流会华南站,报名开启!
  • 【人工智能】DeepSeek的AI狂想曲:从训练到应用的交响乐
  • 2025.05.28【Parallel】Parallel绘图:拟时序分析专用图
  • 创建型模式之 Builder (生成器)
  • 从跟跑到领跑:雷克赛恩17年创业历程
  • 正则表达式的修饰符
  • 如何更新和清理 Go 依赖版本
  • 暗通道先验去雾算法实现
  • Trae配置JAVA本地环境,开发前后端
  • ToolsSet之:大数及高精度运算
  • Web 端顶级视效实现:山海鲸端渲染底层原理与发布模式详解
  • 234. Palindrome Linked List
  • Linux系统编程-DAY07
  • JAVA中常用算法详解:排序(冒泡、快速排序)与查找(二分查找)
  • 途景VR智拍APP:开启沉浸式VR拍摄体验
  • 快速入门Java+Spring Ai+deepseek 开发
  • git 一台电脑一个git账户,对应多个仓库ssh