一、需求分析
對於視頻后期剪輯及相關從業人員來說,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,不依賴其他外部庫,接口簡單。
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這八個字符的十六進制表示,有兩個地方需要同時修改。如果是替換成其他格式的圖片,也要修改成對應格式的十六進制表示。如:
'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>的值:
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>中包含的才是文本。這種過濾條件,能夠過濾掉其他的干擾數據,讓我專注於處理模板中的文本內容。