设计一款照片一键加水印的小工具
- 2022 年 3 月 27 日
本文字数:6077 字
阅读完需:约 20 分钟
1. 前言
现在手机相机拍摄的照片都是 JPG/JPEG 格式,JPEG 格式的照片可以附加 EXIF 信息,这个 EXIF 信息是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据,也就相当于图片的身份信息。它可以记录,拍摄的时间、拍摄的地点、相机型号、曝光参数等很多信息。
这篇文章介绍使用 QT 设计一个小工具,读取 JPG 图片的 EXIF 信息,得到照片的拍摄时间,再绘制到照片上,另存为新图片,代码里使用多线程处理,可以一次性选择多张照片,一键添加时间水印后另存到指定目录下。给照片添加时间水印后有很多方便的地方。比如:以后去打印店打印照片就能将时间打印出来,可以通过时间了解到这个照片的拍摄场景时间线,帮助回忆这个时间线发生的一些美好往事。
QT 本身图片处理接口不支持读取 EXIF 信息,需要采用第三方库来完成,目前 GitHub 上有很多开源的库可以实现 JPG 图片的 EXIF 信息读取,比如:easyexif ,exiv2 等等。easyexif 使用比较简单,如果只是想要读取信息,使用 easyexif 库非常方便,easyexif 是一个很精简的代码,整个项目只包含了 2 个文件: exif.h和exif.c
。
easyexif 库的 GitHub 地址:https://github.com/mayanklahiri/easyexif
exiv2 库的 GitHub 地址:https://github.com/Exiv2/exiv2
2. easyexif 使用介绍
2.1 easyexif 简介
来至官网的介绍:
这是一个小型的符合 ISO 规范的 C++ ExIF 解析库。
EasyExIF 是一个小型、轻量级的 C++库,它可以从 JPEG 文件中解析基本信息。它只使用了 std::string 库,纯 C++编写。使用时,将 JPEG 文件的二进制内容传递给它,它会解析出几个最重要的 EXIF 字段。
为什么要用 EasyExIF 这个库?它包括一个.h 和一个.c 文件。
有时,我们只需要快速从 JPEG 文件的 EXIF 头中提取基本信息:拍摄图像的时间(不是文件时间戳、相机的内部时间)、F-stop 或曝光时间、嵌入 EXIF 文件的 GPS 信息、相机的品牌和型号等。问题是,现在市面上很多的 EXIF 库都不是很轻量级,也不容易集成到更大的程序中。EasyEXIF 旨在解决这个问题,它是在一个非常自由的 BSD 许可证下发布的,几乎可以在任何地方使用。你的项目只需要加入两个文件就可以使用,不依赖于任何构建系统或外部库。
2.2 来至官网的示例代码
#include "exif.h"
EXIFInfo result;
result.parseFrom(JPEGFileBuffer, BufferSize);
printf("Camera make : %s\n", result.Make.c_str());
printf("Camera model : %s\n", result.Model.c_str());
printf("Software : %s\n", result.Software.c_str());
printf("Bits per sample : %d\n", result.BitsPerSample);
printf("Image width : %d\n", result.ImageWidth);
printf("Image height : %d\n", result.ImageHeight);
printf("Image description : %s\n", result.ImageDescription.c_str());
printf("Image orientation : %d\n", result.Orientation);
printf("Image copyright : %s\n", result.Copyright.c_str());
printf("Image date/time : %s\n", result.DateTime.c_str());
printf("Original date/time: %s\n", result.DateTimeOriginal.c_str());
printf("Digitize date/time: %s\n", result.DateTimeDigitized.c_str());
printf("Subsecond time : %s\n", result.SubSecTimeOriginal.c_str());
printf("Exposure time : 1/%d s\n", (unsigned) (1.0/result.ExposureTime));
printf("F-stop : f/%.1f\n", result.FNumber);
printf("ISO speed : %d\n", result.ISOSpeedRatings);
printf("Subject distance : %f m\n", result.SubjectDistance);
printf("Exposure bias : %f EV\n", result.ExposureBiasValue);
printf("Flash used? : %d\n", result.Flash);
printf("Metering mode : %d\n", result.MeteringMode);
printf("Lens focal length : %f mm\n", result.FocalLength);
printf("35mm focal length : %u mm\n", result.FocalLengthIn35mm);
printf("GPS Latitude : %f deg\n", result.GeoLocation.Latitude);
printf("GPS Longitude : %f deg\n", result.GeoLocation.Longitude);
printf("GPS Altitude : %f m\n", result.GeoLocation.Altitude);
3. 小工具实现源码
图片处理采用多线程方式处理,不卡主 UI 界面,处理结果会通过信号槽方式传递给 UI 界面进行显示。
下面贴出实现的核心代码:
#include "widget.h"
#include "ui_widget.h"
class IMAGE_CONFIG image_config;
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("照片自动加水印");
//关联图片转换线程
connect(&scale_out_image,SIGNAL(LogSend(QString)),this,SLOT(Image_Log_Display(QString)));
//获取系统图片目录的路径
QStringList dir_list=QStandardPaths::standardLocations(QStandardPaths::PicturesLocation);
if(dir_list.size()>0)
{
ui->lineEdit->setText(dir_list.at(0)+"/HandlePicture");
}
}
Widget::~Widget()
{
delete ui;
}
//选择照片
void Widget::on_pushButton_select_clicked()
{
QStringList filenamelist=QFileDialog::getOpenFileNames(this,"选择照片",ui->lineEdit->text(),tr("*.jpg *.jpeg"));
image_config.filenamelist=filenamelist;
for(int i=0;i<filenamelist.count();i++)
{
Image_Log_Display(QString("已选:%1\n").arg(filenamelist.at(i)));
// qDebug()<<filenamelist.at(i); //循环取出列表中的文件名称
}
}
//关闭线程
void ScaleOutImage::close()
{
image_config.run_flag=0;
this->quit();
this->wait();
}
//处理图片
int ScaleOutImage::HandleImage(QString file)
{
// Read the JPEG file into a buffer
FILE *fp = fopen(file.toUtf8().data(), "rb");
if (!fp)
{
qDebug("Can't open file.\n");
return -1;
}
fseek(fp, 0, SEEK_END);
unsigned long fsize = ftell(fp);
rewind(fp);
unsigned char *buf = new unsigned char[fsize];
if (fread(buf, 1, fsize, fp) != fsize) {
qDebug("Can't read file.\n");
delete[] buf;
return -2;
}
fclose(fp);
// Parse EXIF
easyexif::EXIFInfo result;
int code = result.parseFrom(buf, fsize);
delete[] buf;
if (code)
{
qDebug("Error parsing EXIF: code %d\n", code);
return -3;
}
// Dump EXIF information
// qDebug("Camera make : %s\n", result.Make.c_str());
// qDebug("Camera model : %s\n", result.Model.c_str());
// qDebug("Software : %s\n", result.Software.c_str());
// qDebug("Bits per sample : %d\n", result.BitsPerSample);
// qDebug("Image width : %d\n", result.ImageWidth);
// qDebug("Image height : %d\n", result.ImageHeight);
// qDebug("Image description : %s\n", result.ImageDescription.c_str());
// qDebug("Image orientation : %d\n", result.Orientation);
// qDebug("Image copyright : %s\n", result.Copyright.c_str());
// qDebug("Image date/time : %s\n", result.DateTime.c_str());
// qDebug("Original date/time : %s\n", result.DateTimeOriginal.c_str());
// qDebug("Digitize date/time : %s\n", result.DateTimeDigitized.c_str());
// qDebug("Subsecond time : %s\n", result.SubSecTimeOriginal.c_str());
// qDebug("Exposure time : 1/%d s\n",
// (unsigned)(1.0 / result.ExposureTime));
// qDebug("F-stop : f/%.1f\n", result.FNumber);
// qDebug("Exposure program : %d\n", result.ExposureProgram);
// qDebug("ISO speed : %d\n", result.ISOSpeedRatings);
// qDebug("Subject distance : %f m\n", result.SubjectDistance);
// qDebug("Exposure bias : %f EV\n", result.ExposureBiasValue);
// qDebug("Flash used? : %d\n", result.Flash);
// qDebug("Flash returned light : %d\n", result.FlashReturnedLight);
// qDebug("Flash mode : %d\n", result.FlashMode);
// qDebug("Metering mode : %d\n", result.MeteringMode);
// qDebug("Lens focal length : %f mm\n", result.FocalLength);
// qDebug("35mm focal length : %u mm\n", result.FocalLengthIn35mm);
// qDebug("GPS Latitude : %f deg (%f deg, %f min, %f sec %c)\n",
// result.GeoLocation.Latitude, result.GeoLocation.LatComponents.degrees,
// result.GeoLocation.LatComponents.minutes,
// result.GeoLocation.LatComponents.seconds,
// result.GeoLocation.LatComponents.direction);
// qDebug("GPS Longitude : %f deg (%f deg, %f min, %f sec %c)\n",
// result.GeoLocation.Longitude, result.GeoLocation.LonComponents.degrees,
// result.GeoLocation.LonComponents.minutes,
// result.GeoLocation.LonComponents.seconds,
// result.GeoLocation.LonComponents.direction);
// qDebug("GPS Altitude : %f m\n", result.GeoLocation.Altitude);
// qDebug("GPS Precision (DOP) : %f\n", result.GeoLocation.DOP);
// qDebug("Lens min focal length: %f mm\n", result.LensInfo.FocalLengthMin);
// qDebug("Lens max focal length: %f mm\n", result.LensInfo.FocalLengthMax);
// qDebug("Lens f-stop min : f/%.1f\n", result.LensInfo.FStopMin);
// qDebug("Lens f-stop max : f/%.1f\n", result.LensInfo.FStopMax);
// qDebug("Lens make : %s\n", result.LensInfo.Make.c_str());
// qDebug("Lens model : %s\n", result.LensInfo.Model.c_str());
// qDebug("Focal plane XRes : %f\n", result.LensInfo.FocalPlaneXResolution);
// qDebug("Focal plane YRes : %f\n", result.LensInfo.FocalPlaneYResolution);
QImage image(file);//加载图片
QPainter painter(&image);//构建 QImage 绘图对象
painter.setPen(Qt::white);
int font_size=image_config.font_size;
painter.setFont(QFont("宋体", font_size));
QString text="";
QRect rect;
rect.setX(0);
rect.setY(image.height()-font_size*2);
rect.setWidth(image.width());
rect.setHeight(font_size*2);
qDebug()<<"照片的尺寸:"<<image.rect();
qDebug()<<"水印尺寸位置:"<<rect;
if(image_config.camera_type)
{
text+=result.Make.c_str();
text+=" ";
}
if(image_config.camera_time)
{
QString csv=QString::fromStdString(result.DateTime);
QString date=csv.section(' ',0, 0); //日期
QString time=csv.section(' ',1, 1); //时间
date=date.replace(':',"/");
text+=date+" "+time;
}
if(text.size()>0)
{
painter.drawText(rect,Qt::AlignHCenter,text);
qDebug()<<"绘制的水印:"<<text;
}
QString out=image_config.lineEdit_out_addr+"/"+QFileInfo(file).baseName()+".jpg";
//如果文件已经存在就先删除
if(QFileInfo(out).exists())
{
QFile::remove(out);
}
if(image.save(out))return 0;
return -6;
}
//线程执行函数
void ScaleOutImage::run()
{
QString out_name;
int run=0;
for(int i=0;i<image_config.filenamelist.count();i++)
{
run=HandleImage(image_config.filenamelist.at(i));
if(run==0)
{
LogSend(QString("处理:%1成功.\n").arg(QFileInfo(image_config.filenamelist.at(i)).fileName()));
}
else
{
LogSend(QString("处理:%1失败.\n").arg(QFileInfo(image_config.filenamelist.at(i)).fileName()));
}
if(image_config.run_flag==0)
{
break;
}
}
}
void Widget::Image_Log_Display(QString text)
{
Log_Text_Display(ui->plainTextEdit_log,text);
}
/*日志显示*/
void Widget::Log_Text_Display(QPlainTextEdit *plainTextEdit_log,QString text)
{
//设置光标到文本末尾
plainTextEdit_log->moveCursor(QTextCursor::End, QTextCursor::MoveAnchor);
//当文本数量超出一定范围就清除
if(plainTextEdit_log->toPlainText().size()>4096)
{
plainTextEdit_log->clear();
}
plainTextEdit_log->insertPlainText(text);
//移动滚动条到底部
QScrollBar *scrollbar = plainTextEdit_log->verticalScrollBar();
if(scrollbar)
{
scrollbar->setSliderPosition(scrollbar->maximum());
}
}
//开始
void Widget::on_pushButton_start_clicked()
{
//创建目录
QDir dir_image(ui->lineEdit->text());
if(!dir_image.exists())
{
if(dir_image.mkdir(ui->lineEdit->text()))
{
Image_Log_Display("输出目录创建成功.\n");
}
else
{
Image_Log_Display("输出目录创建失败.\n");
return;
}
}
image_config.camera_time=ui->checkBox_camera_time->isChecked();
image_config.camera_type=ui->checkBox_camera_type->isChecked();
image_config.font_size=ui->spinBox->value();
image_config.lineEdit_out_addr=ui->lineEdit->text();
image_config.run_flag=1;
//开始转换
scale_out_image.start();
}
//停止
void Widget::on_pushButton_stop_clicked()
{
scale_out_image.close();
}
//帮助
void Widget::on_pushButton_about_clicked()
{
QMessageBox::about(this,"功能介绍",tr("获取JPG照片属性里的拍摄时间,绘制在照片上重新保存.\n"));
}
版权声明: 本文为 InfoQ 作者【DS小龙哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/2ca443a1b8df6155dfd2cfbd4】。文章转载请联系作者。
DS小龙哥
之所以觉得累,是因为说的比做的多。 2022.01.06 加入
熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域
评论