项目:基于ffmpeg的Gif表情包生成器

项目背景

文字信息时代,传统的文字聊天方式已不能满足大众的需求,很多时候文字不能表达自己的想法,或者沟通技巧的欠缺,这时表情包就派上用场了

“一言不合就斗图”,可以说斗图也成为了交流中的一种乐趣
暴走表情广泛的遍布于网络,网民们大多用作斗图。常见于QQ、微信等聊天工具。斗图活动起始于QQ,群聊时大家发送搞趣图片以相互娱乐。后来发展到百度贴吧等各种论坛上
斗图发展到现在不仅仅用系统本身提供的表情图,还会恶搞明星、动漫人物。以图加文字的自由组合,结合网络语言,制作搞笑逗比的图片

可行性分析

经济可行性——低成本
操作可行性——简单
技术可行性——借助其他工具

需求分析与系统设计

制作Gif动图,因此选择两种生成方式:1.图片生成;2.视频生成
技术方面借助其他的工具实现:选择 ffmpeg 工具
用户在选择后输入文件所在目录,若是图片,直接生成动态图;若是视频,截取视频片段,提取视频裸流,直接生成Gif或提取视频字幕【视频中的字幕必须是外挂字幕,否则使用的工具无法提取】由用户编辑文字显示在视频上最后生成Gif

概要设计

根据对需求的分析设计功能图:

工具

ffmpeg
ffmpeg是一个特别强大的专门用于处理音视频的开源库。可以用它的API对音视频进行处理,也可以使用它提供的工具,如 ffmpeg, ffplay, ffprobe,来编辑音视频文件。
命令:

  • 图片生成动态图:
ffmpeg -r 3 -i .\Picture %d.jpg output.gif -y
#-r后数字:一次显示的帧数
#-i:输入
#.\Picture %d.jpg:图片路径
#output.gif:生成Gif名字
#-y:若目录下有与生成Gif名相同文件就覆盖
  • 视频生成Gif
#从原视频截取片段
ffmpeg -i input,mkv -vcodec.copy -acodec copy -ss 00:01:01 -to 00:01:11 cut.mkv -y
#-vcodec.copy -acodec copy:将音视频拷贝进来
#-ss xx:xx:xx -to xx:xx:xx:剪切的视频
#cut:剪切下得片段


#提取STR
ffmpeg -i cut.mkv input.srt -y
#input.srt中:识别出的字幕
#编辑字幕

#仅提取视频不要字幕、音频
ffmpeg -i cut.mkv -vcodec copy -an -sn cut2.mvk -y

#修改字幕后放入视频
ffmpeg -i cut2.mkv -vf subtitles = input.srt cut3.mkv -y

#生成Gif
ffmpeg -i cut3.mkv -vf scale = iw/2:ih/2 -f gif

需要用cmd控制台执行这些命令,后续需要函数实现向控制台发送命令。
Duilib
Duilib是一款强大轻量级的界面开发工具,可以将用户界面和处理逻辑彻底分离,极大地提高用户界面的开发效率。提供所见即所得的开发工具UIDesigner。duilib主打的界面制作方式是XML+UI引擎+win32框架,通过XML的方式来重写窗口,然后Duilib对XML进行解析,将窗口创建成功。
这里对Win32进行简单的理解:必备的Win32程序设计原理
在下载后先将duilib运行一遍。注意运行时将其设置为启动项,并且进行批生成
VS环境配置
环境配置好后可以开始使用。
使用时添加头文件将DuiLib库包含进来:

#include "UIlib.h"
using namespace DuiLib;
#include <fstream>
#pragma comment(lib, "DuiLib_ud.lib")

各目录

  • Control:Duilib各个控件对应的UI类,比如:按钮(CButtonUI)、编辑框(CEditUI)、下拉框(CComboBoxUI)等
  • Layout:Duilib的各种布局器所对应的UI类,比如:水平布局(CHorizontalLayoutUI)、垂直布局(CVerticalLayoutUI)等
  • Core:Duilib库的核心操作:窗口相关(Win32流程封装–CWindowWnd)、窗口解析(CMarkup)等。
  • Utils:Duilib封装的一些类型:CDUIString、CPoint、压缩等

编辑项目的界面:需要与项目.exe相同目录下创建一xml文件,用生成的DuiDesigner_d应用程序对.xml文件进行编辑
界面编辑器简单介绍了如何进行编辑

实现
界面

最终生成的界面:

编码实现

主函数

WinMain是Windows程序的入口点。当Windows的“外壳”(Shell,如资源管理器)侦察到使用者要执行一个Windows程序,用加载器把该程序加载,然后调用C startup code,再由C startup code调用WinMain,开始执行程序。WinMain函数有四个固定的参数,而且这四个参数都是由操作系统负责传递进来

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int
	nCmdShow)
{
	//对应xml文件,因为要找到路径
	CPaintManagerUI::SetInstance(hInstance);
	// 设置资源的默认路径(和exe在同一目录)
	CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath());
	CDuiFramWnd framWnd;
	
	// UI_WNDSTYLE_FRAME: duilib封装的宏,代表窗口可视,具有标题栏,最大化最小化,关闭功能等
	framWnd.Create(NULL, _T("MakeGif"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);

	framWnd.CenterWindow();
	//显示窗口,激活消息循环
	framWnd.ShowModal();
	return 0;
}

窗口类

新建一个继承自WindowImplBase的类CDuilibDemoWnd

让用户实现的窗口类继承自Duilib封装的:WindowImplBase类即可,该类是一个duilib的基础框架类,封装了常用操作,使用方便。 它是以XML作为界面描述的,所以用它的时候,我们必须将界面描述写到XML里
class CDuiFramWnd : public WindowImplBase

函数

xml文件窗口

    //需要返回xml所在文件夹
	virtual CDuiString GetSkinFolder()
	{
		return _T("");//这里不用给xml文件的路径,在主函数中已经给了
	}
	//需要返回xml文件名
	virtual CDuiString GetSkinFile()
	{
		return _T("gifMake.xml");//一定要与目录放的xml文件文件名称相同

	}
	//需要返回窗口的类名
	virtual LPCTSTR GetWindowClassName(void)const
	{
		return _T("GIFMakeWnd");
	}

消息处理函数

virtual void Notify(TNotifyUI& msg)
	{
		CDuiString strName = msg.pSender->GetName();//获取控件的名字
		if (msg.sType == _T("click"))//鼠标点击
		{
			if (strName == _T("btn_close"))//关闭按钮
			{
				Close();
			}
			else if (strName == _T("btn_min"))//最小化按钮
			{
				SendMessage(WM_SYSCOMMAND, SC_MINIMIZE, 0);
			}
			else if (strName == _T("btn_load"))
			{
				LoadFile();
			}
			else if (strName == _T("btn_cut"))//截取视频
			{
				Cutview();
			}
			else if (strName == _T("btn_get_srt"))//提取字幕
			{
				GetSRTFile();
				LoadSRT();
			}
			else if (strName == _T("btn_commit"))//提交
			{
				CEditUI* pEdit = (CEditUI*)m_PaintManager.FindControl(_T("edit_word"));
				CDuiString strWord = pEdit->GetText();//获取文本

				//将该文本写回到list中
				CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));
				CListTextElementUI* pListItem = (CListTextElementUI*)pList->GetItemAt(pList->GetCurSel());

				pListItem->SetText(1, strWord);
			}
			else if (strName == _T("btn_write_srt"))//写入字幕
			{
				WriteSRT();
			}
			else if (strName == _T("btn_view"))//提取视频
			{
				GenerateView();
			}
			else if (strName == _T("btn_bron"))//烧录
			{
				BornSRTtoView();
			}
			else if (strName == _T("btn_generate"))//生成Gif
			{
				CComboBoxUI* pCombo = (CComboBoxUI*)m_PaintManager.FindControl(_T("combo_select"));
				if (0 == pCombo-> GetCurSel())
				{
					GenerateGifWithPic();
				}
				else
				{
					GenerateGifWithView();
				}
			}
		}
		//默认是图片生成
		else if (msg.sType == _T("windowinit"))
		{
			SetControlEnable(false);
		}
		else if (msg.sType == _T("itemselect"))
		{
			if (strName == _T("List_srt"))
			{
				CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));
				//拿出表中某项的内容
				CListTextElementUI* pListItem = (CListTextElementUI*)pList->GetItemAt(pList->GetCurSel());
				//将选中行中的对应文本信息写入edit中
				CEditUI* pEdit = (CEditUI*)m_PaintManager.FindControl(_T("edit_word"));
				pEdit->SetText(pListItem->GetText(1));
			}
			if (strName == _T("combo_select"))
			{
				CComboBoxUI* pComboUI = (CComboBoxUI*)m_PaintManager.FindControl(_T("combo_select"));
				if (0 == pComboUI->GetCurSel())
				{
					SetControlEnable(false);
				}
				else
				{
					SetControlEnable(true);
				}
			}
		}
	}

设置控件是否有效,如果是由图片生成Gif,那么许多按钮将不再需要

void SetControlEnable(bool IsValid)//将一些控件设置为有效或者无效
	{
		((CEditUI*)m_PaintManager.FindControl(_T("edit_start")))->SetEnabled(IsValid);//start
		((CEditUI*)m_PaintManager.FindControl(_T("edit_end")))->SetEnabled(IsValid);//end
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_cut")))->SetEnabled(IsValid);//截取按钮
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_get_srt")))->SetEnabled(IsValid);//提取SRT按钮
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_write_srt")))->SetEnabled(IsValid);//写入SRT按钮
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_view")))->SetEnabled(IsValid);//提取视频按钮
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_bron")))->SetEnabled(IsValid);//烧录按钮
	}

向控制台发送命令:

void SendCmd(const CDuiString& strCMD)
	{
		SHELLEXECUTEINFO strSEInfo;
		memset(&strSEInfo, 0, sizeof(SHELLEXECUTEINFO));
		strSEInfo.cbSize = sizeof(SHELLEXECUTEINFO);
		strSEInfo.fMask = SEE_MASK_NOCLOSEPROCESS;

		strSEInfo.lpFile = _T("C:\\WINDOWS\\system32\\cmd.exe");
		//windows命令行cmd所在的路径

		strSEInfo.lpParameters = strCMD;//打开程序的参数
		strSEInfo.nShow = SW_SHOW;//隐藏

		ShellExecuteEx(&strSEInfo);//在函数中,会新创建一个进程,来负责调用命令行窗口执行命令

		//等待命令响应
		WaitForSingleObject(strSEInfo.hProcess, INFINITE);
		MessageBox(m_hWnd, _T("命令操作完成"), _T("MakeGif"), IDOK);
	}

用图片生成Gif

void GenerateGifWithPic()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取工程的目录【与exe文件在同一个目录】
		strPath += _T("ffmpeg\\");
		//构造命令
		CDuiString strCMD;
		strCMD += _T("/c ");//构造命令期间第一条必须是'/c',要加上/c参数
		strCMD += strPath;
		strCMD += _T("ffmpeg -r 3 -i ");
		strCMD += strPath;
		strCMD += _T(".\\Picture\\%d.jpg ");
		strCMD += strPath;
		strCMD += _T("output.gif -y");
		//给cmd发命令
		SendCmd(strCMD);
	}

视频文件生成Gif

  1. 截取片段函数(加载视频文件路径):
void Cutview()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
		strPath += _T("ffmpeg\\");
		CDuiString strViewPath = ((CEditUI*)m_PaintManager.FindControl(_T("edit_path")))->GetText();//获取用户输入的路径
		CDuiString strCMD;
		strCMD += _T("/c ");
		strCMD += strPath;
		strCMD += _T("ffmpeg -i ");
		//由用户输入的视频路径
		if (!strViewPath.IsEmpty())
		{
			strCMD += strViewPath;
		}
		else//由默认路径获取文件
		{
			strCMD += strPath;
			strCMD += _T("input.mkv ");//路径
		}
		strCMD += _T("-vcodec copy -acodec copy ");
		strCMD += _T("-ss ");

		//获取start和end
		CDuiString strStartTime = ((CEditUI*)m_PaintManager.FindControl(_T("edit_start")))->GetText();
		if (!IsValidTime(strStartTime))
		{
			MessageBox(NULL, _T("起始时间有误"), _T("MakeGif"), IDOK);
		}
		CDuiString strEndTime = ((CEditUI*)m_PaintManager.FindControl(_T("edit_end")))->GetText();
		if (!IsValidTime(strEndTime))
		{
			MessageBox(NULL, _T("终止时间有误"), _T("MakeGif"), IDOK);
		}
		strCMD += strStartTime;
		strCMD += _T(" -to ");
		strCMD += strEndTime;
		strCMD += _T(" ");

		//输出文件的路径
		strCMD += strPath;
		strCMD += _T("cut.mkv -y");

		SendCmd(strCMD);
	}
		void LoadFile()//加载视频文件的路径
	{
		OPENFILENAME ofn;
		memset(&ofn, 0, sizeof(OPENFILENAME));
		//设置参数
		TCHAR strPath[MAX_PATH] = { 0 };//MAX_PATH--->260
		ofn.lStructSize = sizeof(OPENFILENAME);
		ofn.lpstrFile = strPath;
		ofn.nMaxFile = sizeof(strPath);
		ofn.lpstrFilter = _T("All(*.*)\0 *.*\0mkv(*.mkv)\0 *.mkv\0");//使用这个过滤串,我们这里是过滤出mkv文件
		ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
		if (GetOpenFileName(&ofn))
		{
			//将文件的路径设置到edit
			((CEditUI*)m_PaintManager.FindControl(_T("edit_path")))->SetText(strPath);//将路径设置进编辑框中
		}
	}
  1. 提取字幕文件
    void GetSRTFile()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
		strPath += _T("ffmpeg\\");
		//1.构造命令
		CDuiString strCMD;
		strCMD += _T("/c ");
		strCMD += strPath;
		strCMD += _T("ffmpeg -i ");
		strCMD += strPath;
		strCMD += _T("cut.mkv ");//视频文件的路径
		strCMD += strPath;
		strCMD += _T("input.srt -y");//输出的字幕文件

		//向cmd发送命令
		SendCmd(strCMD);
	}

  1. 编辑字幕文件
	void LoadSRT()
	{
		//将.srt,加载到界面中list控件
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//文件路径
		strPath += _T("ffmpeg\\input.srt");
		std::ifstream fIn(strPath.GetData());//获取字符串类型

		char strSRTCon[512] = { 0 };
		CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));
		
		pList->RemoveAll();//第二次获取字幕时清理第一次获取的字幕
		while (!fIn.eof())//文件指针是否在结尾处
		{
			//字幕序号
			fIn.getline(strSRTCon, 512);

			//给出list中的文本
			CListTextElementUI* pListItem = new CListTextElementUI;
			pList->Add(pListItem);

			//读取时间轴
			fIn.getline(strSRTCon, 512);
			pListItem->SetText(0, UTF8ToUniCode(strSRTCon));
			//从0开始,将文本设置进去,此时strSRTCon是LPCTSTR类型的
			//win32项目是基于Unicode的,不是ASCII形式的,要进行转化

			//读取字幕
			fIn.getline(strSRTCon, 512);
			pListItem->SetText(1, UTF8ToUniCode(strSRTCon));

			//读取空行
			fIn.getline(strSRTCon, 512);
		}
		fIn.close();
	}
	void WriteSRT()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();
		strPath += _T("ffmpeg\\input.srt");
		std::ofstream fOut(strPath.GetData());

		// 获取文本内容
		CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));
		int szCount = pList->GetCount();//全部的行

		for (int i = 0; i < szCount; ++i)
		{
			CListTextElementUI* pListItem = (CListTextElementUI*)pList->GetItemAt(i);

			CDuiString strNo;
			strNo.Format(_T("%d"), i + 1);

			// 时间轴
			CDuiString strTime = pListItem->GetText(0);

			// 文本内容
			CDuiString strWord = pListItem->GetText(1);

			// 将获取到的内容写进srt文件中
			string strNewLine = Unicode2UTF8(_T("\n"));

			// 行号
			string itemNo = Unicode2UTF8(strNo);
			fOut.write(itemNo.c_str(), itemNo.size());
			fOut.write(strNewLine.c_str(), strNewLine.size());

			// 时间轴
			string itemTime = Unicode2UTF8(strTime);
			fOut.write(itemTime.c_str(), itemTime.size());
			fOut.write(strNewLine.c_str(), strNewLine.size());

			// 文本
			string itemWord = Unicode2UTF8(strWord);
			fOut.write(itemWord.c_str(), itemWord.size());
			fOut.write(strNewLine.c_str(), strNewLine.size());

			// 字幕和字幕之间都要换行 
			fOut.write(strNewLine.c_str(), strNewLine.size());
		}

		fOut.close();
	}
  1. 提取视频裸流
	void GenerateView()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
		strPath += _T("ffmpeg\\");
		CDuiString strCMD;
		strCMD += _T("/c ");//构造命令期间第一条必须是'/c'
		strCMD += strPath;
		strCMD += _T("ffmpeg -i ");//要加上\c参数
		strCMD += strPath;
		strCMD += _T("cut.mkv -vcodec copy -an -sn ");
		strCMD += strPath;
		strCMD += _T("cut2.mkv -y");

		SendCmd(strCMD);
	}

  1. 写入字幕
	void BornSRTtoView()
	{
		CDuiString strCMD;
		strCMD += _T("/c ");
		strCMD += _T("cd ");
		strCMD += CPaintManagerUI::GetInstancePath() + _T("ffmpeg");//获取路径
		strCMD += _T(" & ");
		//构造命令
		strCMD += _T("ffmpeg -i cut2.mkv -vf subtitles=input.srt cut3.mkv -y");
		SendCmd(strCMD);
	}
  1. 生成Gif
	void GenerateGifWithView()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
		strPath += _T("ffmpeg\\");
		
		CDuiString strCMD;
		strCMD += _T("/c ");
		strCMD += strPath;
		strCMD += _T("ffmpeg -i ");
		strCMD += strPath;
		strCMD += _T("cut3.mkv -vf scale=iw/2:ih/2 -f gif ");
		strCMD += strPath;
		strCMD += _T("output!.gif -y");

		SendCmd(strCMD);
	}

UTF-8、Unicode转换

	CDuiString UTF8ToUniCode(const char* str)
	{
		//获取转化之后的目标串的长度
		int szLen = ::MultiByteToWideChar(CP_UTF8, 0, str, strlen(str), NULL, 0);

		wchar_t* pContent = new wchar_t[szLen + 1];//为目标串申请空间

		//进行转化
		::MultiByteToWideChar(CP_UTF8, NULL, str, strlen(str), pContent, szLen);
		pContent[szLen] = '\0';
		CDuiString s(pContent);
		delete[]pContent;
		return s;
	}
		string Unicode2UTF8(CDuiString str)
	{
		int len = WideCharToMultiByte(CP_UTF8, 0, str.GetData(), -1, NULL, 0, NULL, NULL);
		CHAR *szUtf8 = new CHAR[len + 1]{0};

		::WideCharToMultiByte(CP_UTF8, 0, str.GetData(), -1, (LPSTR)szUtf8, len, NULL, NULL);
		string s(szUtf8);
		delete[] szUtf8;
		return s;
	}

判断时间是否有效

	bool IsValidTime(CDuiString strTime)//判断给的时间是否有效
	{
		//时间格式:xx:xx:xx
		if (strTime.GetLength() != 8)
		{
			return false;
		}
		for (int i = 0; i < strTime.GetLength(); ++i)
		{
			if (strTime[i] == ':')
				continue;
			if (!(strTime[i] >= '0' && strTime[i] <= '9'))
			{
				return false;
			}
		}
		return true;
	}
测试

由于本程序需要实现的功能简单明了,这里用测试V模型简单进行测试【略去验收测试】

单元测试

对各个模块接口进行测试

文件加载测试
按选择文件按钮,确认用户能够实现选择文件的功能
视频剪辑测试
因为视频剪辑使用ffmpeg工具,与cmd发送测试类似,此外要注意对输入的时间进行判断
cmd命令发送测试
打开cmd命令行,岔开输出的命令,验证命令输出组装是否成功
生成Gif测试
同样对cmd测试,验证命令输出组装是否成功

集成测试

根据概要设计文档对程序进行测试,将多个模块组合起来进行测试,观察运行结果是否符合预期结果

系统测试

首先进行冒烟测试,确认核心功能正常运行

功能【场景设计法】

  • 基本事件流:
    用户选择视频或图片后加载相应的文件,选择图片就直接生成Gif;选择视频需要由用户截取视频(截取时间段不超出加载视频时长),选择是否更换视频中字幕,最后生成Gif
  • 备选事件流:
    1.用户选择与加载文件不符合:如用户选择视频后却加载图片文件【操作有错】
    2.用户截取视频时间段超出加载的视频文件【操作有错】

界面
由于本程序对界面要求是简洁明了,符合常规即可
界面中元素文字信息与功能信息一致
易用性
界面风格符合大多使用人的习惯,功能能够适应用户需求的不断变化
性能
各按钮响应时间在3秒以内
兼容性
在各种类型的电脑上是否能正常运行

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值