好的,我们开始今天的讲座。主题是 LibTxt 引擎解析:ParagraphBuilder 如何将样式映射到 Skia/Impeller。
LibTxt 是一个用于文本布局和渲染的库,它抽象了底层的图形 API,例如 Skia 和 Impeller。 ParagraphBuilder 是 LibTxt 中用于构建文本段落的核心组件。它的主要职责是将文本内容和样式信息转换为底层图形 API 可以理解的形式,最终完成文本的绘制。 今天我们将深入探讨 ParagraphBuilder 如何将文本样式(例如字体、颜色、字号、粗细等)映射到 Skia 和 Impeller 这两个不同的渲染引擎。
ParagraphBuilder 的总体架构
在深入细节之前,我们先了解一下 ParagraphBuilder 的总体架构。ParagraphBuilder 接收文本内容和样式指令,并将其存储在一个内部的数据结构中。这个数据结构通常是一个由 TextRun 对象组成的列表,每个 TextRun 对象代表一段具有相同样式的文本。
// 简化版的 TextRun 结构体
struct TextRun {
std::string text;
TextStyle style;
};
// 简化版的 TextStyle 结构体
struct TextStyle {
std::string fontFamily;
double fontSize;
SkColor color;
SkFontStyle fontStyle; // 包含粗细、斜体等信息
// 其他样式属性
};
class ParagraphBuilder {
public:
ParagraphBuilder();
~ParagraphBuilder();
void addText(const std::string& text);
void pushStyle(const TextStyle& style);
void pop();
// ... 其他方法
private:
std::vector<TextRun> runs_;
std::vector<TextStyle> styleStack_;
};
addText 方法将文本添加到当前段落,并应用当前样式栈顶部的样式。pushStyle 方法将新的样式压入栈中,而 pop 方法则弹出栈顶的样式。通过这种方式,ParagraphBuilder 可以处理复杂的嵌套样式。
样式映射到 Skia
Skia 是一个开源的 2D 图形库,被广泛应用于 Chrome、Android 等平台。ParagraphBuilder 在使用 Skia 作为渲染后端时,需要将 TextStyle 中的属性转换为 Skia 对应的类型。
- 字体映射:
TextStyle 中的 fontFamily 和 fontSize 属性需要转换为 Skia 的 SkFont 对象。这涉及到字体查找和字形缓存。
#include "include/core/SkFont.h"
#include "include/core/SkFontMgr.h"
SkFont CreateSkFont(const TextStyle& style) {
SkFontMgr* fontMgr = SkFontMgr::RefDefault();
SkString familyName(style.fontFamily.c_str());
SkFontStyle fontStyle = style.fontStyle;
SkFont skFont(fontMgr->matchFamilyStyle(familyName.c_str(), fontStyle), style.fontSize);
fontMgr->unref();
return skFont;
}
SkFontMgr::matchFamilyStyle 方法用于查找与给定字体名称和样式匹配的字体。SkFont 对象包含了字体数据和字形信息,可以用于绘制文本。 SkFontStyle 可以由 TextStyle 中的粗细(weight)、倾斜(slant)、宽度(width)转换而来,例如:
SkFontStyle ConvertToSkFontStyle(const TextStyle& style) {
SkFontStyle::Slant slant;
switch (style.fontStyle.slant) {
case FontSlant::kUpright:
slant = SkFontStyle::Slant::kUpright;
break;
case FontSlant::kItalic:
slant = SkFontStyle::Slant::kItalic;
break;
case FontSlant::kOblique:
slant = SkFontStyle::Slant::kOblique;
break;
default:
slant = SkFontStyle::Slant::kUpright;
break;
}
int weight = style.fontStyle.weight; // 例如 400 代表 Normal, 700 代表 Bold
int width = style.fontStyle.width; // 字体宽度,正常情况下是 5 (kNormal_Width)
return SkFontStyle(weight, width, slant);
}
- 颜色映射:
TextStyle 中的 color 属性可以直接转换为 Skia 的 SkColor 类型。
SkColor ConvertToSkColor(const TextStyle& style) {
return style.color;
}
- 其他样式:
TextStyle 中可能包含其他样式属性,例如下划线、删除线、背景色等。这些属性需要转换为 Skia 对应的绘制操作。例如,下划线可以通过绘制一条线来实现。
void DrawUnderline(SkCanvas* canvas, const SkFont& font, const SkColor& color, SkScalar x, SkScalar y, SkScalar width) {
SkPaint paint;
paint.setColor(color);
paint.setStyle(SkPaint::kStroke_Style);
paint.setStrokeWidth(1.0); // 可根据需要调整
SkFontMetrics metrics;
font.getMetrics(&metrics);
SkScalar underlinePosition = y + metrics.underlinePosition;
canvas->drawLine(x, underlinePosition, x + width, underlinePosition, paint);
}
在 ParagraphBuilder 中,这些转换后的 Skia 对象和绘制操作会被用于构建 SkTextBlob 对象。SkTextBlob 对象是一个包含文本和样式信息的不可变对象,可以高效地进行绘制。
#include "include/core/SkTextBlob.h"
SkTextBlobBuilder blobBuilder;
SkFont skFont = CreateSkFont(run.style);
SkColor skColor = ConvertToSkColor(run.style);
SkPaint paint;
paint.setColor(skColor);
blobBuilder.allocRunText(skFont, run.text.size(), 0, 0).utf8Text(run.text.c_str(), run.text.size());
sk_sp<SkTextBlob> blob = blobBuilder.make();
// 绘制 TextBlob
canvas->drawTextBlob(blob.get(), x, y, paint);
样式映射到 Impeller
Impeller 是 Flutter 团队开发的新的渲染引擎,旨在解决 Skia 在移动设备上的一些性能问题。Impeller 使用 Metal (iOS) 和 Vulkan (Android) 作为图形 API。ParagraphBuilder 在使用 Impeller 作为渲染后端时,需要将 TextStyle 中的属性转换为 Impeller 对应的类型。
Impeller 与 Skia 的主要区别在于 Impeller 在设计上更加面向硬件,并且使用了不同的数据结构和渲染流程。因此,ParagraphBuilder 需要进行不同的样式映射。
- 字体映射:
Impeller 使用自己的字体引擎,因此 ParagraphBuilder 需要将 TextStyle 中的 fontFamily 和 fontSize 属性转换为 Impeller 字体引擎可以理解的形式。这涉及到查找字体文件、解析字体数据,并创建 Impeller 的字体对象。
#include "impeller/typographer/font.h"
#include "impeller/typographer/font_context.h"
std::shared_ptr<impeller::Font> CreateImpellerFont(const TextStyle& style, impeller::FontContext& font_context) {
// 根据字体名称查找字体文件
std::string fontPath = FindFontPath(style.fontFamily);
// 创建字体对象
auto font = font_context.GetFontCollection().FindFont(
style.fontFamily.c_str(),
style.fontSize,
ConvertToImpellerFontStyle(style.fontStyle)
);
return font;
}
impeller::FontWeight ConvertToImpellerFontWeight(int weight) {
if (weight >= 700) {
return impeller::FontWeight::kBold;
} else if (weight >= 500) {
return impeller::FontWeight::kMedium;
} else {
return impeller::FontWeight::kNormal;
}
}
impeller::FontStyle ConvertToImpellerFontStyle(const TextStyle& style) {
impeller::FontStyle font_style;
font_style.weight = ConvertToImpellerFontWeight(style.fontStyle.weight);
font_style.slant = (style.fontStyle.slant == FontSlant::kItalic || style.fontStyle.slant == FontSlant::kOblique) ? impeller::FontSlant::kItalic : impeller::FontSlant::kUpright;
return font_style;
}
FindFontPath 函数负责根据字体名称查找字体文件的路径。impeller::FontContext 用于管理字体资源。impeller::FontCollection::FindFont 方法用于创建字体对象。
- 颜色映射:
TextStyle 中的 color 属性需要转换为 Impeller 的 Color 类型。
#include "impeller/geometry/color.h"
impeller::Color ConvertToImpellerColor(const TextStyle& style) {
return impeller::Color(
SkColorGetR(style.color) / 255.0f,
SkColorGetG(style.color) / 255.0f,
SkColorGetB(style.color) / 255.0f,
SkColorGetA(style.color) / 255.0f
);
}
- 文本布局:
与 Skia 不同,Impeller 并没有 SkTextBlob 这样的概念。Impeller 的文本布局过程更加精细,需要手动计算每个字形的坐标和绘制指令。
#include "impeller/typographer/text_run.h"
#include "impeller/typographer/glyph_map.h"
impeller::TextRun CreateImpellerTextRun(const std::string& text, const TextStyle& style, impeller::FontContext& font_context) {
auto font = CreateImpellerFont(style, font_context);
if (!font) {
return {}; // 或者抛出异常
}
impeller::TextRun text_run;
text_run.font = font;
text_run.text = text;
impeller::GlyphMap glyph_map = font->GetGlyphMap(text);
text_run.glyph_data = glyph_map.GetGlyphData(); // 获取字形数据(索引、偏移等)
// 计算文本宽度
text_run.size = font->MeasureText(text);
return text_run;
}
// 绘制文本run
void DrawImpellerTextRun(const impeller::TextRun& run, const impeller::Color& color, impeller::ContentContext& content_context, impeller::Matrix matrix, impeller::Allocator& allocator) {
if (!run.font || run.text.empty()) {
return;
}
// 构建绘制指令,例如使用SDF渲染
impeller::GlyphAtlas::TextureSet glyph_atlas_texture_set = content_context.GetGlyphAtlas()->GetTextureSet();
if(!glyph_atlas_texture_set.valid) {
return;
}
// 创建渲染通道(RenderPass)并添加绘制指令
auto render_target = impeller::RenderTarget::CreateOffscreen(content_context.GetDevice(), {1024, 1024}); // 示例尺寸
auto pass = render_target.CreateRenderPass();
pass->SetColorSource(render_target.GetColorTexture());
size_t glyph_count = run.glyph_data.size();
for(size_t i = 0; i < glyph_count; ++i) {
impeller::GlyphData glyph = run.glyph_data[i];
// 计算字形的位置和变换矩阵
impeller::Matrix glyph_matrix = matrix * impeller::Matrix::MakeTranslation(glyph.offset);
// 构建顶点数据和索引数据,并添加到渲染通道
// 具体实现取决于 Impeller 的渲染管线和使用的 Shader
// 示例:使用SDF渲染字形
// ... 构建顶点数据和索引数据,使用 glyph_atlas_texture_set 中的纹理坐标 ...
// pass->AddCommand(command); // 添加绘制命令
}
if (pass->IsValid()) {
pass->EncodeCommands(content_context.GetCommandEncoder());
content_context.Submit(render_target);
}
}
这段代码展示了如何创建 Impeller 的 TextRun 对象,并使用 ContentContext 和 RenderTarget 进行渲染。 实际的 Impeller 渲染过程非常复杂,涉及到顶点数据的构建、Shader 的编写和纹理的管理。
Skia 与 Impeller 的差异对比
为了更清晰地理解 Skia 和 Impeller 的差异,我们用表格的形式进行对比。
| 特性 | Skia | Impeller |
|---|---|---|
| API 抽象程度 | 较高,抽象了底层图形 API | 较低,更接近硬件 |
| 文本对象 | SkTextBlob | 无类似对象,需要手动布局和绘制 |
| 字体处理 | 使用 SkFontMgr 和 SkFont | 使用 FontContext 和 Font |
| 渲染流程 | 基于 Canvas 的命令式渲染 | 基于 RenderPass 的声明式渲染 |
| 性能 | 在移动设备上可能存在性能瓶颈 | 针对移动设备优化,性能更佳 |
| 跨平台性 | 良好,支持多种平台 | 主要针对 iOS 和 Android 平台 |
ParagraphBuilder 的策略选择
ParagraphBuilder 需要根据不同的渲染后端选择不同的样式映射策略。这可以通过条件编译或者运行时判断来实现。
class ParagraphBuilder {
public:
// ...
void build(RenderingBackend backend) {
if (backend == RenderingBackend::kSkia) {
buildWithSkia();
} else if (backend == RenderingBackend::kImpeller) {
buildWithImpeller();
} else {
// ...
}
}
private:
void buildWithSkia();
void buildWithImpeller();
// ...
};
在 buildWithSkia 和 buildWithImpeller 方法中,ParagraphBuilder 可以使用不同的代码路径来将样式映射到对应的渲染引擎。
处理复杂样式
除了基本的字体、颜色和字号之外,TextStyle 还可能包含其他复杂的样式属性,例如:
- 文本阴影: 在 Skia 中,可以使用
SkMaskFilter::MakeShadow来创建阴影效果。在 Impeller 中,需要手动实现阴影的绘制。 - 渐变色: 在 Skia 中,可以使用
SkShader::MakeLinearGradient或SkShader::MakeRadialGradient来创建渐变色。在 Impeller 中,也需要使用类似的 API。 - 文本装饰(下划线、删除线): 这些装饰可以通过绘制额外的线条来实现。
ParagraphBuilder 需要根据不同的样式属性选择合适的绘制方法,并将这些方法应用到文本的绘制过程中。
性能优化
文本渲染是一个计算密集型的任务,因此性能优化至关重要。ParagraphBuilder 可以采用以下策略来提高性能:
- 缓存字体对象: 避免重复创建字体对象。
- 使用 TextBlob (Skia): 将文本和样式信息打包成 TextBlob 对象,减少绘制调用次数。
- 批量渲染: 将多个文本段落合并成一个批次进行渲染。
- 使用硬件加速: 尽可能利用 GPU 进行渲染。
- 减少状态切换: 尽量减少绘制状态的切换次数。
多语言支持
LibTxt 需要支持多种语言,包括从左到右 (LTR) 和从右到左 (RTL) 的文本。ParagraphBuilder 需要根据文本的方向选择合适的布局算法,并将文本正确地排列。这涉及到 Unicode 双向算法 (Bidirectional Algorithm) 的实现。
总结
ParagraphBuilder 在 LibTxt 引擎中扮演着至关重要的角色,它负责将文本内容和样式信息转换为底层图形 API 可以理解的形式。为了支持 Skia 和 Impeller 两种不同的渲染引擎,ParagraphBuilder 需要采用不同的样式映射策略。理解 Skia 和 Impeller 的差异,以及掌握性能优化的技巧,对于开发高性能的文本渲染应用至关重要。
思考题
- 如何在 ParagraphBuilder 中实现文本换行和对齐?
- 如何支持自定义字体和字体回退?
- 如何在 Impeller 中实现复杂的文本效果,例如文本阴影和渐变色?
进一步学习的建议
- 阅读 Skia 和 Impeller 的官方文档。
- 研究 LibTxt 的源代码。
- 学习 Unicode 双向算法。
- 关注文本渲染领域的最新技术发展。
一些关键点的概括
ParagraphBuilder 负责文本样式到Skia/Impeller的转换。
Skia和Impeller的架构差异导致了不同的样式映射方法。
性能优化和多语言支持是文本渲染的重要考虑因素。