Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions resources/cli/import_resolve_skew_siblings.pagx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<pagx width="40" height="40">
<Layer centerX="0" centerY="0">
<svg viewBox="0 0 20 20">
<path d="M2 2H8V8H2Z" fill="#FF4757" transform="scale(0.01) skewX(20)"/>
<path d="M10 10H18V18H10Z" fill="#3498DB"/>
</svg>
</Layer>
</pagx>
9 changes: 9 additions & 0 deletions resources/cli/import_resolve_transformed_siblings.pagx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<pagx width="40" height="40">
<Layer centerX="0" centerY="0">
<svg viewBox="0 0 20 20">
<path d="M0 0H4V4H0Z" fill="#FF4757"
transform="translate(4 5) rotate(30) scale(2 3)"/>
<path d="M10 10H18V18H10Z" fill="#3498DB"/>
</svg>
</Layer>
</pagx>
86 changes: 42 additions & 44 deletions src/cli/CommandResolve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
#include "pagx/PAGXOptimizer.h"
#include "pagx/nodes/Composition.h"
#include "pagx/nodes/Group.h"
#include "pagx/nodes/LayoutNode.h"

namespace pagx::cli {

Expand Down Expand Up @@ -125,6 +124,20 @@ static int ParseResolveOptions(int argc, char* argv[], ResolveOptions* options)
// Resolve logic
//--------------------------------------------------------------------------------------------------

// A Group's position/scale/rotation fields can represent a matrix whose axes remain orthogonal.
// Compare the normalized axis dot product so the result does not depend on the matrix scale.
// Degenerate axes are kept as Layers because the decomposition below cannot recover their rotation.
static bool CanDowngradeMatrixToGroup(const Matrix& matrix) {
float xLength = std::hypot(matrix.a, matrix.b);
float yLength = std::hypot(matrix.c, matrix.d);
if (pag::FloatNearlyZero(xLength) || pag::FloatNearlyZero(yLength)) {
return false;
}
float normalizedDot =
(matrix.a / xLength) * (matrix.c / yLength) + (matrix.b / xLength) * (matrix.d / yLength);
return pag::FloatNearlyZero(normalizedDot);
}

static bool ResolveOneLayer(Layer* layer, const std::string& baseDir,
const ImportFormatOptions& formatOptions, PAGXDocument* doc) {
bool hasImportSource = !layer->importDirective.source.empty();
Expand Down Expand Up @@ -209,7 +222,8 @@ static bool ResolveOneLayer(Layer* layer, const std::string& baseDir,
} else if (elementLayers.size() > 1) {
canDowngradeAll = true;
for (auto* el : elementLayers) {
if (!el->children.empty() || HasLayerOnlyFeatures(el)) {
if (!el->children.empty() || HasLayerOnlyFeatures(el) ||
!CanDowngradeMatrixToGroup(el->matrix)) {
canDowngradeAll = false;
break;
}
Expand All @@ -221,52 +235,36 @@ static bool ResolveOneLayer(Layer* layer, const std::string& baseDir,
layer->contents.push_back(element);
}
} else if (canDowngradeAll) {
for (size_t i = 0; i < elementLayers.size(); i++) {
auto* elemLayer = elementLayers[i];
bool unpackFirst = false;
if (i == 0 && elemLayer->matrix.isIdentity()) {
unpackFirst = true;
for (auto* child : elemLayer->contents) {
auto* layoutNode = LayoutNode::AsLayoutNode(child);
if (layoutNode != nullptr &&
(!std::isnan(layoutNode->right) || !std::isnan(layoutNode->bottom) ||
!std::isnan(layoutNode->centerX) || !std::isnan(layoutNode->centerY))) {
unpackFirst = false;
break;
}
}
// Wrap every element layer uniformly in its own Group so sibling shapes stay at the same
// depth (peer Groups), preserving the source hierarchy. Flattening only the first layer's
// contents while wrapping the rest would break that symmetry and is unnecessary — a trailing
// painter is isolated by its enclosing Group either way. Structurally, this uses the same
// all-siblings Group strategy as PAGXOptimizer::DowngradeShellChildren, while also supporting
// transformable non-identity matrices here.
for (auto* elemLayer : elementLayers) {
if (elemLayer->contents.empty() && elemLayer->customData.empty()) {
continue;
}
if (unpackFirst) {
for (auto* element : elemLayer->contents) {
layer->contents.push_back(element);
}
} else {
auto m = elemLayer->matrix;
bool hasSkew = !pag::FloatNearlyEqual(m.a * m.c + m.b * m.d, 0.0f);
if (hasSkew) {
// Matrix contains skew which cannot be represented by Group's
// position/scale/rotation. Keep it as a Layer instead of downgrading.
layer->children.push_back(elemLayer);
continue;
}
auto* group = doc->makeNode<Group>();
group->elements = std::move(elemLayer->contents);
if (!elemLayer->matrix.isIdentity()) {
group->position = {m.tx, m.ty};
if (m.a != 1 || m.b != 0 || m.c != 0 || m.d != 1) {
float sx = std::sqrt(m.a * m.a + m.b * m.b);
float sy = std::sqrt(m.c * m.c + m.d * m.d);
float det = m.a * m.d - m.b * m.c;
if (det < 0) {
sy = -sy;
}
float rot = pag::RadiansToDegrees(std::atan2(m.b, m.a));
group->scale = {sx, sy};
group->rotation = rot;
auto m = elemLayer->matrix;
auto* group = doc->makeNode<Group>();
Comment thread
OnionsYu marked this conversation as resolved.
group->elements = std::move(elemLayer->contents);
group->customData = std::move(elemLayer->customData);
group->sourceLine = elemLayer->sourceLine;
if (!elemLayer->matrix.isIdentity()) {
group->position = {m.tx, m.ty};
if (m.a != 1 || m.b != 0 || m.c != 0 || m.d != 1) {
float sx = std::sqrt(m.a * m.a + m.b * m.b);
float sy = std::sqrt(m.c * m.c + m.d * m.d);
float det = m.a * m.d - m.b * m.c;
if (det < 0) {
sy = -sy;
}
float rot = pag::RadiansToDegrees(std::atan2(m.b, m.a));
group->scale = {sx, sy};
group->rotation = rot;
}
layer->contents.push_back(group);
}
layer->contents.push_back(group);
}
} else {
for (auto* elemLayer : elementLayers) {
Expand Down
17 changes: 9 additions & 8 deletions src/pagx/html/importer/HTMLBoxAttributes.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ static constexpr const char* HTML_DEFAULT_FONT_FAMILY = "Arial";

/**
* Default font style/variant name written to every imported `Text` node. The synthesis in
* `ResolveFontStyleSynthesis` leaves the style label empty for the plain base face (bold / italic
* are carried by `fauxBold` / `fauxItalic`); this constant substitutes the canonical "Regular"
* `ResolveFontStyleSynthesis` leaves the style label empty for the plain Regular-weight upright
* face (italic is carried by `fauxItalic`); this constant substitutes the canonical "Regular"
* name so every HTML-imported `Text` node always carries a concrete `fontStyle`.
*/
static constexpr const char* HTML_DEFAULT_FONT_STYLE = "Regular";
Expand Down Expand Up @@ -74,12 +74,13 @@ struct HTMLInheritedStyle {
std::string fontSize = {};
std::string fontWeight = {};
std::string fontStyle = {};
std::string fontStyleName = {}; // real-face style label, e.g. "Light" / "Medium" / ""
// Synthetic weight / slant the renderer must emboss on top of the resolved face. Set by
// `resolveInheritedStyle` for bold (CSS weight >= 600) and italic/oblique requests whose axis is
// dropped from `fontStyleName` (see `ResolveFontStyleSynthesis`). Carried through to
// `Text::fauxBold` / `Text::fauxItalic` so authored weight / slant survives even when the styled
// web face is not installed on the render host.
std::string fontStyleName = {}; // real-face style label, e.g. "Light" / "Bold" / "Black" / ""
// Synthetic slant the renderer must emboss on top of the resolved face. Set by
// `resolveInheritedStyle` for italic/oblique requests, whose axis is dropped from `fontStyleName`
// (see `ResolveFontStyleSynthesis`) and carried through to `Text::fauxItalic` so the authored
// slant survives even when the styled italic face is not installed on the render host. The weight
// axis is never synthesised (it stays in `fontStyleName` as a real-face keyword), so `fauxBold`
// is always false here.
bool fauxBold = false;
bool fauxItalic = false;
std::string letterSpacing = {};
Expand Down
16 changes: 7 additions & 9 deletions src/pagx/html/importer/HTMLElementEmitter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -697,8 +697,8 @@ void HTMLParserContext::applyMaskOrClip(Layer* layer, const HTMLBoxAttributes& b
float intrinsicH = svgDoc->height;

// The mask SVG produces one or more content layers (a single drawn shape in the common case).
// Wrap them under one invisible, layout-excluded mask layer so a multi-shape clip-path also
// works, then transplant every node from the temporary SVG document into ours.
// Wrap them under one layout-excluded mask layer so a multi-shape clip-path also works, then
// transplant every node from the temporary SVG document into ours.
Layer* maskLayer = nullptr;
if (svgDoc->layers.size() == 1) {
maskLayer = svgDoc->layers[0];
Expand All @@ -708,7 +708,6 @@ void HTMLParserContext::applyMaskOrClip(Layer* layer, const HTMLBoxAttributes& b
maskLayer->children.push_back(l);
}
}
maskLayer->visible = false;
maskLayer->includeInLayout = false;
// CSS `mask-size` / `mask-position` scale and offset the intrinsic mask box onto the masked
// element; replay that transform onto the mask layer. Contour clip-paths are framed to the box
Expand All @@ -730,9 +729,9 @@ void HTMLParserContext::applyMaskOrClip(Layer* layer, const HTMLBoxAttributes& b
layer->mask = maskLayer;
layer->maskType = maskType;
// The mask layer must be reachable in the display list for the renderer's mask lookup, and must
// share the masked layer's local coordinate origin. Adding it as an invisible, layout-excluded
// child satisfies both: it is walked by LayerBuilder but neither drawn (maskOwner is set) nor
// laid out (includeInLayout is false).
// share the masked layer's local coordinate origin. Adding it as a layout-excluded child
// satisfies both: it is walked by LayerBuilder but neither drawn (maskOwner is set on the tgfx
// side) nor laid out (includeInLayout is false).
layer->children.push_back(maskLayer);
}

Expand Down Expand Up @@ -767,7 +766,6 @@ void HTMLParserContext::applyRoundedOverflowClip(Layer* layer, const HTMLBoxAttr
// resolved by layout. A Contour mask reads only the shape's coverage, clipping descendants to
// the rounded outline instead of the layer's rectangle.
auto* maskLayer = _document->makeNode<Layer>();
maskLayer->visible = false;
maskLayer->includeInLayout = false;
maskLayer->percentWidth = 100.0f;
maskLayer->percentHeight = 100.0f;
Expand All @@ -782,8 +780,8 @@ void HTMLParserContext::applyRoundedOverflowClip(Layer* layer, const HTMLBoxAttr
// The rounded mask now performs the clip; drop the rectangular scrollRect so it does not also
// square off the corners the mask just rounded.
layer->clipToBounds = false;
// Invisible, layout-excluded child so it shares the masked layer's local coordinate origin and
// stays reachable by the renderer's mask lookup (mirrors `applyMaskOrClip`).
// Layout-excluded child so it shares the masked layer's local coordinate origin and stays
// reachable by the renderer's mask lookup (mirrors `applyMaskOrClip`).
layer->children.push_back(maskLayer);
}

Expand Down
2 changes: 1 addition & 1 deletion src/pagx/html/importer/HTMLParserContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class HTMLParserContext {
// equivalent SVG geometry — and attaches it to `layer` as `layer->mask` / `maskType`
// (the inverse of `HTMLWriter::writeMaskCSS` / `writeClipDef`). The mask geometry SVG is parsed
// through `SVGImporter`, and its nodes are transplanted into `_document`. The mask layer is added
// as an invisible, layout-excluded child of `layer` so it shares the masked layer's local
// as a layout-excluded child of `layer` so it shares the masked layer's local
// coordinate space and is reachable by the renderer's mask lookup. No-op when the box carries
// neither a mask nor a clip-path reference. `box` supplies the masked layer's resolved size used
// to frame a contour clip-path SVG.
Expand Down
10 changes: 5 additions & 5 deletions src/pagx/html/importer/HTMLStyleCascade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -545,11 +545,11 @@ HTMLInheritedStyle HTMLStyleCascade::resolveInheritedStyle(const std::shared_ptr
out.textFillImage = ownBgImage;
}
// Split the CSS font-weight / font-style request into the real-face style label PAGX Text
// resolves plus the synthetic (faux) axes the renderer embosses on top. Bold (weight >= 600) and
// italic/oblique are baked as faux flags and dropped from the label so an uninstalled web face
// (e.g. "Noto Sans SC Black Italic") still renders at the authored weight and slant instead of
// collapsing to a thin upright fallback. Lighter weights (Light / Medium) cannot be synthesised
// and stay in the label.
// resolves plus the synthetic (faux) italic axis the renderer embosses on top. The weight axis
// is always written as a real-face keyword (Bold / SemiBold / Black) so the renderer resolves the
// authored heavy face when it is installed or embedded and preserves the SemiBold / Bold / Black
// distinction, falling back to host faux emboldening only for a missing face. Italic stays a faux
// flag so an oblique slant survives when the styled italic face is unavailable.
FontStyleSynthesis fontSynthesis = ResolveFontStyleSynthesis(out.fontWeight, out.fontStyle);
out.fontStyleName = fontSynthesis.fontStyleName;
out.fauxBold = fontSynthesis.fauxBold;
Expand Down
9 changes: 5 additions & 4 deletions src/pagx/html/importer/HTMLTextFragmentBuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ class HTMLTextFragmentBuilder {
struct TextFragment {
std::string text = {};
std::string fontFamily = {};
std::string fontStyleName = {}; // real-face style label, e.g. "Light" / "Medium" / ""
// Synthetic weight / slant baked in from the CSS request (see `ResolveFontStyleSynthesis`).
// Surface as `Text::fauxBold` / `Text::fauxItalic` so authored bold / italic survives a
// missing styled face on the render host.
std::string fontStyleName = {}; // real-face style label, e.g. "Light" / "Bold" / "Black" / ""
// Synthetic slant baked in from the CSS request (see `ResolveFontStyleSynthesis`). Surfaces as
// `Text::fauxItalic` so an authored oblique slant survives a missing styled italic face on the
// render host. The weight axis is never synthesised (it stays in `fontStyleName`), so
// `fauxBold` is always false.
bool fauxBold = false;
bool fauxItalic = false;
float fontSize = HTML_DEFAULT_FONT_SIZE;
Expand Down
Loading
Loading