写点什么

设计一款照片一键加水印的小工具

作者:DS小龙哥
  • 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"));}
复制代码


发布于: 刚刚阅读数: 2
用户头像

DS小龙哥

关注

之所以觉得累,是因为说的比做的多。 2022.01.06 加入

熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域

评论

发布
暂无评论
设计一款照片一键加水印的小工具_3 月月更_DS小龙哥_InfoQ写作平台