Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
316ae23
Align PAGX group skew sign across SVG PPT and HTML exporters with nat…
OnionsYu Jun 4, 2026
3a14721
Embed vector fonts as WOFF2 in SVG export and share generator with HTML.
OnionsYu Jun 5, 2026
a7cdaa4
Remove the now-relocated WOFF2 font generator from pagx/html.
OnionsYu Jun 5, 2026
4a6082b
Merge remote-tracking branch 'origin/main' into feature/onionsyu_font…
OnionsYu Jun 5, 2026
89c1dad
Add tests for SVG export embedFontsAsWoff2 option covering @font-face…
OnionsYu Jun 5, 2026
9542a1c
Fix PPT export Group isolation so Painters outside the Group no longe…
OnionsYu Jun 5, 2026
22acd61
Distribute continuous TrimPath across shapes per-path so SVG gradient…
OnionsYu Jun 5, 2026
d4005fe
Merge remote-tracking branch 'origin/main' into feature/onionsyu_font…
OnionsYu Jun 5, 2026
8a28f26
Reduce repetitive style and matrix string concatenation in SVG export…
OnionsYu Jun 5, 2026
61f76f5
Cover SVG path parser, feature probe, and modifier resolver edge bran…
OnionsYu Jun 5, 2026
63e8602
Apply foreground placement in SVG export so painters overlay child la…
OnionsYu Jun 8, 2026
ab39cd0
Apply foreground placement in PPTX export so painters overlay child l…
OnionsYu Jun 8, 2026
f144748
Bake renderPosition and renderScale into SVG path data so userSpaceOn…
OnionsYu Jun 8, 2026
48189f2
Split mask/clip-path onto an outer SVG group without the layer transf…
OnionsYu Jun 8, 2026
f52858a
Address PR review comments on foreground placement, WOFF2 embedding, …
OnionsYu Jun 11, 2026
662152a
Merge remote-tracking branch 'origin/main' into feature/onionsyu_font…
OnionsYu Jun 11, 2026
bdc69ee
Merge branch 'main' into feature/onionsyu_font_woff
OnionsYu Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ if (PAG_BUILD_PAGX)
file(GLOB_RECURSE HTML_EXPORTER_SOURCES CONFIGURE_DEPENDS src/pagx/html/*.*)
list(APPEND PAG_FILES ${HTML_EXPORTER_SOURCES})

# woff2 encoder + brotli (for embedded font → WOFF2 conversion in HTML exporter)
# woff2 encoder + brotli (for embedded font → WOFF2 conversion in HTML/SVG exporters)
set(WOFF2_DIR third_party/woff2)
set(BROTLI_DIR ${WOFF2_DIR}/brotli)
set(WOFF2_SOURCES
Expand Down
13 changes: 13 additions & 0 deletions include/pagx/SVGExporter.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ struct SVGExportOptions {
* default of HTMLExportOptions::rasterScale.
*/
float rasterScale = 2.0f;

/**
* Whether to embed vector Font resources as WOFF2 @font-face rules with base64 data URIs and
* render their Text elements via <text> with PUA Unicode characters. When enabled, Text
* nodes whose GlyphRun references an embeddable vector Font become real <text> elements —
* selectable, searchable, and animatable per character — instead of opaque outline <path>
* elements. Bitmap (CBDT) fonts and GlyphRuns that carry per-glyph scales / skews remain on the
* outline path because plain SVG <text> cannot express them. When disabled, every Text
* with GlyphRun data is emitted as <path> (the legacy behaviour). Has no effect when
* `convertTextToPath` is true (the user has explicitly requested outline geometry). The default
* value is true.
*/
bool embedFontsAsWoff2 = true;
};

/**
Expand Down
20 changes: 3 additions & 17 deletions src/pagx/html/FontSignature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,23 +105,9 @@ FontSignature CollectUniformSignature(const std::vector<Element*>& contents) {
return sig;
}

std::string EscapeCssFontFamily(const std::string& family) {
std::string out;
out.reserve(family.size());
for (char c : family) {
// Drop control characters (including NUL, \n, \r, \t) and the characters that would let an
// attacker escape the single-quoted font-family value into the surrounding CSS context.
auto uc = static_cast<unsigned char>(c);
if (uc < 0x20 || c == ';' || c == '}' || c == '{' || c == '<' || c == '>') {
continue;
}
if (c == '\\' || c == '\'') {
out += '\\';
}
out += c;
}
return out;
}
// EscapeCssFontFamily moved to pagx/utils/ExporterUtils.h so SVGExporter can share the same
// implementation. The declaration in FontSignature.h re-exports the symbol from the utils
// header for backwards compatibility with html/* call sites.

std::string FontSignatureToCss(const FontSignature& sig) {
std::string css;
Expand Down
10 changes: 6 additions & 4 deletions src/pagx/html/FontSignature.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ std::string FontSignatureToCss(const FontSignature& sig);

/**
* Escapes a font-family value for safe emission inside a CSS declaration wrapped in single
* quotes. Backslash-escapes single quotes and backslashes, and strips characters that would
* allow escaping the CSS context (';', '}', '<', '>') or control characters. The returned
* string is intended to be used as the content between single quotes in
* "font-family:'<escaped>'". Empty input yields an empty output.
* quotes. Implementation lives in pagx/utils/ExporterUtils.h so the SVG exporter shares the
* same definition; the declaration is re-exported here for the html call sites that already
* include FontSignature.h. Backslash-escapes single quotes and backslashes, and strips
* characters that would allow escaping the CSS context (';', '}', '<', '>') or control
* characters. The returned string is intended to be used as the content between single quotes
* in "font-family:'<escaped>'". Empty input yields an empty output.
*/
std::string EscapeCssFontFamily(const std::string& family);

Expand Down
2 changes: 1 addition & 1 deletion src/pagx/html/HTMLExporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
#include "pagx/html/HTMLBuilder.h"
#include "pagx/html/HTMLStyleExtractor.h"
#include "pagx/html/HTMLWriter.h"
#include "pagx/html/Woff2FontGenerator.h"
#include "pagx/nodes/Font.h"
#include "pagx/utils/StringParser.h"
#include "pagx/utils/Woff2FontGenerator.h"

namespace pagx {

Expand Down
13 changes: 1 addition & 12 deletions src/pagx/html/HTMLWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
#include "pagx/html/FontSignature.h"
#include "pagx/html/HTMLBuilder.h"
#include "pagx/html/HTMLPlusDarkerRenderer.h"
#include "pagx/html/Woff2FontGenerator.h"
#include "pagx/nodes/ColorSource.h"
#include "pagx/nodes/ColorStop.h"
#include "pagx/nodes/Composition.h"
Expand All @@ -47,6 +46,7 @@
#include "pagx/types/Padding.h"
#include "pagx/types/Rect.h"
#include "pagx/types/SelectorTypes.h"
#include "pagx/utils/Woff2FontGenerator.h"

namespace pagx {

Expand Down Expand Up @@ -84,17 +84,6 @@ const char* BlendModeToMixBlendMode(BlendMode mode);

std::string LayerTransformCSS(const Layer* layer);

/**
* HTML-local wrapper around pagx::BuildGroupMatrix that negates the `group->skew` angle so the
* resulting shear matches tgfx native rendering (VectorGroup::ApplySkew uses
* `DegreesToRadians(-skew)`). The shared pagx::BuildGroupMatrix follows the SVG matrix sign
* convention asserted by main's PAGXSVGTest.SVGExport_GroupSkew, so we cannot fix the sign at
* that layer without breaking the SVG / PPT exporters and their pinned test expectations. Use
* this wrapper everywhere the HTML exporter would have called BuildGroupMatrix on a Group node
* (path bake in flattenGroup, transform emission in writeGroup, etc.).
*/
Matrix BuildGroupMatrixForHTML(const Group* group);

const char* AlignmentToCSS(Alignment alignment);
const char* ArrangementToCSS(Arrangement arrangement);
std::string PaddingToCSS(const Padding& padding);
Expand Down
21 changes: 6 additions & 15 deletions src/pagx/html/HTMLWriterGroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "pagx/nodes/Group.h"
#include "pagx/nodes/Repeater.h"
#include "pagx/nodes/Stroke.h"
#include "pagx/utils/ExporterUtils.h"
#include "pagx/utils/StringParser.h"

namespace pagx {
Expand All @@ -38,7 +39,7 @@ void HTMLWriter::writeGroup(HTMLBuilder& out, const Group* group, float alpha, b
if (guard.overflowed()) {
return;
}
Matrix gm = BuildGroupMatrixForHTML(group);
Matrix gm = BuildGroupMatrix(group);
if (!parentMatrix.isIdentity()) {
gm = parentMatrix * gm;
}
Expand All @@ -57,20 +58,10 @@ void HTMLWriter::writeGroup(HTMLBuilder& out, const Group* group, float alpha, b
out.closeTagStart();
const Padding* groupPadding = group->padding.isZero() ? nullptr : &group->padding;
writeElements(out, group->elements, 1.0f, false, LayerPlacement::Background, groupPadding);
bool hasForegroundPainter = false;
for (auto* e : group->elements) {
if (e->nodeType() == NodeType::Fill) {
if (static_cast<const Fill*>(e)->placement == LayerPlacement::Foreground) {
hasForegroundPainter = true;
break;
}
} else if (e->nodeType() == NodeType::Stroke) {
if (static_cast<const Stroke*>(e)->placement == LayerPlacement::Foreground) {
hasForegroundPainter = true;
break;
}
}
}
// Recurse through nested Group / TextBox containers so a foreground painter inside a child
// Group still triggers the second pass — the shallow flat-list scan would otherwise leave it
// suppressed by both passes' filters.
bool hasForegroundPainter = HasForegroundPainter(group->elements);
if (hasForegroundPainter) {
writeElements(out, group->elements, 1.0f, false, LayerPlacement::Foreground, groupPadding);
}
Expand Down
23 changes: 7 additions & 16 deletions src/pagx/html/HTMLWriterLayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#include "pagx/nodes/TrimPath.h"
#include "pagx/svg/SVGPathParser.h"
#include "pagx/types/MergePathMode.h"
#include "pagx/utils/ExporterUtils.h"
#include "pagx/utils/StringParser.h"

namespace pagx {
Expand Down Expand Up @@ -521,7 +522,7 @@ void HTMLWriter::writeElements(HTMLBuilder& out, const std::vector<Element*>& el
// reads Text::renderPosition relative to the Group — not the enclosing Layer — so a
// flattened Group would drop its constraint offset from the span's top/left and the
// text would collapse onto the Layer's origin (app_icons Calendar "17" symptom).
bool groupHasTransform = !BuildGroupMatrixForHTML(group).isIdentity();
bool groupHasTransform = !BuildGroupMatrix(group).isIdentity();
// Only use the DOM wrapper (writeGroup) when the Group has a transform AND contains
// Text — the wrapper is needed so Text can resolve its renderPosition in Group space.
// Groups with alpha but no transform/text must use the flatten path so their geometry
Expand Down Expand Up @@ -650,7 +651,7 @@ void HTMLWriter::flattenGroup(HTMLBuilder& out, const Group* group, float alpha,
const TextBox* curTextBox, ElementDispatchState& state) {
ElementDispatchStateGuard stateGuard(state);
std::vector<GeoInfo> groupGeos;
Matrix gm = BuildGroupMatrixForHTML(group);
Matrix gm = BuildGroupMatrix(group);
// When a Group has alpha < 1, its Painters render with that alpha applied. In the
// flatten path, carry the group's alpha into every paintGeos/writeTextPath/
// writeTextModifier call so the fill-opacity matches the tgfx compositing result.
Expand Down Expand Up @@ -2476,20 +2477,10 @@ void HTMLWriter::writeLayerInner(HTMLBuilder& out, const Layer* layer, float con
float childOffX = _ctx->savedChildLayerOffsetX;
float childOffY = _ctx->savedChildLayerOffsetY;

bool hasForeground = false;
for (auto* e : layer->contents) {
if (e->nodeType() == NodeType::Fill) {
if (static_cast<const Fill*>(e)->placement == LayerPlacement::Foreground) {
hasForeground = true;
break;
}
} else if (e->nodeType() == NodeType::Stroke) {
if (static_cast<const Stroke*>(e)->placement == LayerPlacement::Foreground) {
hasForeground = true;
break;
}
}
}
// Recurses through Group / TextBox containers so a foreground painter buried inside a nested
// Group still triggers the second pass — the shallow flat-list scan would otherwise leave it
// suppressed by both passes' filters.
bool hasForeground = HasForegroundPainter(layer->contents);

writeLayerContents(out, layer, contentAlpha, childDistribute, LayerPlacement::Background);

Expand Down
46 changes: 0 additions & 46 deletions src/pagx/html/HTMLWriterUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -466,52 +466,6 @@ std::string LayerTransformCSS(const Layer* layer) {
return MatrixTransformToCSS(m);
}

// HTML-local skew sign fix. Mirrors pagx::BuildGroupMatrix line-for-line except for one shear:
// the shear coefficient uses `tan(-group->skew)` so the result agrees with tgfx native rendering
// (VectorGroup::ApplySkew passes `DegreesToRadians(-skew)` into the shear). The shared
// pagx::BuildGroupMatrix uses the +skew sign because main's PAGXSVGTest.SVGExport_GroupSkew
// pinned that convention for SVG output, and the SVG/PPT exporters depend on it. Rather than
// flipping the shared helper (which would break those exporters and the pinned test) we keep
// the HTML exporter's path bake aligned with native by routing every BuildGroupMatrix call
// through this wrapper. Any change to the rest of BuildGroupMatrix must be mirrored here.
Matrix BuildGroupMatrixForHTML(const Group* group) {
auto renderPos = group->renderPosition();
bool hasAnchor = !FloatNearlyZero(group->anchor.x) || !FloatNearlyZero(group->anchor.y);
bool hasPosition = !FloatNearlyZero(renderPos.x) || !FloatNearlyZero(renderPos.y);
bool hasRotation = !FloatNearlyZero(group->rotation);
bool hasScale =
!FloatNearlyZero(group->scale.x - 1.0f) || !FloatNearlyZero(group->scale.y - 1.0f);
bool hasSkew = !FloatNearlyZero(group->skew);

if (!hasAnchor && !hasPosition && !hasRotation && !hasScale && !hasSkew) {
return {};
}

Matrix m = {};
if (hasAnchor) {
m = Matrix::Translate(-group->anchor.x, -group->anchor.y);
}
if (hasScale) {
m = Matrix::Scale(group->scale.x, group->scale.y) * m;
}
if (hasSkew) {
m = Matrix::Rotate(group->skewAxis) * m;
Matrix shear = {};
// Sign deliberately negated relative to pagx::BuildGroupMatrix; see function comment.
shear.c = std::tan(DegreesToRadians(-group->skew));
m = shear * m;
m = Matrix::Rotate(-group->skewAxis) * m;
}
if (hasRotation) {
m = Matrix::Rotate(group->rotation) * m;
}
if (hasPosition) {
m = Matrix::Translate(renderPos.x, renderPos.y) * m;
}

return m;
}

//==============================================================================
// Text & Font
//==============================================================================
Expand Down
Loading
Loading