一款批量修改AE模板的工具
一、需求分析
对于视频后期剪辑及相关从业人员来说,AE(After Effects)模板效果是一个不错的开始点。在模板效果的基础上,可以很快的做出各种炫酷的后期效果。但是在网上下载的模板工程中,往往包含了非常多的模板文字、图片、图形实体、AI资源等。这些资源文件往往并不是我们需要的,在使用模板时需要手动替换或者删除。但是网上下载的模板工程往往非常大,包含的资源非常多。这样手动改动起来的话,工作量会成倍增加。那么,是否可以考虑做一个小工具来高效完成这项枯燥的工作呢?要替换模板中的文字和图片,第一步就是要定位到这些图片和文字;其次才能考虑使用程序替换。那么,如何定位模板工程中的图片和文字呢?定位到之后又如何修改呢?如果要修改的话,又要修改哪些地方呢?接下来就来分析下整个解决过程。
二、实现方案
Adobe After Effects工程使用aep格式来存储。aep格式是一种紧凑的二进制格式,工程中的所有资源及组织结构都以二进制格式保存。如果要从这种二进制的格式中来定位图片和文字,倒也不是不可能:
但是有一个致命的缺点。先不说定位的时候无法做到精确匹配,就算成功找到了文本或图片路径,替换的时候很可能还要进行位置移动。因为替换的文本可能比原文本长,如果不移动腾出空位的话,替换的内容就会覆盖掉后面的二进制数据。修改后的aep文件极有可能因此损坏。因此,直接修改aep文件是不可取的。经过一番搜索,得知AE工程还有另外一种存储格式:AEPX。
*.aepx是以XML格式进行存储的。相对于二进制格式aep而言,aepx的文件尺寸比较大,加载速度也会慢些。但是XML格式非常容易操作,而且在成熟的XML库的帮助下,修改标签和遍历标签只需要几行代码即可搞定。那么,接下来的工作就是确定XML的组织结构以及需要修改哪些字段了。首先看一个比较复杂的AEP工程:
这是一个典型的AEP工程,使用文件夹的方式来组织各种资源。那么XML中是怎么组织的呢?上面这个工程中存在8个顶级文件夹,可以在XML中看到对应8个<Item>标签:
再来分析其中的合成(Composite):
这张图是关键的:我们可以看到,文件夹中的子元素是以<Sfdr>标签来包裹的。而不管是Composite还是文件夹,都是以<Item>标签来表示的,只不过以子标签<idta>的值来区分。0001开头的表示是文件夹,0004开头的表示合成,而0007开头的则表示是其他普通资源文件,如图片、AI文件等。经过分析,文本都是以<Layr>标签包裹的,我们要替换文本的话,直接替换子标签<string>中的文本即可。那么图片是怎样一种结构呢?
图片资源的引用是封装在<Pin>标签里面的<fileReference>里面,直接以路径的形式引用。确定了这些东西,就可以开始编码来定位文本和图片了。这里采用了一个C++ XML解析库TinyXML,不依赖其他外部库,接口简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | void XMLParser::parseTemplateItem(XMLNode* rootElement, int & index) { if (rootElement == nullptr ) { return ; } XMLElement* str = rootElement->FirstChildElement( "string" ); const char * txt = str->GetText(); XMLElement* idtaNode = rootElement->FirstChildElement( "idta" ); if (idtaNode != nullptr ) { const char * idatBdata = idtaNode->Attribute( "bdata" ); ItemType itemType = whichType(idatBdata); if (itemType == NORMAL_ITEM) { XMLElement* pinNode = idtaNode->NextSiblingElement( "Pin" ); if (pinNode != nullptr ) { XMLElement* sspcNode = pinNode->FirstChildElement( "sspc" ); if (sspcNode == nullptr ) { return ; } const char * sspcBdata = sspcNode->Attribute( "bdata" ); bool isNormalFormat = isImageFormat(sspcBdata); if (isNormalFormat) { XMLElement* Als2Node = sspcNode->NextSiblingElement( "Als2" ); if (Als2Node == nullptr ) { return ; } XMLElement* fileReferenceNode = Als2Node->FirstChildElement( "fileReference" ); if (fileReferenceNode == nullptr ) { return ; } const char * fullPath = fileReferenceNode->Attribute( "fullpath" ); m_imageMap.insertMulti(fullPath, index); index++; } } } else if (itemType == COMPOSITE_ITEM) { XMLElement* LayrNode = idtaNode->NextSiblingElement( "Layr" ); while (LayrNode != nullptr ) { XMLElement* stringNode = LayrNode->FirstChildElement( "string" ); if (stringNode) { // 文本为空的层直接跳过不要 const char * layerStr = stringNode->GetText(); if (layerStr != nullptr && strcmp (layerStr, "" )) { XMLElement* tdgpOuter = stringNode->NextSiblingElement( "tdgp" ); if (tdgpOuter) { XMLElement* tdmnOuter = tdgpOuter->FirstChildElement( "tdmn" ); if (tdmnOuter) { const char * tdmnOuterBdata = tdmnOuter->Attribute( "bdata" ); // 'ADBE Text Properties' if (tdmnOuterBdata != nullptr && ! strcmp ( "4144424520546578742050726f706572746965730000000000000000000000000000000000000000" , tdmnOuterBdata)) { XMLElement* tdgpInner = tdmnOuter->NextSiblingElement( "tdgp" ); if (tdgpInner != nullptr ) { XMLElement* tdmnInner = tdgpInner->FirstChildElement( "tdmn" ); if (tdmnInner != nullptr ) { const char * tdmnInnerBdata = tdmnInner->Attribute( "bdata" ); // 'ADBE Text Document' if (tdmnInnerBdata != nullptr || ! strcmp ( "41444245205465787420446f63756d656e7400000000000000000000000000000000000000000000" , tdmnInnerBdata)) { m_textMap.insertMulti(layerStr, index); index++; } } } } } } } } LayrNode = LayrNode->NextSiblingElement( "Layr" ); } } else if (itemType == FOLDER_ITEM) { XMLElement* SfdrNode = idtaNode->NextSiblingElement( "Sfdr" ); if (SfdrNode == nullptr ) { return ; } XMLElement* tempItem = SfdrNode->FirstChildElement( "Item" ); while (tempItem != nullptr ) { parseTemplateItem(tempItem, index); tempItem = tempItem->NextSiblingElement( "Item" ); } } else { return ; } } } |
三、修改字段
完成了图片和文字的解析工作之后,剩下的就是替换了。不妨先来观察下使用AE修改资源时,XML文件会发生哪些变化。这样,我们用程序修改时,把相关的字段也修改掉就可以了。对于图片修改可以看下图:
总共需要修改三个地方。其中,"4a504547"是JPEG这八个字符的十六进制表示,有两个地方需要同时修改。如果是替换成其他格式的图片,也要修改成对应格式的十六进制表示。如:
1 2 3 4 | '706e6721' -> PNG format '4a504547' -> JPEG or JPG format '5449465f' -> TIF or TIFF format '424d5020' -> BMP format |
另外一个要修改的就是<fileReference>的属性fullpath值了。也就是图片资源的路径。文本的修改就要稍显复杂一点了。如下图:
这里采用了一个小技巧,使用了文本层的一个属性:text.sourceText=name。给了这个属性之后,文本层的内容和名称保持一致。也即是说,我们只要修改文本层的名称,就能达到修改文本层内容的目的。这个技巧需要修改两个地方。一个是<tdb4>标签值的倒数第七位置1;另一个就是增加一个<tdb4>的兄弟标签<expr>,其值为“746578742e736f75726365546578743d6e616d6500”,也就是"text.sourceText=name"的十六进制表示。这样就实现了文本层和文本内容的同步设置了。
此外,Layr层不光只有text在里面,还有色块(Solid)、过渡效果、动画等内容。因此还需要根据<tdmn>标签的值来过滤。条件就是<tdmn>的值:
1 2 3 4 5 6 7 8 9 10 11 12 | 4144424520546578742050726f706572746965730000000000000000000000000000000000000000 // 'ADBE Text Properties' 41444245205472616e73666f726d2047726f75700000000000000000000000000000000000000000 // 'ADBE Transform Group' 41444245204c61796572205374796c65730000000000000000000000000000000000000000000000 // 'ADBE Layer Styles' 414442452045787472736e204f7074696f6e732047726f7570000000000000000000000000000000 // 'ADBE Extrsn Options Group' 41444245204d6174657269616c204f7074696f6e732047726f757000000000000000000000000000 // 'ADBE Material Options Group' 4144424520417564696f2047726f7570000000000000000000000000000000000000000000000000 // 'ADBE Audio Group' 414442452047726f757020456e640000000000000000000000000000000000000000000000000000 // 'ADBE Group End' 41444245205465787420446f63756d656e7400000000000000000000000000000000000000000000 // 'ADBE Text Document' 4144424520546578742050617468204f7074696f6e73000000000000000000000000000000000000 // 'ADBE Path Options' 414442452054696d652052656d617070696e67000000000000000000000000000000000000000000 // 'ADBE Time Remapping' 4144424520506c616e65204f7074696f6e732047726f757000000000000000000000000000000000 // 'ADBE Plane Options Group' 41444245204566666563742050617261646500000000000000000000000000000000000000000000 // 'ADBE Effect Parade' |
只有内层<tdmn>和外层<tdmn>的值分别是'ADBE Text Properties'和'ADBE Text Document'的时候,<Layr>中包含的才是文本。这种过滤条件,能够过滤掉其他的干扰数据,让我专注于处理模板中的文本内容。
四、最终效果
作者:24K纯开源
Email: zhangzhongke007@163.com
出处:http://www.cnblogs.com/csuftzzk/
本文版权归24K纯开源和博客园共同拥有,欢迎转载,但未经作者同意必须保留此声明,且在文章明显位置给出原文链接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】