Skip to content

Add PPTExporter with PPTX export support for PAGX documents#3361

Open
OnionsYu wants to merge 141 commits intoTencent:mainfrom
OnionsYu:feature/onionsyu_export_ppt2
Open

Add PPTExporter with PPTX export support for PAGX documents#3361
OnionsYu wants to merge 141 commits intoTencent:mainfrom
OnionsYu:feature/onionsyu_export_ppt2

Conversation

@OnionsYu
Copy link
Copy Markdown
Contributor

@OnionsYu OnionsYu commented Apr 3, 2026

新增 PPT 导出功能,支持将 PAGX 文档导出为 PPTX 格式。

主要变更:

  • 新增 PPTExporter,支持形状、文本、图片填充、渐变、描边、阴影、蒙版等元素导出为 PPTX
  • 提取 ExporterUtils 公共工具模块,供 SVG 和 PPT 导出器共用
  • 重构 SVGExporter 和 SVGTextLayout 复用 ExporterUtils 中的共享方法
  • 在 CLI CommandExport 中集成 PPT 导出命令
  • 新增 PPT 导出测试用例

@OnionsYu OnionsYu force-pushed the feature/onionsyu_export_ppt2 branch from 1bc505f to cdaf2a3 Compare April 3, 2026 08:07
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 78.44747% with 919 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.49%. Comparing base (96a6e0a) to head (7e1d0e4).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/pagx/ppt/PPTContourUtils.cpp 15.26% 210 Missing and 1 partial ⚠️
src/pagx/utils/ExporterUtils.cpp 74.03% 92 Missing and 49 partials ⚠️
src/pagx/ppt/PPTTextWriter.cpp 74.45% 74 Missing and 31 partials ⚠️
src/pagx/ppt/PPTGeomEmitter.cpp 41.00% 79 Missing and 3 partials ⚠️
src/pagx/ppt/PPTStyleEmitter.cpp 77.32% 66 Missing and 12 partials ⚠️
src/pagx/ppt/PPTWriter.h 72.04% 39 Missing and 32 partials ⚠️
src/pagx/ppt/PPTExporter.cpp 85.48% 31 Missing and 23 partials ⚠️
src/pagx/xml/XMLBuilder.h 75.64% 23 Missing and 24 partials ⚠️
src/pagx/svg/SVGExporter.cpp 68.75% 26 Missing and 14 partials ⚠️
src/pagx/ppt/PPTModifierResolver.cpp 88.02% 21 Missing and 16 partials ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3361      +/-   ##
==========================================
+ Coverage   78.41%   80.49%   +2.07%     
==========================================
  Files         532      550      +18     
  Lines       41448    48626    +7178     
  Branches    12486    13460     +974     
==========================================
+ Hits        32502    39142    +6640     
- Misses       6391     6750     +359     
- Partials     2555     2734     +179     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

OnionsYu added 17 commits April 23, 2026 14:27
…calation, textbox containers, and export-option toggles to raise PPT module coverage from 64% to 87%.
…n bakes render at the requested pixel density.
…ing text-as-path glyphs inline so WeChat still pins the bbox and PowerPoint stops painting dots at corners.
@OnionsYu OnionsYu force-pushed the feature/onionsyu_export_ppt2 branch from 6018ec9 to 0e5392f Compare April 29, 2026 09:05
…rimitive and dispatching writes through early returns.
… the group and multi-group paths share bounds marker handling.
Copy link
Copy Markdown
Collaborator

@shlzxjp shlzxjp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review 第四轮:新发现 14 个问题(1 Critical、6 Major、7 Minor),详见行级评论。

Comment thread src/pagx/utils/ExporterUtils.cpp Outdated
if (memcmp(data + offset + 4, "IDAT", 4) == 0) {
break;
}
offset += 12 + chunkLen;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] PNG chunk 遍历存在无限循环风险。offset += 12 + chunkLen12 + chunkLenuint32_t 运算,当畸形 PNG 的 chunkLen 接近 0xFFFFFFF4 时,加法回绕为 0,offset 不变导致死循环。GetPNGDPI 也有相同模式(第496行)。建议转为 size_t 后再加:offset += 12 + static_cast<size_t>(chunkLen);

Comment thread src/pagx/ppt/PPTExporter.cpp Outdated
return;
}

auto* mutableText = const_cast<Text*>(text);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] const_cast<Text*> 存在未定义行为风险。此处以及第 2198、2228 行(SVGExporter.cpp 的 899、997 行也有同样问题),TextLayout::Layout()getTextLines() 要求非 const 指针,迫使调用方对 const Text* 做 cast。如果 Text 对象是 const 构造的,任何内部修改均为 UB。建议让 TextLayout::Layout / getTextLines / getTextBounds 接受 const Text*,或让导出器从源头持有非 const 引用。

if (!run->font || run->glyphs.empty()) {
continue;
}
float scale = run->fontSize / static_cast<float>(run->font->unitsPerEm);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] run->font->unitsPerEm 未检查是否为 0。当前第 248 行的跳过条件只检查了 !run->font,但损坏字体数据的 unitsPerEm == 0 会导致此处除零产生 inf/NaN,传播到所有字形坐标和 EMU 转换,生成畸形 OOXML。建议在跳过条件中增加 || run->font->unitsPerEm <= 0

Comment thread src/pagx/ppt/PPTModifierResolver.cpp Outdated
generated.reserve(static_cast<size_t>(maxCount));
for (int i = 0; i < maxCount; ++i) {
float progress = static_cast<float>(i) + rep->offset;
float sx = std::pow(rep->scale.x, progress);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] std::pow(负数, 分数) 返回 NaN。rep->offset 使 progress 成为分数,当 rep->scale.xrep->scale.y 为负时,std::pow 返回 NaN 并传播到下游几何计算。建议将 scale 基数钳制为非负,或使用 std::abs 并重新应用符号。

Comment thread src/pagx/ppt/PPTGeomEmitter.cpp Outdated
namespace pagx {

int64_t PxToEMU(float px) {
return static_cast<int64_t>(std::round(static_cast<double>(px) * EMU_PER_PX));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] 极端像素值存在未定义行为。当 px 超过 ~9.68e14(INT64_MAX / 9525)时,乘积溢出 int64_t 范围,static_cast<int64_t> 从越界 double 转换是 UB。建议钳制:return static_cast<int64_t>(std::clamp(std::round(v), (double)INT64_MIN, (double)INT64_MAX));

Comment thread src/pagx/ppt/PPTExporter.cpp Outdated
const std::vector<LayerStyle*>& styles) {
out.openElement("a:r").closeElementStart();
out.openElement("a:rPr")
.addRequiredAttribute("lang", "en-US")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] 语言标签硬编码为 en-US(此处及第 1959 行、PPTBoilerplate.cpp 等处)。对 CJK 为主的文档,PowerPoint 会对正常文本显示拼写检查红线。建议考虑启发式检测(如文本含 CJK 码点则用 zh-CN),或作为选项暴露。

for (const auto& verb : verbs) {
if (verb == PathVerb::Move) {
contours.emplace_back();
contours.back().start = points[ptIndex++];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] points[ptIndex++] 无越界检查。如果 PathData 的 verbs 和 points 数量不一致,ptIndex 会超出 points.size() 导致越界访问。建议添加 assert(ptIndex < points.size()) 或 bounds check。

Comment thread src/pagx/xml/XMLBuilder.h
if (_prettyPrint) {
_buf += '\n';
}
_tags.pop_back();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] _tags.pop_back() 无空检查(此处及第 208、223 行)。open/close 不匹配时会触发 UB。同时第 305 行 _indent 为负时 static_cast<size_t> 回绕为巨大数导致 OOM。建议添加 assert(!_tags.empty())assert(_indent >= 0) 做调试期防御。

std::vector<uint8_t> inside;
size_t n = 0;

bool isInside(size_t i, size_t j) const {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] isInside(i, j) 直接以 i * n + j 索引 inside 数组,无越界校验。所有当前调用方均由循环索引保护,但建议添加 assert(i < n && j < n) 防止未来误用。

Comment thread src/pagx/ppt/PPTExporter.cpp Outdated
// Maps dash-to-stroke-width ratios to OOXML preset dash types (ISO/IEC 29500-1,
// §20.1.10.48 ST_PresetLineDashVal). Thresholds approximate the boundary between
// dot (≤1.5×), dash (≤4.5×), and long-dash (>4.5×) categories.
static const char* MatchPresetDash(const std::vector<float>& dashes, float strokeWidth) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] MatchPresetDash1.5f2.0f4.5f 等阈值用于匹配 OOXML 预设虚线类型,但缺少规范来源注释。建议添加引用 ISO/IEC 29500-1 §20.1.10.48(ST_PresetLineDashVal)说明各阈值的对应关系。

s.reserve(512);
s += XML_DECL;
s += "<cp:coreProperties "
"xmlns:cp=\"http://schemas.openxmlformats.org/package/2006/metadata/core-properties\" "
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

发现好几个函数中都有增加xmlns,排查下ppt文件中是不是重复冗余的

Comment thread src/cli/CommandExport.cpp Outdated
<< " backdrop beneath the layer, so the blend composites\n"
<< " correctly at the cost of turning native content under\n"
<< " the patch into baked pixels (default: off, the blend\n"
<< " mode is silently dropped and the layer renders Normal)\n"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[建议] CLI 的 PPT 参数过多,建议精简。当前 5 个 --ppt-* 参数中有 4 个是关闭默认光栅化行为的调试级选项(关掉后蒙版/裁剪/平铺被静默丢弃,普通用户不会想要这些效果):

参数 关掉后的用户可见效果 面向谁
--ppt-no-bake-mask 蒙版被丢弃 开发调试
--ppt-no-bake-scroll-rect 裁剪区域被丢弃,内容溢出 开发调试
--ppt-no-bake-tiled-pattern 跨应用平铺渲染不一致 开发调试
--ppt-no-bridge-contours 部分渲染器填充可能出错 开发调试

建议:

  1. CLI 只保留 --text-to-path--ppt-rasterize-blend(用户真正需要做取舍的选项)
  2. 新增 --ppt-raster-dpi <n>(C++ API 已有 rasterDPI 但 CLI 未暴露,用户确实可能需要调整导出清晰度/文件大小)
  3. 上述 4 个调试参数从 CLI 移除,仅保留在 C++ API 供高级用户使用

这样 PPT 选项从 14 行 help 文本降到 3 行,与 SVG 的 2 个参数量级一致,用户认知成本更低。

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--ppt-no-bridge-contours 参数还是先去掉吧,小众场景,代码层面兼容的,不用对外暴露命令行参数了

Comment thread src/pagx/xml/XMLBuilder.h
*
* All mutating methods return *this to allow chaining.
*/
class XMLBuilder {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PAGXExporter.cpp里面已经有一个XMLBuilder class了

OnionsYu and others added 4 commits April 30, 2026 10:21
…rtedBlend, and rasterizeWideGamut into a single rasterizeUnsupported option defaulting to false; tile patterns now always bake.
…m --ppt-no-bridge-contours to --ppt-bridge-contours.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cripts, detect CJK language, split PPTWriter into style and text units, and drop unused xmlns on some boilerplate parts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…conf.h.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@shlzxjp shlzxjp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

提交之前的 pending review。

size_t ptIndex = 0;
for (const auto& verb : verbs) {
if (verb == PathVerb::Move) {
assert(ptIndex < points.size());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert全局替换成DEBUG_ASSERT

Comment thread src/pagx/xml/XMLBuilder.h
_indent--;
writeIndent();
}
assert(!_tags.empty());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

使用DEBUG_ASSERT,全局统一修改下

size_t ptIndex = 0;
for (const auto& verb : verbs) {
if (verb == PathVerb::Move) {
assert(ptIndex < points.size());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] assert(ptIndex < points.size()) 在 Release 构建中被移除。若 PathData 的 verbs 与 points 数量不匹配(如文件损坏),ptIndex 将越界读取,导致崩溃或脏数据。建议替换为运行时边界检查 + early return:if (ptIndex >= points.size()) break;。第 216 行同理。

Comment thread src/pagx/xml/XMLBuilder.h
_indent--;
writeIndent();
}
assert(!_tags.empty());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] closeElement()/closeElementSelfClosing()assert(!_tags.empty()) 在 Release 构建中被移除。若 open/close 调用不匹配(逻辑 bug 或异常路径),对空 vector 调用 back()/pop_back() 是 UB,通常导致崩溃。建议在 assert 后补上运行时保护:if (_tags.empty()) return *this;

Comment thread src/pagx/xml/XMLBuilder.h

void writeIndent() {
assert(_indent >= 0);
_buf.append(static_cast<size_t>(_indent * _indentSpaces), ' ');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] 若 _indent 因 close 调用不平衡而变为负数,_indent * _indentSpaces 为负数,static_cast<size_t>(negative) 回绕为极大正数(~18EB),导致 _buf.append 抛出 std::bad_alloc 或 OOM 崩溃。Release 构建中前面的 assert 无效。建议改为:if (_indent < 0) return;

return;
}

auto* mutableText = const_cast<Text*>(text);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] const_cast<Text*>(text) 存在未定义行为风险。若 Text 对象是 const 构造的(如来自只读内存映射),TextLayout::Layout 内部的写操作将触发 UB。此处及第 660、690 行同理。根本原因是 TextLayout::Layout 接口要求非 const 指针。建议修改 TextLayout::Layout 接口使其接受 const 输入,或确保调用路径中 Text 始终非 const。

}

if (!ok) {
zipClose(zf, nullptr);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] ZIP 写入失败时 zipClose 后直接 return false,但此时文件已被 zipOpen(APPEND_STATUS_CREATE) 创建在磁盘上,遗留一个不完整的损坏 PPTX 文件。建议在 return false 前添加 std::remove(filePath.c_str()) 清理残留文件。

Comment thread src/pagx/ppt/PPTWriter.h

PPTWriterContext* _ctx = nullptr;
PAGXDocument* _doc = nullptr;
bool _convertTextToPath = true;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Design] _convertTextToPath 成员默认值为 true,但 PPTExportOptions::convertTextToPath 默认值为 false。虽然构造函数会正确覆盖,但声明处的默认值容易误导读者。建议改为 bool _convertTextToPath = false; 与 Options 保持一致。


// ── Transform decomposition ────────────────────────────────────────────────

PPTWriter::Xform PPTWriter::decomposeXform(float localX, float localY, float localW, float localH,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] 命名规范:decomposeXform 是 static 类方法(PPTWriter.h:771),按项目规范静态方法应大写开头,建议改为 DecomposeXform。同类中 WriteXfrmWriteBlip 已正确使用大写。

Comment thread src/pagx/ppt/PPTWriter.h
// Shared contour-to-custGeom emitter used by writePath and writeTextAsPath
// for the non-bridged or single-group case (when callers haven't already
// prepared per-group emission themselves).
void WriteContourGeom(XMLBuilder& out, std::vector<PathContour>& contours, int64_t pathWidth,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] 命名规范:WriteContourGeom 是非静态成员方法(内部访问 _bridgeContours 成员),按项目规范非静态成员方法应小写开头,建议改为 writeContourGeom

* elements when glyph outline data is unavailable. The default value is true.
* elements when glyph outline data is unavailable. The default value is false.
*/
bool convertTextToPath = false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] convertTextToPath 默认值从 true 改为 false 是用户可感知的 Breaking Change。所有通过 C++ API 调用 SVGExporter::ToFile()/ToSVG() 且未显式设置此选项的下游代码,输出将从 path 元素变为 text 元素。建议在 PR 描述或 CHANGELOG 中明确标注此变更。

return geom;
}

void PPTWriter::emitNativeTextShapeFrame(XMLBuilder& out, const Matrix& m,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] emitNativeTextShapeFrame(此处)与 emitTextBoxShapeFrame(第 520 行)构建的 XML 框架高度相似(p:sp > p:nvSpPr > p:cNvPr + p:cNvSpPr + p:nvPr > p:spPr > a:xfrm + a:prstGeom + a:noFill),差异仅在 a:bodyPr 属性。建议提取公共的 emitTextShapeEnvelope 方法减少重复。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants