嵌入式JPEG图像加水印实战技巧
使用 libjpeg 和 FreeType 在 JPEG 图像中添加文字水印
- 一、背景知识
- libjpeg
- FreeType
- 二、项目功能概述
- 三、关键宏定义
- 四、结构体对齐小实验
- 五、主要流程详解
- 1. JPEG 解码
- 2. 初始化 FreeType 并加载字体
- 3. 绘制半透明矩形背景
- 4. 渲染文字并绘制
- 5. JPEG 编码并输出
- 六、实际运行效果
- 七、总结
- 八、后续优化建议
- 九、源码Demo
在嵌入式开发和图像处理领域中,为 JPEG 图片添加自定义文字水印是一个常见需求。本文将介绍如何使用 libjpeg
和 FreeType
两个经典的 C 语言图形处理库,在 JPEG 图片中右下角绘制一个带半透明黑色背景和白色文字的水印。
一、背景知识
libjpeg
libjpeg
是处理 JPEG 图像的标准库,可实现 JPEG 图片的解码和编码功能。
FreeType
FreeType
是一个开源的字体引擎,可用于将矢量字体渲染为位图,非常适合文字水印的绘制。
二、项目功能概述
本程序的功能包括:
- 从 JPEG 文件读取图像到内存;
- 使用 FreeType 加载字体并渲染文字;
- 在图像右下角绘制一个带有半透明背景的矩形;
- 在矩形中叠加白色文字水印;
- 将处理后的图像再次保存为 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);
}