視頻編輯場景下的文字模版技術方案
作者 | Lok'tar ogar
導讀
本文根據度咔剪輯APP文字模版開發實踐,分享視頻編輯場景下,靜態文字模版渲染能力的技術方案。作為富文本渲染方案的父集,此技術方案可以擴展到其他需要複雜富文本渲染的場景下。
全文6745字,預計閲讀時間17分鐘。
先睹為快
文字模版效果展示:
△文字模版在度咔剪輯中的應用
01 背景
視頻創作工具的核心競爭力之一是其豐富的素材庫,其中包括各種視頻素材、音頻素材以及貼紙素材等等。其中的文字模版也是不可或缺的一部分。文字模版提供了富文本的編輯功能,使用户能夠在視頻中添加更多樣式優美的文字信息,從而增添了視頻素材的多樣性。此外,通過預設的樣式,用户可以更加方便地選擇適合自己的文字模版,節省了素材選擇的時間,提升了用户體驗。 在度咔的早期版本中,我們並沒有提供文字模版這一素材類型。為了提升產品的競爭力和提高素材滲透率,我們進行了一定的研發工作,最終推出了文字模版素材。這些文字模版素材不僅可以滿足用户的需求,而且可以為用户提供更多的創作靈感和思路。同時,我們也不斷地更新和優化我們的素材庫,以確保用户能夠獲得最新和最優質的素材資源。 文字模版需要呈現的圖文樣式較為複雜,度咔文字模版已支持的特性見下面的列表:
02 整體設計
我們基於已建設的素材平台,新增了文字模版這一類型,並且在素材平台提供了素材編輯、預覽、配置上線的功能。素材生產和預覽相結合,可以在同一界面預覽剛剛調整的效果,可以直接匹配度咔的字體庫,以及直接修改圖片資源。這種素材生產方式具有高可複用性,用一個文字模版,改一個背景圖、新增一個描邊,就可以直接生產出另一個文字模版。發佈這一模版,並導出效果圖,即進入待審核隊列,審核後可配置上線。 截至目前,我們已上線了361套文字模版,並完成了【素材生產】-【素材平台預覽】-【素材下發和客户端載入】-【客户端渲染】的完整鏈路。
03 功能實現
3.1 素材生產
目前視頻編輯行業主流的素材格式通常採用資源文件和配置文件(描述文件)的方式進行。其中,資源文件包括圖片資源和字體文件,而配置文件則主要用於描述文字模版的排版屬性和渲染參數。這種方式的優點在於,生產端只需要通過特定字段來描述相關特性,就可以在渲染端呈現出來。這種方式靈活度較高,可以根據具體場景需要由簡單到複雜地迭代相關特性,同時實現成本也相對較低。不過,缺點在於素材生產形式是自定義的,需要一定的設計學習成本。 除此之外,還有一種生產方式是針對專業設計軟件的,以Photoshop(PS)為例。PS有比較成熟的文件格式文檔,包含了各類數據結構,可以直接使用PSD文件解析圖文屬性進行渲染。這種方式的優點在於素材生產方式較為通用,設計幾乎沒有學習成本。不過,缺點在於我們需要的一些特性無法通過PSD簡單地滿足,比如多層陰影效果,其是由多個文本框層層疊加得到的效果。在修改文字內容的時候,我們需要同步修改這幾個文本框,因此需要把它們作為一個組來處理,邏輯就變得比較複雜。如果用配置文件來描述,則可以直接進行多層繪製,免去複雜的邏輯處理。 考慮到業務ROI和短期上線功能的可行性,我們採用了第一種方式,並借鑑了黃油相機團隊的素材生產標準,設計了用於描述排版屬性和渲染參數的JSON結構。
3.2 端渲染
在視頻編輯場景下,文字的處理需要進行文字排版和文字繪製兩個部分的處理。對於文字排版,iOS平台採用了CoreText底層框架來進行排版處理,而Android則可以通過FontMetrics等獲取底層FreeType對於字形處理的結果。不論是將一段文字作為整體進行排版處理,還是分別計算每個文字的位置,總的處理的性能消耗是相同的。 在進行文字繪製方面,需要在性能開銷和開發成本之間尋求平衡。最終,iOS採用了QuartzCore框架,Android則使用Canvas來進行文字繪製。這樣,在預覽時,文字可以直接呈現在視圖上,支持實時編輯預覽。當需要將視頻導出時,我們將其處理為貼紙的形式添加在視頻中。以iOS為例,花字組件架構如下:
3.3 描述文件設計
上文提到,我們用json文件描述文字模版的排版屬性和渲染參數,資源下發到客户端後,客户端會解析對應參數,來進行文字模版的排版和最終效果呈現。描述文件中會涉及以下內容:
(1)文字排版屬性
- baseline:字符基線,baseline是虛擬的線
- ascent:字形最高點到baseline的推薦距離
- descent:字形最低點到baseline的推薦距離
- leading:行間距,即前一行的descent與下一行的ascent之間的距離
- advance width:Origin到下一個字形Origin的距離
- left-side bearing:Origin到字形最左邊的距離
- right-side bearing:字形最右邊到下一個字形Origin的距離
- bounding box:包含字形的最小矩形
- x-height:一般指小寫字母x最高點到baseline的推薦距離
- Cap-height:一般指H或I最高點到baseline的推薦距離
(2)文本對象組合 下圖為兩個文字繪製區域的組合示例
3.4 排版繪製流程
在我們的文字模版中,排版和繪製是密不可分的,需要在代碼邏輯中穿插進行處理。我們的繪製步驟是從底層到頂層逐層繪製,但由於一些繪製過程會消耗大量時間,為了避免阻塞主線程,我們使用了異步繪製的技術。在異步繪製的過程中,我們將一些比較耗時的繪製過程放在了後台線程中進行處理,這樣就能夠不影響用户的正常使用。同時,在異步繪製的過程中,我們也會進行文字排版的計算,這樣能夠在後續繪製過程中快速獲取到文字的相關信息,進而提高繪製效率。總的來説,我們通過採用異步繪製的方式,能夠保證文字模版的排版和繪製過程順利進行,同時也不會對用户造成太多的干擾。
04 難點與挑戰
1、多端效果的對齊
我們的項目支持web、iOS和Android端的渲染,但由於通用的跨端方案需要在底層使用OpenGL渲染,而當時的人力資源限制使得短期內難以實現。因此,我們採用了多端獨立渲染的方式,每個平台都有獨立的渲染方案。這種方式也帶來了一個問題:不同平台的渲染效果會有差異。 為了解決這個問題,我們需要保證多端效果的一致性。由於在技術層面難以抹平差異,我們決定通過規則和標準的統一來實現一致性。在設計json文件的格式時,我們就統一了多端渲染的標準,比如文字裝飾相對於文字的初始位置是左上角對齊還是居中對齊,座標原點的統一等。同時,我們還統一了對應的參數所用的單位,從而最大程度地保證了最終呈現效果的一致性。這樣,無論在哪個平台上渲染,我們都能夠得到一致的結果,使用户體驗更加統一和良好。
2、文字預排版
在文字模版中,我們將字號分為兩種類型:固定字號和非固定字號。對於固定字號,我們可以直接進行文字排版計算和繪製。然而,對於非固定字號的字體,我們需要進行預排版的計算,以算出當前文字內容下對應的字號大小。這裏有些方案是採用二分法,先定一個較大的字號值,在0到該字號值的範圍內逐漸逼近正確值,但這樣其實造成了非必要的時間損耗,結合文字排版的基礎邏輯和限定條件,我們可以做出時間複雜度接近O(1)的算法:計算最大字高->計算最小字高->計算字數最長行的字高->根據行數算字高->算出最終字高->根據字高算字號。iOS中用CoreText排版技術時,少部分情況會自動裁切填不滿的文字,直接用計算出的字號會導致部分文字被裁切,所以要將以上結果作為估算結果,將size逐次減1,直至能填入path。
CGFloat ascent, descent;
UIFont *font = [self.calFont fontWithSize:size];
CTFontRef fontRefMeasure = (__bridge CTFontRef)font;
[attrString addAttribute:(id)kCTFontAttributeName value:(__bridge id)fontRefMeasure range:NSMakeRange(0, attrString.length)];
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attrString);
CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
//calculate max font size
CGFloat calFontHeight = MIN(height, width);
self.maxFontHeight = calFontHeight;
//calculate min font size
CGFloat maxLine = self.document.maxLine * BDTZBigFontDataOriginScale;
if (maxLine <= 0) {
maxLine = 1;
}
calFontHeight = [self itemWidth] / (maxLine + (maxLine - 1) * (self.leadingRatio * BDTZBigFontDataOriginScale - 1));
self.minFontHeight = MIN(self.maxFontHeight, calFontHeight);
// longest column
int64_t n = 0;
NSArray *strArray = [self.document.content componentsSeparatedByString:@"\n"];
NSString *measureStr = self.document.content;
// 這裏是針對多行文本的處理,循環次數為行數,量級較小(一般為1-10行)
for (NSString *str in strArray) {
if (str.length > n) {
n = str.length;
measureStr = str;
}
}
CGFloat fontWidthRatioOrigin = (self.document.fontWidthRatio * BDTZBigFontDataOriginScale);
CGFloat trackingRatio = (self.document.trackingRatio * BDTZBigFontDataOriginScale) * (ascent + descent) / ascent;
CGRect rect = [@"我" boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:self.calFont} context:nil];
CGFloat fontWidthRatio = fontWidthRatioOrigin > 0 ? fontWidthRatioOrigin * (ascent + descent) / ascent : rect.size.width / rect.size.height;
CGFloat fontHeight = width / (n * fontWidthRatio + n * trackingRatio);
if (strArray.count > 1) {
//calculate font size accoring column count
calFontHeight = [self itemWidth] / (strArray.count + (strArray.count - 1) * (self.leadingRatio * BDTZBigFontDataOriginScale - 1));
//take the min value of the above two font sizes
fontHeight = MIN(fontHeight, calFontHeight);
}
if (fontHeight > self.maxFontHeight) {
fontHeight = self.maxFontHeight;
} else if (fontHeight < self.minFontHeight) {
fontHeight = self.minFontHeight;
}
CGFloat calSize = fontHeight;
calFontHeight = [self calculateFontHeightSize:calSize];
calSize = floorf(calSize / (calFontHeight * (ascent + descent) / ascent) * calSize);
//exact value, calculate repeatedly with frame until the path can be filled
//根據估算結果,將size逐次減1,直至能填入path,此處代碼省略
if (calSize <= 0) {
return calSize;
}
calFontHeight = [self calculateFontHeightSize:calSize];
self.fontHeight = calFontHeight * (ascent + descent) / ascent;
self.font = [self.calFont fontWithSize:calSize];
3、繪製性能
文字模版的實時預覽需要頻繁繪製,這會對CPU帶來較大的負擔,從而導致卡頓。為了解決這個問題,我們必須採用異步繪製的方式。具體來説,我們可以創建一個異步的串行隊列,來存儲用户每次操作的文字內容狀態。每當用户進行修改操作時,我們就將當前狀態加入到隊列中,等待後台線程進行異步繪製。當前一個狀態繪製完成後,我們再從隊列中取出下一個待繪製狀態,直到所有狀態都被繪製完畢。這樣,既實現了異步繪製防止卡主線程,又將用户每次修改的結果完整呈現出來。 為了進一步優化文字模版的用户體驗,除了異步繪製之外,還可以考慮使用緩存機制來提升渲染性能。在用户對文字模版進行操作時,文字視圖會重新進行排版和繪製,如果每次都重新繪製整個模版,不僅會佔用大量CPU資源,而且會降低用户體驗。因此,我們可以使用緩存來存儲已繪製的模版視圖,當用户修改文字內容時,只需要重新繪製被修改的部分,而不是整個視圖。通過這種方式,我們可以提高渲染性能,同時還能減少資源消耗,提高系統的響應速度。
4、內存優化
我們的文字模版主要用於視頻編輯場景,用户需要根據具體情況對文字模版進行放大或縮小。如果使用純矢量繪製刷新方式,當用户將文字模版放大到一定程度時,內存的佔用量將非常高。此外,我們的用户在編輯器中通常會添加許多素材,如貼紙、特效和字幕等,而這些素材中單個佔用內存較高,使用一段時間後,內存容易增加到OOM閾值,導致應用崩潰。因此,我們目前將單個文字模版的內存控制在20M以下,並根據不同視頻寬高的畫幅,計算出文字模版達到預期清晰度所需的寬高閾值,以實現清晰度和佔用內存大小的平衡,每個文字模版都有一個不同的平衡參數。儘管這只是一個內存優化的細節,但對於我們控制素材的內存佔用以及線上OOM率的控制起到了很大的作用。
05 結語
在視頻編輯領域中,富文本渲染是一個相當複雜的過程。就端渲染而言,沒有一種萬能的解決方案,只有最適合特定場景的解決方案。在設計和實現文字模版渲染方案的過程中,有許多細節需要考慮。同時,還需要深入瞭解主流設計軟件如PS、Figma等的文件格式。 我們團隊提供了靜態文字模版相關的技術方案,這些方案可以滿足較為普遍的富文本渲染場景。整體思路對於文字排版和繪製都是大致類似的。在本文中,我們介紹了基礎概念和富文本特性,以幫助讀者更好地理解我們的技術方案。 然而,即使是我們提供的方案,實現起來也需要考慮許多細節。我們需要考慮字體的大小、顏色、對齊方式、字距、行距等因素,以確保渲染出來的富文本能夠達到預期的效果。 因此,為了實現富文本渲染的最佳效果,需要投入大量時間和精力進行設計和實現。只有深入理解富文本的特性和設計原理,才能為用户提供高質量的視頻編輯體驗。
——END——
推薦閲讀: 淺談活動場景下的圖算法在反作弊應用
- 精準水位在流批一體數據倉庫的探索和實踐
- 視頻編輯場景下的文字模版技術方案
- 淺談活動場景下的圖算法在反作弊應用
- 百度工程師帶你玩轉正則
- Serverless:基於個性化服務畫像的彈性伸縮實踐
- 百度APP iOS端內存優化-原理篇
- 從稀疏表徵出發、召回方向的前沿探索
- 性能平台數據提速之路
- 採編式AIGC視頻生產流程編排實踐
- 百度工程師漫談視頻理解
- PGLBox 超大規模 GPU 端對端圖學習訓練框架正式發佈
- 百度工程師淺談分佈式日誌
- 百度工程師帶你瞭解Module Federation
- 巧用Golang泛型,簡化代碼編寫
- Go語言DDD實戰初級篇
- 百度工程師帶你玩轉正則
- Diffie-Hellman密鑰協商算法探究
- 貼吧低代碼高性能規則引擎設計
- 淺談權限系統在多利熊業務應用
- 分佈式系統關鍵路徑延遲分析實踐