ppt-master 的 Mask 与 Gradient 保真冒烟指南:SVG 校验、DrawingML 映射与背景提升全链路验证
【免费下载链接】ppt-masterAI turns documents or topics into real, native PowerPoint decks—with native shapes, transitions and animations,>项目地址: https://gitcode.com/GitHub_Trending/ppt/ppt-master
本文基于 ppt-master 仓库中的手动冒烟文档 mask-gradient-smoke.md,系统讲解如何在不引入测试框架、XML 全程留在内存的前提下,一次性验证“渐变校验、渐变导入/导出、原生背景提升、图标展开、mask 拒绝”五条链路的正确性。读完并动手跑通这份冒烟脚本后,你将能够理解 ppt-master 从 SVG 侧到 DrawingML(OOXML)侧的渐变几何契约——包括 1/60000 度角度单位、单位盒投影坐标、规范圆焦点约束——以及 mask 在 Checker 与原生导出两条路径上被一致拒绝的实现细节。
为什么需要这份冒烟测试
ppt-master 的核心能力是把 SVG 页面转换为真实的原生 PowerPoint 对象:形状、渐变、图表、表格都必须落到 DrawingML XML 上,而不是退化成图片。其中渐变(<a:gradFill>)与 mask 的互转是最容易出偏差的环节:
- SVG → PPTX 方向:
<linearGradient>/<radialGradient>必须被 build_gradient_fill 编译成合法的<a:lin>/<a:path path="circle">结构; - PPTX → SVG 方向:resolve_fill 必须能把
<a:gradFill>反向还原为受项目约束的 SVG 渐变定义; - 校验层:project_gradient_errors 与 project_mask_errors 是所有共享验证器,Checker(质量检查)与直接导出都必须复用它们。
因此文档规定:每当修改了渐变校验、渐变导入/导出、原生背景提升、图标展开或 mask 拒绝逻辑中的任意一项,就应从仓库根目录运行这份手动冒烟。它的刻意设计是:XML 全部保留在内存中,只有少量 SVG fixture 写入临时目录,并且明确警告“不要把它变成测试框架或示例幻灯片”——它只是一根随取随用的“探针”。
运行方式:完整冒烟脚本
以下命令必须从仓库根目录执行(脚本内部会自行解析skills/ppt-master/scripts并插入sys.path)。脚本末尾打印Mask and gradient smoke: passed即表示全部断言通过:
python3 - <<'PY' import math import re import sys import tempfile from pathlib import Path from xml.etree import ElementTree as ET scripts = Path("skills/ppt-master/scripts").resolve() sys.path.insert(0, str(scripts)) from pptx_to_svg.color_resolver import ColorPalette from pptx_to_svg.fill_to_svg import _angle_to_unit_endpoints, resolve_fill from svg_quality_checker import SVGQualityChecker from svg_to_pptx.drawingml.converter import ( SvgNativeConversionError, convert_svg_to_slide_shapes, ) from svg_to_pptx.drawingml.styles import build_gradient_fill from svg_to_pptx.drawingml.utils import ( parse_project_linear_gradient_coordinate, project_gradient_errors, project_mask_errors, ) SVG_NS = "http://www.w3.org/2000/svg" def svg(fragment): return ET.fromstring( f'<svg xmlns="{SVG_NS}" viewBox="0 0 1280 720" ' f'data-pptx-page-role="content">{fragment}</svg>' ) valid = svg( """<defs> <linearGradient id="linear"> <stop offset="0" stop-color="#2563EB"/> <stop offset="100%" stop-color="#F97316" stop-opacity="0.4"/> </linearGradient> <radialGradient id="radial" cx="0.25" cy="0.7" r="0.8"> <stop offset="0" stop-color="#FFFFFF"/> <stop offset="1" stop-color="#0F172A"/> </radialGradient> </defs> <rect id="bg" x="0" y="0" width="1280" height="720" fill="url(#linear)"/>""" ) assert not project_gradient_errors(valid) linear, radial = list(valid.find(f"{{{SVG_NS}}}defs")) linear_xml = build_gradient_fill(linear) radial_xml = build_gradient_fill(radial) assert '<a:lin ang="0" scaled="1"/>' in linear_xml assert '<a:alpha val="40000"/>' in linear_xml assert '<a:path path="circle">' in radial_xml assert ( '<a:fillToRect l="25000" t="70000" r="75000" b="30000"/>' in radial_xml ) radial.set("fx", "0.8") radial.set("fy", "0.2") focused_xml = build_gradient_fill(radial) assert ( '<a:fillToRect l="80000" t="20000" r="20000" b="80000"/>' in focused_xml ) native_gradient = ET.fromstring( f'<root xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">' f'{focused_xml}</root>' )[0] restored = resolve_fill(native_gradient, None) assert 'cx="0.5" cy="0.5" r="0.5" fx="0.8" fy="0.2"' in restored.defs[0] assert '<a:fillToRect l="80000" t="20000" r="20000" b="80000"/>' in ( build_gradient_fill(ET.fromstring(restored.defs[0])) ) radial.set("fx", "0") radial.set("fy", "0") outside_focus_errors = project_gradient_errors(valid) assert any( "must lie within the canonical circle" in error for error in outside_focus_errors ), outside_focus_errors try: build_gradient_fill(radial) except ValueError as exc: assert "must lie within the canonical circle" in str(exc) else: raise AssertionError("outside radial focus reached DrawingML") radial.set("fx", "0.8") radial.set("fy", "0.2") diagnostics = [] palette = ColorPalette( None, None, strict=False, diagnostic_sink=lambda code, message, fallback: diagnostics.append( (code, message, fallback) ), ) outside_native_xml = focused_xml.replace( 'l="80000" t="20000" r="20000" b="80000"', 'l="0" t="0" r="100000" b="100000"', ) outside_native_gradient = ET.fromstring( f'<root xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">' f"{outside_native_xml}</root>" )[0] normalized = resolve_fill(outside_native_gradient, palette) assert " fx=" not in normalized.defs[0] assert " fy=" not in normalized.defs[0] assert any( code == "path-gradient-focus-normalized" for code, _message, _fallback in diagnostics ) with tempfile.TemporaryDirectory(prefix="ppt-master-gradient-smoke-") as tmp: source = Path(tmp) / "gradient.svg" source.write_text( ET.tostring(valid, encoding="unicode"), encoding="utf-8", ) trace = [] slide_xml, *_ = convert_svg_to_slide_shapes(source, trace_out=trace) assert slide_xml.count("<p:bg>") == 1 assert "<a:gradFill>" in slide_xml assert trace[0]["summary"]["promoted_backgrounds"] == 1 assert any( event.get("decision") == "native-background" for event in trace[0]["events"] ) invalid_gradients = [ ( """<linearGradient id="single"> <stop offset="0" stop-color="#2563EB"/> </linearGradient>""", "requires at least two direct <stop> children", ), ( """<linearGradient id="descending"> <stop offset="1" stop-color="#2563EB"/> <stop offset="0" stop-color="#F97316"/> </linearGradient>""", "offsets must be non-decreasing", ), ( """<linearGradient id="zero" x1="0.5" y1="0.5" x2="0.5" y2="0.5"> <stop offset="0" stop-color="#2563EB"/> <stop offset="1" stop-color="#F97316"/> </linearGradient>""", "linear gradient axis must not collapse to one point", ), ] for definition, expected in invalid_gradients: errors = project_gradient_errors(svg(f"<defs>{definition}</defs>")) assert any(expected in error for error in errors), errors mask_cases = [ """<defs><mask id="fade"><rect width="1" height="1"/></mask></defs>""", """<rect width="100" height="100" mask="url(#fade)"/>""", """<rect width="100" height="100" style="mask: url(#fade)"/>""", ] for fragment in mask_cases: errors = project_mask_errors(svg(fragment)) assert any("unsupported SVG mask" in error for error in errors), errors checker = SVGQualityChecker() with tempfile.TemporaryDirectory(prefix="ppt-master-mask-smoke-") as tmp: for index, fragment in enumerate(mask_cases, start=1): source = Path(tmp) / f"mask-{index}.svg" source.write_text( ET.tostring(svg(fragment), encoding="unicode"), encoding="utf-8", ) checked = checker.check_file(str(source)) assert any("mask" in error.lower() for error in checked["errors"]) try: convert_svg_to_slide_shapes(source) except SvgNativeConversionError as exc: assert "invalid project mask" in str(exc) else: raise AssertionError(f"native export accepted {source.name}") with tempfile.TemporaryDirectory(prefix="ppt-master-icon-mask-smoke-") as tmp: project = Path(tmp) icon_dir = project / "icons" / "imported" icon_dir.mkdir(parents=True) (icon_dir / "masked.svg").write_text( f"""<svg xmlns="{SVG_NS}" viewBox="0 0 24 24"> <defs> <mask id="fade"> <rect x="0" y="0" width="24" height="24" fill="#FFFFFF"/> </mask> </defs> <rect x="0" y="0" width="24" height="24" fill="#000000" mask="url(#fade)"/> </svg>""", encoding="utf-8", ) source = project / "icon-mask.svg" source.write_text( ET.tostring( svg( """<use contenteditable="false">【免费下载链接】ppt-masterAI turns documents or topics into real, native PowerPoint decks—with native shapes, transitions and animations,>
项目地址: https://gitcode.com/GitHub_Trending/ppt/ppt-master创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考