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

375 阅读5分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

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地址:github.com/mayanklahir…

exiv2 库的GitHub地址:github.com/Exiv2/exiv2

image-20220216113350464

image-20220216113415357

image.png

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许可证下发布的,几乎可以在任何地方使用。你的项目只需要加入两个文件就可以使用,不依赖于任何构建系统或外部库。

image-20220216112831991

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"));
 }